PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.17
Yatra – Travel Booking & Tour Operator Software v3.0.17
3.0.17 3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 All 85 releases
yatra / resources / js / components / Tools.tsx

Tools.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.17, at resources/js/components/Tools.tsx

3,444 lines 140.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useState, useEffect, useRef } from "react";
2 import { useToast } from "../components/ui/toast";
3 import { Card } from "./ui/card";
4 import { Button } from "./ui/button";
5 import { Badge } from "./ui/badge";
6 import { Modal } from "./ui/modal";
7 import { __ } from "../lib/i18n";
8 import { todayYmd } from "../lib/dateFormat";
9 import { apiService } from "../lib/api-client";
10 import {
11 Download,
12 Upload,
13 Server,
14 FileText,
15 AlertCircle,
16 AlertTriangle,
17 CheckCircle,
18 XCircle,
19 X,
20 Trash2,
21 RefreshCw,
22 MapPin,
23 Globe,
24 Activity,
25 Folder,
26 Mountain,
27 Calendar,
28 Users,
29 UserCheck,
30 CreditCard,
31 Star,
32 MessageSquare,
33 Tag,
34 BarChart3,
35 List,
36 Settings,
37 Database,
38 Play,
39 Copy,
40 Eye,
41 } from "lucide-react";
42
43 interface SystemStatus {
44 php: {
45 version: string;
46 memory_limit: string;
47 max_execution_time: string;
48 upload_max_filesize: string;
49 post_max_size: string;
50 };
51 wordpress: {
52 version: string;
53 multisite: boolean;
54 debug_mode: boolean;
55 };
56 yatra: {
57 version: string;
58 plugin_path: string;
59 plugin_url: string;
60 };
61 database: {
62 version: string;
63 charset: string;
64 collate: string;
65 };
66 server: {
67 software: string;
68 php_sapi: string;
69 https: boolean;
70 };
71 extensions: Record<string, boolean>;
72 requirements: Record<
73 string,
74 {
75 required: string;
76 current: string;
77 status: "pass" | "fail" | "warning";
78 }
79 >;
80 }
81
82 interface LogEntry {
83 id: number;
84 timestamp: string;
85 level: string;
86 message: string;
87 context?: Record<string, any>;
88 }
89
90 interface LogsResponse {
91 logs: LogEntry[];
92 total: number;
93 page: number;
94 per_page: number;
95 pages: number;
96 }
97
98 interface JobStatus {
99 id: string;
100 type: "export" | "import";
101 status: "pending" | "running" | "completed" | "failed";
102 progress: number;
103 total_records: number;
104 processed_records: number;
105 file_path?: string;
106 file_url?: string;
107 error?: string;
108 created_at: string;
109 started_at?: string;
110 completed_at?: string;
111 import_stats?: Record<
112 string,
113 { total: number; imported: number; failed: number }
114 >;
115 seen_notification?: boolean;
116 }
117
118 interface CronJob {
119 hook: string;
120 next_run: number;
121 next_run_formatted: string;
122 next_run_relative: string;
123 schedule: string;
124 schedule_label: string;
125 interval: number;
126 args: any[];
127 is_overdue: boolean;
128 }
129
130 const MIGRATION_NOTICE_KEY = "yatra_migration_notice_dismissed_at";
131
132 const Tools: React.FC = () => {
133 const [activeTab, setActiveTab] = useState("export-import");
134 const [isExporting, setIsExporting] = useState(false);
135 const [isImporting, setIsImporting] = useState(false);
136 const [selectedExportData, setSelectedExportData] = useState<string[]>([
137 "trips",
138 "destinations",
139 "activities",
140 "bookings",
141 "customers",
142 ]);
143 const [selectedImportData, setSelectedImportData] = useState<string[]>([]);
144 const [isDragOver, setIsDragOver] = useState(false);
145 const [showExportModal, setShowExportModal] = useState(false);
146 const [showImportModal, setShowImportModal] = useState(false);
147 const [showDeleteModal, setShowDeleteModal] = useState(false);
148 const [showClearLogsModal, setShowClearLogsModal] = useState(false);
149 const [pendingImportFile, setPendingImportFile] = useState<File | null>(null);
150 const [systemStatus, setSystemStatus] = useState<SystemStatus | null>(null);
151 const [logs, setLogs] = useState<Record<string, LogsResponse>>({});
152 const [selectedLogType, setSelectedLogType] = useState("error");
153 const [isLoadingStatus, setIsLoadingStatus] = useState(false);
154 const [isLoadingLogs, setIsLoadingLogs] = useState(false);
155 const [isLoadingJobs, setIsLoadingJobs] = useState(false);
156 const [allJobs, setAllJobs] = useState<JobStatus[]>([]);
157 const [isClearingCache, setIsClearingCache] = useState(false);
158 const [showCacheModal, setShowCacheModal] = useState(false);
159 const [cacheData, setCacheData] = useState<any[]>([]);
160 const [isLoadingCache, setIsLoadingCache] = useState(false);
161 const [cronJobs, setCronJobs] = useState<CronJob[]>([]);
162 const [cronInfo, setCronInfo] = useState<{
163 wp_cron_disabled: boolean;
164 alternate_cron: boolean;
165 } | null>(null);
166 const [isLoadingCronJobs, setIsLoadingCronJobs] = useState(false);
167 const [runningCronJob, setRunningCronJob] = useState<string | null>(null);
168 const [migrationStatus, setMigrationStatus] = useState<any>(null);
169 const [isLoadingMigration, setIsLoadingMigration] = useState(false);
170 const [migrationProgress, setMigrationProgress] = useState<any>(null);
171 const [isMigrating, setIsMigrating] = useState(false);
172 const [isStartingMigration, setIsStartingMigration] = useState(false);
173 const migrationPollingRef = useRef<ReturnType<typeof setInterval> | null>(
174 null,
175 );
176 const [showMigrationConfirm, setShowMigrationConfirm] = useState(false);
177 const [showMigrationCompleteNotice, setShowMigrationCompleteNotice] =
178 useState(true);
179
180 // Sample Data states
181 const [isImportingSampleData, setIsImportingSampleData] = useState(false);
182 const [sampleDataStatus, setSampleDataStatus] = useState<any>(null);
183 const [sampleDataJob, setSampleDataJob] = useState<any>(null);
184
185 const { showToast } = useToast();
186
187 // Deep-link from WP admin notice: admin.php?page=yatra&subpage=tools&tools_tab=migration
188 useEffect(() => {
189 const allowed = new Set([
190 "export-import",
191 "jobs",
192 "system-status",
193 "logs",
194 "migration",
195 ]);
196 try {
197 const params = new URLSearchParams(window.location.search);
198 const t = params.get("tools_tab");
199 if (t && allowed.has(t)) {
200 setActiveTab(t);
201 }
202 } catch {
203 // ignore invalid URL
204 }
205 }, []);
206
207 // Helper function to format bytes
208 const formatBytes = (bytes: number): string => {
209 if (bytes === 0) return "0 Bytes";
210 const k = 1024;
211 const sizes = ["Bytes", "KB", "MB", "GB"];
212 const i = Math.floor(Math.log(bytes) / Math.log(k));
213 return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
214 };
215
216 useEffect(() => {
217 if (!migrationProgress?.started_at || !migrationProgress?.all_complete) {
218 setShowMigrationCompleteNotice(false);
219 if (typeof window !== "undefined") {
220 window.localStorage.removeItem(MIGRATION_NOTICE_KEY);
221 }
222 return;
223 }
224
225 if (typeof window === "undefined") {
226 setShowMigrationCompleteNotice(true);
227 return;
228 }
229
230 const dismissedAt = window.localStorage.getItem(MIGRATION_NOTICE_KEY);
231 if (dismissedAt && dismissedAt === migrationProgress.started_at) {
232 setShowMigrationCompleteNotice(false);
233 } else {
234 setShowMigrationCompleteNotice(true);
235 }
236 }, [migrationProgress?.started_at, migrationProgress?.all_complete]);
237
238 // Load sample data status on mount
239 useEffect(() => {
240 const loadSampleDataStatus = async () => {
241 try {
242 const status = await apiService.getSampleDataStatus();
243 setSampleDataStatus(status);
244 } catch (error) {
245 console.error("Failed to load sample data status:", error);
246 }
247 };
248
249 loadSampleDataStatus();
250 }, []);
251
252 // Background job states
253 const [exportJob, setExportJob] = useState<JobStatus | null>(null);
254 const [importJob, setImportJob] = useState<JobStatus | null>(null);
255 const pollingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(
256 null,
257 );
258
259 const logTypes = [
260 { key: "error", label: "Error Logs", icon: XCircle },
261 { key: "payment", label: "Payment Logs", icon: CheckCircle },
262 { key: "booking", label: "Booking Logs", icon: FileText },
263 { key: "system", label: "System Logs", icon: Server },
264 ];
265
266 // Available data types for export/import based on Yatra database structure
267 const dataTypes = [
268 {
269 key: "all",
270 label: "All Yatra data",
271 description:
272 "Complete backup: all tables, settings, itinerary, payments, and Pro data (if present)",
273 icon: Database,
274 },
275 {
276 key: "trips",
277 label: "Trips",
278 description: "All trip packages, itineraries, and details",
279 icon: MapPin,
280 },
281 {
282 key: "destinations",
283 label: "Destinations",
284 description: "Travel destinations and locations",
285 icon: Globe,
286 },
287 {
288 key: "activities",
289 label: "Activities",
290 description: "Trip activities and experiences",
291 icon: Activity,
292 },
293 {
294 key: "categories",
295 label: "Trip Categories",
296 description: "Trip categorization and taxonomy",
297 icon: Folder,
298 },
299 {
300 key: "difficulty_levels",
301 label: "Difficulty Levels",
302 description: "Trip difficulty classifications",
303 icon: Mountain,
304 },
305 {
306 key: "bookings",
307 label: "Bookings",
308 description: "Customer bookings and reservations",
309 icon: Calendar,
310 },
311 {
312 key: "customers",
313 label: "Customers",
314 description: "Customer profiles and CRM data",
315 icon: Users,
316 },
317 {
318 key: "travelers",
319 label: "Travelers",
320 description: "Individual traveler information",
321 icon: UserCheck,
322 },
323 {
324 key: "payments",
325 label: "Payments",
326 description: "Payment transactions and history",
327 icon: CreditCard,
328 },
329 {
330 key: "reviews",
331 label: "Reviews",
332 description: "Trip reviews and ratings",
333 icon: Star,
334 },
335 {
336 key: "enquiries",
337 label: "Enquiries",
338 description: "Customer enquiries and leads",
339 icon: MessageSquare,
340 },
341 {
342 key: "discounts",
343 label: "Discounts",
344 description: "Discount codes and promotions",
345 icon: Tag,
346 },
347 {
348 key: "availability",
349 label: "Availability",
350 description: "Trip availability and schedules",
351 icon: BarChart3,
352 },
353 {
354 key: "itinerary",
355 label: "Itinerary Items",
356 description: "Itinerary items and types",
357 icon: List,
358 },
359 {
360 key: "settings",
361 label: "Settings",
362 description: "Plugin configuration and settings",
363 icon: Settings,
364 },
365 ];
366
367 // Load system status
368 const loadSystemStatus = async () => {
369 setIsLoadingStatus(true);
370 try {
371 const data = await apiService.getSystemStatus();
372 // API returns data directly, not wrapped in success/data
373 setSystemStatus(data);
374 } catch (error) {
375 console.error("Failed to load system status:", error);
376 } finally {
377 setIsLoadingStatus(false);
378 }
379 };
380
381 // Load active jobs on mount (to show status when returning to page)
382 const loadActiveJobs = async () => {
383 try {
384 const jobs = await apiService.getActiveJobs();
385
386 if (Array.isArray(jobs) && jobs.length > 0) {
387 // Find most recent export and import jobs
388 const exportJobData = jobs.find((j: JobStatus) => j.type === "export");
389 const importJobData = jobs.find((j: JobStatus) => j.type === "import");
390
391 if (exportJobData) {
392 setExportJob(exportJobData);
393 // Resume polling if job is still running
394 if (
395 exportJobData.status === "pending" ||
396 exportJobData.status === "running"
397 ) {
398 setIsExporting(true);
399 pollJobStatus(exportJobData.id, "export");
400 }
401 }
402
403 if (importJobData) {
404 setImportJob(importJobData);
405 // Resume polling if job is still running
406 if (
407 importJobData.status === "pending" ||
408 importJobData.status === "running"
409 ) {
410 setIsImporting(true);
411 pollJobStatus(importJobData.id, "import");
412 }
413 }
414 }
415 } catch (error) {
416 console.error("Failed to load active jobs:", error);
417 }
418 };
419
420 // Load active jobs on component mount
421 useEffect(() => {
422 loadActiveJobs();
423
424 // We don't need to check localStorage for completed import jobs anymore
425 // The message will only appear once after import completes and then
426 // will be removed from localStorage when dismissed
427
428 // Load system status when tab is system-status
429 if (activeTab === "system-status") {
430 loadSystemStatus();
431 }
432 // eslint-disable-next-line react-hooks/exhaustive-deps
433 }, [activeTab]);
434
435 // Load logs
436 const loadLogs = async (type: string, page = 1) => {
437 setIsLoadingLogs(true);
438 try {
439 const data = await apiService.getLogs(type, page);
440 if (data.logs && Array.isArray(data.logs)) {
441 // The API returns: { logs: [...], total: number, page: number, per_page: number, pages: number }
442 const logsData = data; // Use the full response, not data.data
443
444 setLogs((prev) => ({ ...prev, [type]: logsData }));
445 }
446 } catch (error) {
447 console.error("Failed to load logs:", error);
448 } finally {
449 setIsLoadingLogs(false);
450 }
451 };
452
453 const selectableDataTypes = dataTypes.filter((dt) => dt.key !== "all");
454
455 // Handle data type selection for export
456 const handleExportDataToggle = (dataType: string) => {
457 if (dataType === "all") {
458 setSelectedExportData((prev) => (prev.includes("all") ? [] : ["all"]));
459 return;
460 }
461 setSelectedExportData((prev) => {
462 const withoutAll = prev.filter((t) => t !== "all");
463 return withoutAll.includes(dataType)
464 ? withoutAll.filter((t) => t !== dataType)
465 : [...withoutAll, dataType];
466 });
467 };
468
469 // Handle select all for export (individual types only, not the "all" preset)
470 const handleExportSelectAll = () => {
471 const keys = selectableDataTypes.map((dt) => dt.key);
472 const allRegularSelected =
473 keys.length > 0 &&
474 keys.every((k) => selectedExportData.includes(k)) &&
475 !selectedExportData.includes("all");
476 const onlyAll =
477 selectedExportData.length === 1 && selectedExportData.includes("all");
478
479 if (allRegularSelected || onlyAll) {
480 setSelectedExportData([]);
481 } else {
482 setSelectedExportData(keys);
483 }
484 };
485
486 // Handle data type selection for import
487 const handleImportDataToggle = (dataType: string) => {
488 if (dataType === "all") {
489 setSelectedImportData((prev) => (prev.includes("all") ? [] : ["all"]));
490 return;
491 }
492 setSelectedImportData((prev) => {
493 const withoutAll = prev.filter((t) => t !== "all");
494 return withoutAll.includes(dataType)
495 ? withoutAll.filter((t) => t !== dataType)
496 : [...withoutAll, dataType];
497 });
498 };
499
500 const handleImportSelectAll = () => {
501 const keys = selectableDataTypes.map((dt) => dt.key);
502 const allRegularSelected =
503 keys.length > 0 &&
504 keys.every((k) => selectedImportData.includes(k)) &&
505 !selectedImportData.includes("all");
506 const onlyAll =
507 selectedImportData.length === 1 && selectedImportData.includes("all");
508
509 if (allRegularSelected || onlyAll) {
510 setSelectedImportData([]);
511 } else {
512 setSelectedImportData(keys);
513 }
514 };
515
516 // Handle export button click - show modal
517 const handleExportClick = () => {
518 setShowExportModal(true);
519 };
520
521 // Handle actual export after data type selection - uses background job
522 const handleExport = async () => {
523 if (selectedExportData.length === 0) {
524 showToast(__("Please select at least one data type to export.", "yatra"), "error");
525 return;
526 }
527
528 setIsExporting(true);
529 setShowExportModal(false);
530 try {
531 // Create background export job
532 const data = await apiService.createExportJob({
533 data_types: selectedExportData,
534 });
535
536 // Start polling for job status
537 const jobId = data.job_id;
538 pollJobStatus(jobId, "export");
539 } catch (error) {
540 console.error("Export error:", error);
541 showToast(__("Export failed. Please try again.", "yatra"), "error");
542 setIsExporting(false);
543 }
544 };
545
546 // Stop polling
547 const stopPolling = () => {
548 if (pollingIntervalRef.current) {
549 clearInterval(pollingIntervalRef.current);
550 pollingIntervalRef.current = null;
551 }
552 };
553
554 // Poll job status
555 const pollJobStatus = (jobId: string, type: "export" | "import") => {
556 const endpoint = type === "export" ? "export-job" : "import-job";
557
558 const poll = async () => {
559 try {
560 const jobData = (await apiService.performJobAction(
561 endpoint,
562 jobId,
563 )) as JobStatus;
564 if (jobData) {
565 if (type === "export") {
566 setExportJob(jobData);
567 } else {
568 setImportJob(jobData);
569 }
570
571 // Check if job is complete or failed - stop polling
572 if (jobData.status === "completed" || jobData.status === "failed") {
573 stopPolling();
574
575 if (type === "export") {
576 setIsExporting(false);
577 } else {
578 setIsImporting(false);
579 }
580
581 if (jobData.status === "failed") {
582 showToast(
583 (type === "export"
584 ? __("Export failed: %s", "yatra")
585 : __("Import failed: %s", "yatra")
586 ).replace(
587 "%s",
588 jobData.error || __("Unknown error", "yatra"),
589 ),
590 "error",
591 );
592 }
593 }
594 }
595 } catch (error) {
596 console.error("Failed to poll job status:", error);
597 }
598 };
599
600 // Clear any existing polling first
601 stopPolling();
602
603 // Poll immediately, then every 2 seconds
604 poll();
605 pollingIntervalRef.current = setInterval(poll, 2000);
606 };
607
608 // Download completed export and delete file after download
609 const handleDownloadExport = async () => {
610 if (!exportJob || exportJob.status !== "completed") return;
611
612 try {
613 // First download the file
614 const blob = await apiService.downloadExportJobBlob(exportJob.id);
615 const url = window.URL.createObjectURL(blob);
616 const a = document.createElement("a");
617 a.style.display = "none";
618 a.href = url;
619 a.download = `yatra-export-${todayYmd()}.json`;
620 document.body.appendChild(a);
621 a.click();
622 window.URL.revokeObjectURL(url);
623 document.body.removeChild(a);
624
625 // Then delete the file from server after download
626 await apiService.deleteExportJob(exportJob.id);
627 // Clear the job after download and deletion
628 setExportJob(null);
629 } catch (error) {
630 console.error("Download error:", error);
631 showToast(__("Download failed. Please try again.", "yatra"), "error");
632 }
633 };
634
635 // Show delete confirmation modal
636 const handleDeleteClick = () => {
637 if (!exportJob) return;
638 setShowDeleteModal(true);
639 };
640
641 // Close delete confirmation modal
642 const handleCancelDelete = () => {
643 setShowDeleteModal(false);
644 };
645
646 // Delete export job and file after confirmation
647 const handleConfirmDelete = async () => {
648 if (!exportJob) return;
649 setShowDeleteModal(false);
650
651 try {
652 await apiService.deleteExportJob(exportJob.id);
653 // Immediately clear the export job from state to remove it from UI
654 setExportJob(null);
655 } catch (error) {
656 console.error("Delete error:", error);
657 showToast(__("Failed to delete export file.", "yatra"), "error");
658 }
659 };
660
661 // Load logs when logs tab becomes active
662 useEffect(() => {
663 if (activeTab === "logs") {
664 loadLogs(selectedLogType);
665 }
666 }, [activeTab, selectedLogType]);
667
668 // Handle file processing (for both input and drop) - uses background job
669 const processFile = async (file: File) => {
670 if (selectedImportData.length === 0) {
671 showToast(__("Please select at least one data type to import.", "yatra"), "error");
672 return;
673 }
674
675 setIsImporting(true);
676 try {
677 const formData = new FormData();
678 formData.append("file", file);
679 formData.append("data_types", JSON.stringify(selectedImportData));
680
681 // Create background import job
682 const data = await apiService.createImportJob(formData);
683
684 // Start polling for job status
685 const jobId = data.job_id;
686 pollJobStatus(jobId, "import");
687 setSelectedImportData([]);
688 } catch (error) {
689 console.error("Import error:", error);
690 showToast(__("Import failed. Please check the file format and try again.", "yatra"), "error");
691 setIsImporting(false);
692 }
693 };
694
695 // Import data from file input - show modal for data type selection
696 const handleImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
697 const file = event.target.files?.[0];
698 if (!file) return;
699
700 setPendingImportFile(file);
701 setShowImportModal(true);
702 event.target.value = "";
703 };
704
705 // Handle drag and drop events - show modal for data type selection
706 const handleDragOver = (e: React.DragEvent) => {
707 e.preventDefault();
708 setIsDragOver(true);
709 };
710
711 const handleDragLeave = (e: React.DragEvent) => {
712 e.preventDefault();
713 setIsDragOver(false);
714 };
715
716 const handleDrop = async (e: React.DragEvent) => {
717 e.preventDefault();
718 setIsDragOver(false);
719
720 const files = Array.from(e.dataTransfer.files);
721 const jsonFile = files.find(
722 (file) => file.type === "application/json" || file.name.endsWith(".json"),
723 );
724
725 if (!jsonFile) {
726 showToast(__("Please drop a valid JSON file.", "yatra"), "error");
727 return;
728 }
729
730 setPendingImportFile(jsonFile);
731 setShowImportModal(true);
732 };
733
734 // Handle actual import after data type selection
735 const handleImportConfirm = async () => {
736 if (!pendingImportFile) return;
737
738 setShowImportModal(false);
739 await processFile(pendingImportFile);
740 setPendingImportFile(null);
741 setSelectedImportData([]);
742 };
743
744 // Clear logs
745 const handleClearLogs = () => {
746 setShowClearLogsModal(true);
747 };
748
749 // Confirm clear logs
750 const confirmClearLogs = async () => {
751 try {
752 const data = await apiService.clearLogs(selectedLogType);
753 if (data.success) {
754 showToast(data.message || "Logs cleared successfully", "success");
755 setLogs((prev) => ({
756 ...prev,
757 [selectedLogType]: {
758 logs: [],
759 total: 0,
760 page: 1,
761 per_page: 50,
762 pages: 0,
763 },
764 }));
765 } else {
766 showToast(data.message || "Failed to clear logs", "error");
767 }
768 } catch (error) {
769 console.error("Failed to clear logs:", error);
770 showToast("Failed to clear logs. Please try again.", "error");
771 }
772 };
773
774 // Copy individual log to clipboard
775 const copyLogToClipboard = async (log: any) => {
776 const logText = `[${log.timestamp}] ${log.level.toUpperCase()}: ${log.message}${log.context ? "\nContext: " + JSON.stringify(log.context, null, 2) : ""}`;
777
778 try {
779 await navigator.clipboard.writeText(logText);
780 // You could add a toast notification here if you have one
781 } catch (error) {
782 console.error("Failed to copy log:", error);
783 // Fallback for older browsers
784 const textArea = document.createElement("textarea");
785 textArea.value = logText;
786 document.body.appendChild(textArea);
787 textArea.select();
788 document.execCommand("copy");
789 document.body.removeChild(textArea);
790 }
791 };
792
793 // Copy all logs to clipboard
794 const copyAllLogsToClipboard = async () => {
795 const currentLogs = logs[selectedLogType];
796 if (!currentLogs?.logs || currentLogs.logs.length === 0) return;
797
798 const allLogsText = currentLogs.logs
799 .map(
800 (log: any) =>
801 `[${log.timestamp}] ${log.level.toUpperCase()}: ${log.message}${log.context ? "\nContext: " + JSON.stringify(log.context, null, 2) : ""}`,
802 )
803 .join("\n\n");
804
805 try {
806 await navigator.clipboard.writeText(allLogsText);
807 // You could add a toast notification here if you have one
808 } catch (error) {
809 console.error("Failed to copy all logs:", error);
810 // Fallback for older browsers
811 const textArea = document.createElement("textarea");
812 textArea.value = allLogsText;
813 document.body.appendChild(textArea);
814 textArea.select();
815 document.execCommand("copy");
816 document.body.removeChild(textArea);
817 }
818 };
819
820 // Handle sample data import - imports all data automatically
821 const handleImportSampleData = async () => {
822 setIsImportingSampleData(true);
823 setSampleDataJob({ progress: 0 });
824
825 try {
826 // Import all sample data from JSON files automatically
827 const response = await apiService.importSampleData({
828 data_types: [], // Not used anymore - imports all data types
829 overwrite: false,
830 });
831
832 if (response.success) {
833 setSampleDataJob({ ...response.data, progress: 100 });
834
835 // Show detailed success message with counts
836 const data = response.data || {};
837 const message = `Successfully imported: ${data.trips || 0} trips, ${data.classifications || 0} classifications, ${data.items || 0} items, ${data.trip_classifications || 0} trip assignments, ${data.discounts || 0} discounts, ${data.availability_dates || 0} availability dates, ${data.availability_rules || 0} rules, ${data.itinerary_days || 0} itinerary days, ${data.itinerary_entries || 0} itinerary entries`;
838
839 showToast(message, "success");
840
841 // Refresh sample data status
842 const statusResponse = await apiService.getSampleDataStatus();
843 setSampleDataStatus(statusResponse);
844 } else {
845 throw new Error(response.message || __("Import failed"));
846 }
847 } catch (error: any) {
848 console.error("Sample data import error:", error);
849 showToast(
850 error.message || __("Failed to import sample data. Please try again."),
851 "error",
852 );
853 } finally {
854 setIsImportingSampleData(false);
855 setTimeout(() => setSampleDataJob(null), 3000);
856 }
857 };
858
859 // Get status badge
860 const getStatusBadge = (status: string) => {
861 switch (status) {
862 case "pass":
863 return (
864 <Badge variant="success">
865 <CheckCircle className="w-3 h-3 mr-1" />
866 Pass
867 </Badge>
868 );
869 case "warning":
870 return (
871 <Badge variant="warning">
872 <AlertCircle className="w-3 h-3 mr-1" />
873 Warning
874 </Badge>
875 );
876 case "fail":
877 return (
878 <Badge variant="error">
879 <XCircle className="w-3 h-3 mr-1" />
880 Fail
881 </Badge>
882 );
883 default:
884 return <Badge variant="outline">Unknown</Badge>;
885 }
886 };
887
888 // Get log level badge
889 const getLogLevelBadge = (level: string) => {
890 switch (level.toLowerCase()) {
891 case "error":
892 return <Badge variant="error">Error</Badge>;
893 case "warning":
894 return <Badge variant="warning">Warning</Badge>;
895 case "info":
896 return <Badge variant="info">Info</Badge>;
897 default:
898 return <Badge variant="outline">{level}</Badge>;
899 }
900 };
901
902 // Load all jobs
903 const loadAllJobs = async () => {
904 setIsLoadingJobs(true);
905 try {
906 const data = await apiService.getAllJobs();
907 // API returns data directly, not wrapped in {success, data}
908 setAllJobs(Array.isArray(data) ? data : []);
909 } catch (error) {
910 console.error("Failed to load jobs:", error);
911 } finally {
912 setIsLoadingJobs(false);
913 }
914 };
915
916 // Load cron jobs
917 const loadCronJobs = async () => {
918 setIsLoadingCronJobs(true);
919 try {
920 const data = await apiService.getCronJobs();
921 // API returns data directly: {cron_jobs: [], wp_cron_disabled: bool, ...}
922 setCronJobs(data?.cron_jobs || []);
923 setCronInfo({
924 wp_cron_disabled: data?.wp_cron_disabled || false,
925 alternate_cron: data?.alternate_cron || false,
926 });
927 } catch (error) {
928 console.error("Failed to load cron jobs:", error);
929 } finally {
930 setIsLoadingCronJobs(false);
931 }
932 };
933
934 // Run a cron job manually
935 const handleRunCronJob = async (hook: string) => {
936 setRunningCronJob(hook);
937 try {
938 const data = await apiService.runCronJob(hook);
939 // API returns data directly: {success: true, message: ...}
940 if (data.success) {
941 showToast(
942 data.message || `Cron job "${hook}" executed successfully`,
943 "success",
944 );
945 // Reload cron jobs to update next run times
946 loadCronJobs();
947 } else {
948 showToast(data.message || "Failed to run cron job", "error");
949 }
950 } catch (error) {
951 console.error("Failed to run cron job:", error);
952 showToast("Failed to run cron job", "error");
953 } finally {
954 setRunningCronJob(null);
955 }
956 };
957
958 // Load cache data for viewing
959 const loadCacheData = async () => {
960 setIsLoadingCache(true);
961 try {
962 const data = await apiService.getCacheView();
963
964 // API returns consistent structure: {success, message, data}
965 if (data.success && data.data) {
966 setCacheData(data.data.cache_data || []);
967 showToast(data.message || "Cache data loaded successfully", "success");
968 } else {
969 showToast(data.message || "Failed to load cache data", "error");
970 }
971 } catch (error) {
972 console.error("Failed to load cache data:", error);
973 showToast("Failed to load cache data", "error");
974 } finally {
975 setIsLoadingCache(false);
976 }
977 };
978
979 // Clear individual cache item
980 const clearCacheItem = async (key: string, type: string) => {
981 try {
982 const data = await apiService.clearCacheItem(key, type);
983
984 // API returns consistent structure: {success, message, data}
985 if (data.success) {
986 // Remove the item from the cache data
987 setCacheData((prev) => prev.filter((item) => item.key !== key));
988 showToast(
989 data.message || `Cache item "${key}" cleared successfully`,
990 "success",
991 );
992 } else {
993 showToast(data.message || "Failed to clear cache item", "error");
994 }
995 } catch (error) {
996 console.error("Failed to clear cache item:", error);
997 showToast("Failed to clear cache item", "error");
998 }
999 };
1000
1001 // Clear all caches
1002 const clearAllCache = async () => {
1003 setIsClearingCache(true);
1004 try {
1005 const data = await apiService.clearCache();
1006
1007 // Clear React Query cache if available
1008 if (
1009 (window as any).yatraQueryClient &&
1010 typeof (window as any).yatraQueryClient.invalidateQueries === "function"
1011 ) {
1012 (window as any).yatraQueryClient.invalidateQueries();
1013 }
1014
1015 // Fixed handling of response
1016 if (data.success) {
1017 // Only show success message if both response.ok and data.success are true
1018 showToast(
1019 "All caches cleared successfully! Please refresh the page to see the changes.",
1020 "success",
1021 );
1022 } else {
1023 // Create a clean error message without concatenation issues
1024 let errorMessage = "Failed to clear caches";
1025
1026 // Only append additional details if they don't create a contradictory message
1027 if (data.message && !data.message.includes("success")) {
1028 errorMessage += ": " + data.message;
1029 }
1030
1031 showToast(errorMessage, "error");
1032 }
1033 } catch (error) {
1034 console.error("Failed to clear caches:", error);
1035 showToast("Failed to clear caches. Please try again.", "error");
1036 } finally {
1037 setIsClearingCache(false);
1038 }
1039 };
1040
1041 // Load migration status
1042 const loadMigrationStatus = async () => {
1043 setIsLoadingMigration(true);
1044 try {
1045 const data = await apiService.getMigrationStatus();
1046 setMigrationStatus(data);
1047 } catch (error) {
1048 console.error("Failed to load migration status:", error);
1049 showToast("Failed to load migration status", "error");
1050 } finally {
1051 setIsLoadingMigration(false);
1052 }
1053 };
1054
1055 const handleDismissMigrationNotice = async () => {
1056 try {
1057 await apiService.clearMigration();
1058 setShowMigrationCompleteNotice(false);
1059 await loadMigrationProgress();
1060 if (typeof window !== "undefined") {
1061 window.localStorage.removeItem(MIGRATION_NOTICE_KEY);
1062 }
1063 } catch (error) {
1064 showToast("Failed to clear migration data.", "error");
1065 }
1066 };
1067
1068 // Load migration progress
1069 const loadMigrationProgress = async () => {
1070 try {
1071 const data = await apiService.getMigrationProgress();
1072
1073 setMigrationProgress(data);
1074
1075 // If migration is still running, keep polling
1076 if (data.any_running && !data.all_complete) {
1077 setIsMigrating(true);
1078 // Start polling if not already polling
1079 if (!migrationPollingRef.current) {
1080 migrationPollingRef.current = setInterval(
1081 loadMigrationProgress,
1082 3000,
1083 );
1084 }
1085 } else if (data.all_complete && data.started_at) {
1086 setIsMigrating(false);
1087 stopMigrationPolling();
1088 }
1089 } catch (error) {
1090 console.error("Failed to load migration progress:", error);
1091 }
1092 };
1093
1094 // Start polling migration progress
1095 const startMigrationPolling = () => {
1096 if (migrationPollingRef.current) {
1097 clearInterval(migrationPollingRef.current);
1098 }
1099
1100 loadMigrationProgress();
1101 // Poll every 1 second for more frequent progress updates
1102 migrationPollingRef.current = setInterval(loadMigrationProgress, 1000);
1103 };
1104
1105 // Stop polling migration progress
1106 const stopMigrationPolling = () => {
1107 if (migrationPollingRef.current) {
1108 clearInterval(migrationPollingRef.current);
1109 migrationPollingRef.current = null;
1110 }
1111 };
1112
1113 // Migrate all data types
1114 const handleMigrateAll = async (force = false) => {
1115 setIsMigrating(true);
1116 setIsStartingMigration(true);
1117 try {
1118 const data = await apiService.runMigrationAll({ force });
1119
1120 if (data.success) {
1121 showToast(
1122 "Migration started for all data types. Processing in background...",
1123 "success",
1124 );
1125 if (Array.isArray(data.warnings) && data.warnings.length > 0) {
1126 showToast(String(data.warnings[0]), "warning");
1127 }
1128 // Immediately load progress to initialize the display
1129 await loadMigrationProgress();
1130 startMigrationPolling();
1131 setIsStartingMigration(false);
1132 } else {
1133 showToast(data.error || data.message || "Migration failed", "error");
1134 setIsMigrating(false);
1135 setIsStartingMigration(false);
1136 }
1137 } catch (error) {
1138 console.error("Migration error:", error);
1139 showToast("Migration failed. Please try again.", "error");
1140 setIsMigrating(false);
1141 setIsStartingMigration(false);
1142 }
1143 };
1144
1145 // Cancel migration
1146 const handleCancelMigration = async () => {
1147 if (
1148 !confirm(
1149 "Are you sure you want to cancel the migration? This will stop all ongoing migrations.",
1150 )
1151 ) {
1152 return;
1153 }
1154
1155 try {
1156 const data = await apiService.cancelMigration();
1157
1158 if (data.success) {
1159 showToast("Migration cancelled successfully", "success");
1160 setIsMigrating(false);
1161 stopMigrationPolling();
1162 loadMigrationStatus();
1163 } else {
1164 showToast(data.error || "Failed to cancel migration", "error");
1165 }
1166 } catch (error) {
1167 console.error("Cancel migration error:", error);
1168 showToast("Failed to cancel migration", "error");
1169 }
1170 };
1171
1172 useEffect(() => {
1173 if (activeTab === "system-status") {
1174 loadSystemStatus();
1175 } else if (activeTab === "logs") {
1176 loadLogs(selectedLogType);
1177 } else if (activeTab === "jobs") {
1178 loadAllJobs();
1179 loadCronJobs();
1180 } else if (activeTab === "migration") {
1181 loadMigrationStatus();
1182 loadMigrationProgress();
1183 }
1184
1185 // Cleanup migration polling when leaving tab
1186 return () => {
1187 if (activeTab === "migration") {
1188 stopMigrationPolling();
1189 }
1190 };
1191 // eslint-disable-next-line react-hooks/exhaustive-deps
1192 }, [activeTab, selectedLogType]);
1193
1194 return (
1195 <div>
1196 <div className="flex items-center justify-between mb-6">
1197 <div>
1198 <h1 className="text-2xl font-bold text-gray-900 dark:text-white">
1199 Tools
1200 </h1>
1201 <p className="text-gray-600 dark:text-gray-400">
1202 Export/Import data, check system status, and view logs
1203 </p>
1204 </div>
1205 <div className="flex items-center gap-3">
1206 <Button
1207 onClick={() =>
1208 window.open("/wp-admin/admin.php?page=yatra-setup", "_blank")
1209 }
1210 variant="outline"
1211 className="flex items-center gap-2"
1212 >
1213 <Settings className="w-4 h-4" />
1214 Setup Wizard
1215 </Button>
1216 <Button
1217 onClick={clearAllCache}
1218 variant="outline"
1219 className="flex items-center gap-2"
1220 disabled={isClearingCache}
1221 >
1222 <RefreshCw
1223 className={`w-4 h-4 ${isClearingCache ? "animate-spin" : ""}`}
1224 />
1225 {isClearingCache ? "Clearing..." : "Clear Cache"}
1226 </Button>
1227 <Button
1228 onClick={() => {
1229 setShowCacheModal(true);
1230 loadCacheData();
1231 }}
1232 variant="outline"
1233 className="flex items-center gap-2"
1234 >
1235 <Eye className="w-4 h-4" />
1236 View Cache
1237 </Button>
1238 </div>
1239 </div>
1240
1241 <div className="space-y-6">
1242 {/* Tab Navigation - Clean and Spacious Design */}
1243 <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-1 mb-8">
1244 <nav className="flex space-x-1">
1245 <button
1246 onClick={() => setActiveTab("export-import")}
1247 className={`flex-1 py-3 px-6 rounded-md font-medium text-sm flex items-center justify-center gap-3 transition-all duration-200 ${
1248 activeTab === "export-import"
1249 ? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 shadow-sm"
1250 : "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700/50"
1251 }`}
1252 >
1253 <Download className="w-5 h-5" />
1254 <span>Export/Import</span>
1255 </button>
1256 <button
1257 onClick={() => setActiveTab("jobs")}
1258 className={`flex-1 py-3 px-6 rounded-md font-medium text-sm flex items-center justify-center gap-3 transition-all duration-200 ${
1259 activeTab === "jobs"
1260 ? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 shadow-sm"
1261 : "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700/50"
1262 }`}
1263 >
1264 <List className="w-5 h-5" />
1265 <span>Jobs</span>
1266 </button>
1267 <button
1268 onClick={() => setActiveTab("system-status")}
1269 className={`flex-1 py-3 px-6 rounded-md font-medium text-sm flex items-center justify-center gap-3 transition-all duration-200 ${
1270 activeTab === "system-status"
1271 ? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 shadow-sm"
1272 : "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700/50"
1273 }`}
1274 >
1275 <Server className="w-5 h-5" />
1276 <span>System Status</span>
1277 </button>
1278 <button
1279 onClick={() => setActiveTab("logs")}
1280 className={`flex-1 py-3 px-6 rounded-md font-medium text-sm flex items-center justify-center gap-3 transition-all duration-200 ${
1281 activeTab === "logs"
1282 ? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 shadow-sm"
1283 : "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700/50"
1284 }`}
1285 >
1286 <FileText className="w-5 h-5" />
1287 <span>Logs</span>
1288 </button>
1289 <button
1290 onClick={() => setActiveTab("migration")}
1291 className={`flex-1 py-3 px-6 rounded-md font-medium text-sm flex items-center justify-center gap-3 transition-all duration-200 ${
1292 activeTab === "migration"
1293 ? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 shadow-sm"
1294 : "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700/50"
1295 }`}
1296 >
1297 <Database className="w-5 h-5" />
1298 <span>Migration</span>
1299 </button>
1300 </nav>
1301 </div>
1302
1303 {/* Export/Import Tab */}
1304 {activeTab === "export-import" && (
1305 <div className="space-y-8">
1306 <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
1307 {/* Export Section */}
1308 <Card className="p-8">
1309 <div className="flex items-center gap-4 mb-6">
1310 <div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
1311 <Download className="w-6 h-6 text-blue-600 dark:text-blue-400" />
1312 </div>
1313 <div>
1314 <h3 className="text-xl font-semibold text-gray-900 dark:text-white">
1315 Export Data
1316 </h3>
1317 <p className="text-sm text-gray-500 dark:text-gray-400">
1318 Download your data
1319 </p>
1320 </div>
1321 </div>
1322 <p className="text-gray-600 dark:text-gray-400 mb-6 leading-relaxed">
1323 Export your Yatra data for backup or migration purposes. Click
1324 the button below to select which data types to export.
1325 </p>
1326
1327 {/* Export Job Progress */}
1328 {exportJob &&
1329 (exportJob.status === "pending" ||
1330 exportJob.status === "running") && (
1331 <div className="mb-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
1332 <div className="flex items-center justify-between mb-2">
1333 <span className="text-sm font-medium text-blue-700 dark:text-blue-400">
1334 {exportJob.status === "pending"
1335 ? "Queued..."
1336 : "Exporting..."}
1337 </span>
1338 <span className="text-sm text-blue-600 dark:text-blue-400">
1339 {exportJob.progress}%
1340 </span>
1341 </div>
1342 <div className="w-full bg-blue-200 dark:bg-blue-800 rounded-full h-2">
1343 <div
1344 className="bg-blue-600 dark:bg-blue-400 h-2 rounded-full transition-all duration-300"
1345 style={{ width: `${exportJob.progress}%` }}
1346 />
1347 </div>
1348 <p className="text-xs text-blue-600 dark:text-blue-400 mt-2">
1349 {exportJob.processed_records} /{" "}
1350 {exportJob.total_records} records processed
1351 </p>
1352 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1353 Processing in background. You can close this page and
1354 come back later.
1355 </p>
1356 </div>
1357 )}
1358
1359 {/* Export Complete - Download Button */}
1360 {exportJob && exportJob.status === "completed" && (
1361 <div className="mb-4 p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
1362 <div className="flex items-center gap-2 mb-3">
1363 <CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400" />
1364 <span className="text-sm font-medium text-green-700 dark:text-green-400">
1365 Export completed successfully!
1366 </span>
1367 </div>
1368 <div className="flex gap-2">
1369 <Button
1370 onClick={handleDownloadExport}
1371 className="flex-1"
1372 variant="outline"
1373 >
1374 <Download className="w-4 h-4 mr-2" />
1375 Download
1376 </Button>
1377 <Button
1378 onClick={handleDeleteClick}
1379 variant="outline"
1380 className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20"
1381 >
1382 <XCircle className="w-4 h-4" />
1383 </Button>
1384 </div>
1385 </div>
1386 )}
1387
1388 <Button
1389 onClick={handleExportClick}
1390 disabled={
1391 isExporting ||
1392 !!(
1393 exportJob &&
1394 (exportJob.status === "pending" ||
1395 exportJob.status === "running")
1396 )
1397 }
1398 className="w-full py-3"
1399 size="lg"
1400 >
1401 {isExporting ? (
1402 <>
1403 <RefreshCw className="w-5 h-5 mr-3 animate-spin" />
1404 Starting export...
1405 </>
1406 ) : (
1407 <>
1408 <Download className="w-5 h-5 mr-3" />
1409 Select Data Types to Export
1410 </>
1411 )}
1412 </Button>
1413 </Card>
1414
1415 {/* Import Section */}
1416 <Card className="p-8">
1417 <div className="flex items-center gap-4 mb-6">
1418 <div className="p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
1419 <Upload className="w-6 h-6 text-green-600 dark:text-green-400" />
1420 </div>
1421 <div>
1422 <h3 className="text-xl font-semibold text-gray-900 dark:text-white">
1423 Import Data
1424 </h3>
1425 <p className="text-sm text-gray-500 dark:text-gray-400">
1426 Restore from backup
1427 </p>
1428 </div>
1429 </div>
1430 <p className="text-gray-600 dark:text-gray-400 mb-6 leading-relaxed">
1431 Import Yatra data from a previously exported JSON file. Drop
1432 your file below or click to browse.
1433 </p>
1434
1435 {/* Drag and Drop Zone */}
1436 <div
1437 onDragOver={handleDragOver}
1438 onDragLeave={handleDragLeave}
1439 onDrop={handleDrop}
1440 className={`relative border-2 border-dashed rounded-lg p-8 text-center transition-all duration-200 ${
1441 isDragOver
1442 ? "border-green-400 bg-green-50 dark:bg-green-900/20"
1443 : "border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 hover:border-green-400 hover:bg-green-50 dark:hover:bg-green-900/20"
1444 } ${isImporting ? "pointer-events-none opacity-50" : ""}`}
1445 >
1446 <input
1447 type="file"
1448 accept=".json"
1449 onChange={handleImport}
1450 disabled={isImporting}
1451 className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed"
1452 />
1453 <div className="flex flex-col items-center gap-4">
1454 <div
1455 className={`p-4 rounded-full ${
1456 isDragOver
1457 ? "bg-green-100 dark:bg-green-900/30"
1458 : "bg-gray-100 dark:bg-gray-700"
1459 }`}
1460 >
1461 <Upload
1462 className={`w-8 h-8 ${
1463 isDragOver
1464 ? "text-green-600 dark:text-green-400"
1465 : "text-gray-400 dark:text-gray-500"
1466 }`}
1467 />
1468 </div>
1469 <div>
1470 <p className="text-lg font-medium text-gray-900 dark:text-white mb-2">
1471 {isDragOver
1472 ? "Drop your JSON file here"
1473 : "Drop JSON file or click to browse"}
1474 </p>
1475 <p className="text-sm text-gray-500 dark:text-gray-400">
1476 Supports JSON files exported from Yatra
1477 </p>
1478 </div>
1479 </div>
1480 </div>
1481
1482 {/* Import Job Progress */}
1483 {importJob &&
1484 (importJob.status === "pending" ||
1485 importJob.status === "running") && (
1486 <div className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
1487 <div className="flex items-center justify-between mb-2">
1488 <span className="text-sm font-medium text-green-700 dark:text-green-400">
1489 {importJob.status === "pending"
1490 ? "Queued..."
1491 : "Importing..."}
1492 </span>
1493 <span className="text-sm text-green-600 dark:text-green-400">
1494 {importJob.progress}%
1495 </span>
1496 </div>
1497 <div className="w-full bg-green-200 dark:bg-green-800 rounded-full h-2">
1498 <div
1499 className="bg-green-600 dark:bg-green-400 h-2 rounded-full transition-all duration-300"
1500 style={{ width: `${importJob.progress}%` }}
1501 />
1502 </div>
1503 <p className="text-xs text-green-600 dark:text-green-400 mt-2">
1504 {importJob.processed_records} /{" "}
1505 {importJob.total_records} records processed
1506 </p>
1507 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
1508 Processing in background. You can close this page and
1509 come back later.
1510 </p>
1511 </div>
1512 )}
1513
1514 {/* Import Complete - with detailed statistics and shown only once */}
1515 {importJob && importJob.status === "completed" && (
1516 <div className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
1517 <div className="flex items-center gap-2 mb-2">
1518 <CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400" />
1519 <span className="text-sm font-medium text-green-700 dark:text-green-400">
1520 {__("Import completed successfully!", "yatra")}
1521 </span>
1522 <button
1523 onClick={async () => {
1524 if (!importJob) return;
1525
1526 // Call API to delete the import job and file
1527 try {
1528 await apiService.deleteImportJob(importJob.id);
1529 } catch (e) {
1530 console.error("Error deleting import job:", e);
1531 }
1532
1533 // Clear from state
1534 setImportJob(null);
1535 }}
1536 className="ml-auto text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
1537 >
1538 <XCircle className="w-4 h-4" />
1539 </button>
1540 </div>
1541
1542 {(() => {
1543 // "0 imported" on its own reads as a failed import. A
1544 // re-import is supposed to bring in nothing: the rows are
1545 // already here and get skipped, which is the feature
1546 // working. Say so, rather than leaving a column of zeroes
1547 // and a red failure count to speak for themselves.
1548 const totals = Object.values(
1549 importJob.import_stats || {},
1550 ).reduce(
1551 (acc: any, s: any) => ({
1552 imported: acc.imported + (s.imported || 0),
1553 skipped: acc.skipped + (s.skipped || 0),
1554 failed: acc.failed + (s.failed || 0),
1555 }),
1556 { imported: 0, skipped: 0, failed: 0 },
1557 ) as { imported: number; skipped: number; failed: number };
1558
1559 return (
1560 <div className="text-sm text-green-700 dark:text-green-400 mb-2">
1561 <strong>{totals.imported}</strong>{" "}
1562 {__("imported", "yatra")}
1563 {totals.skipped > 0 && (
1564 <>
1565 {", "}
1566 <strong>{totals.skipped}</strong>{" "}
1567 {__("already here", "yatra")}
1568 </>
1569 )}
1570 {totals.failed > 0 && (
1571 <>
1572 {", "}
1573 <strong>{totals.failed}</strong>{" "}
1574 {__("could not be linked", "yatra")}
1575 </>
1576 )}
1577 {totals.imported === 0 && totals.skipped > 0 && (
1578 <div className="text-xs mt-1">
1579 {__(
1580 "Everything in this file was already on the site, so nothing was duplicated.",
1581 "yatra",
1582 )}
1583 </div>
1584 )}
1585 </div>
1586 );
1587 })()}
1588
1589 {/* Detailed statistics by data type */}
1590 {importJob.import_stats &&
1591 Object.keys(importJob.import_stats).length > 0 && (
1592 <div className="mt-2 border-t border-green-200 dark:border-green-800 pt-2">
1593 <div className="text-xs font-medium text-green-700 dark:text-green-400 mb-2">
1594 {__("Import Details:", "yatra")}
1595 </div>
1596 <div className="grid grid-cols-1 gap-1">
1597 {Object.entries(importJob.import_stats).map(
1598 ([dataType, stats]: [string, any]) => {
1599 // Find the matching data type object for the icon
1600 const dataTypeObj = dataTypes.find(
1601 (dt) => dt.key === dataType,
1602 );
1603 const Icon = dataTypeObj?.icon || FileText;
1604
1605 return (
1606 <div
1607 key={dataType}
1608 className="flex items-center justify-between text-xs"
1609 >
1610 <div className="flex items-center gap-1">
1611 <Icon className="w-3 h-3 text-green-600 dark:text-green-400" />
1612 <span className="capitalize">
1613 {dataType.replace(/_/g, " ")}:
1614 </span>
1615 </div>
1616 <div>
1617 <span className="text-green-700 dark:text-green-400">
1618 {stats.imported}
1619 </span>
1620 {stats.skipped > 0 && (
1621 <span
1622 className="text-gray-500 dark:text-gray-400 ml-1"
1623 title={__(
1624 "Already present on this site, so not imported again",
1625 "yatra",
1626 )}
1627 >
1628 ({stats.skipped}{" "}
1629 {__("already here", "yatra")})
1630 </span>
1631 )}
1632 {stats.failed > 0 && (
1633 <span
1634 className="text-amber-600 dark:text-amber-400 ml-1"
1635 title={__(
1636 "These rows point at a record the export file does not contain — usually something deleted on the original site — so there was nothing to attach them to.",
1637 "yatra",
1638 )}
1639 >
1640 ({stats.failed}{" "}
1641 {__("not linked", "yatra")})
1642 </span>
1643 )}
1644 </div>
1645 </div>
1646 );
1647 },
1648 )}
1649 </div>
1650 </div>
1651 )}
1652
1653 {/* We don't need to mark as seen anymore since we'll remove from localStorage on dismiss */}
1654 </div>
1655 )}
1656
1657 {/* Import Failed */}
1658 {importJob && importJob.status === "failed" && (
1659 <div className="mt-4 p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">
1660 <div className="flex items-center gap-2">
1661 <XCircle className="w-5 h-5 text-red-600 dark:text-red-400" />
1662 <span className="text-sm font-medium text-red-700 dark:text-red-400">
1663 Import failed: {importJob.error || "Unknown error"}
1664 </span>
1665 </div>
1666 </div>
1667 )}
1668
1669 {isImporting && !importJob && (
1670 <div className="flex items-center justify-center gap-3 text-sm text-gray-600 dark:text-gray-400 bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg mt-4">
1671 <RefreshCw className="w-5 h-5 animate-spin" />
1672 <span>Starting import...</span>
1673 </div>
1674 )}
1675 </Card>
1676 </div>
1677
1678 {/* Import Warning */}
1679 <Card className="p-6 bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800">
1680 <div className="flex items-start gap-4">
1681 <div className="p-2 bg-yellow-100 dark:bg-yellow-900/30 rounded-lg">
1682 <AlertCircle className="w-5 h-5 text-yellow-600 dark:text-yellow-400" />
1683 </div>
1684 <div>
1685 <h4 className="font-semibold text-yellow-800 dark:text-yellow-200 mb-2">
1686 Important Notice
1687 </h4>
1688 <p className="text-sm text-yellow-700 dark:text-yellow-300 leading-relaxed">
1689 Importing data will create new records. Existing data will
1690 not be overwritten. Always backup your database before
1691 importing data to prevent any potential data loss.
1692 </p>
1693 </div>
1694 </div>
1695 </Card>
1696
1697 {/* Sample Data Section */}
1698 <Card className="p-8">
1699 <div className="flex items-center gap-4 mb-6">
1700 <div className="p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
1701 <Database className="w-6 h-6 text-purple-600 dark:text-purple-400" />
1702 </div>
1703 <div>
1704 <h3 className="text-xl font-semibold text-gray-900 dark:text-white">
1705 Import Sample Data
1706 </h3>
1707 <p className="text-sm text-gray-500 dark:text-gray-400">
1708 Quick setup with demo content
1709 </p>
1710 </div>
1711 </div>
1712 <p className="text-gray-600 dark:text-gray-400 mb-6 leading-relaxed">
1713 Quickly populate your Yatra site with sample trips, categories,
1714 destinations, and more. Perfect for testing and demonstration
1715 purposes.
1716 </p>
1717
1718 {/* What Will Be Imported */}
1719 <div className="space-y-4 mb-6">
1720 <div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-4">
1721 <h4 className="font-semibold text-blue-900 dark:text-blue-100 mb-2 flex items-center gap-2">
1722 <CheckCircle className="w-4 h-4" />
1723 The following data will be imported automatically:
1724 </h4>
1725 <div className="grid grid-cols-1 md:grid-cols-2 gap-2 text-sm text-blue-800 dark:text-blue-200">
1726 <div className="flex items-center gap-2">
1727 <MapPin className="w-4 h-4" />
1728 <span>11 Sample Trips (multi-day & single-day)</span>
1729 </div>
1730 <div className="flex items-center gap-2">
1731 <Tag className="w-4 h-4" />
1732 <span>8 Categories</span>
1733 </div>
1734 <div className="flex items-center gap-2">
1735 <Activity className="w-4 h-4" />
1736 <span>10 Activities</span>
1737 </div>
1738 <div className="flex items-center gap-2">
1739 <Globe className="w-4 h-4" />
1740 <span>10 Destinations</span>
1741 </div>
1742 <div className="flex items-center gap-2">
1743 <Mountain className="w-4 h-4" />
1744 <span>6 Difficulty Levels</span>
1745 </div>
1746 <div className="flex items-center gap-2">
1747 <Settings className="w-4 h-4" />
1748 <span>8 Attributes</span>
1749 </div>
1750 <div className="flex items-center gap-2">
1751 <List className="w-4 h-4" />
1752 <span>6 Itinerary Item Types</span>
1753 </div>
1754 <div className="flex items-center gap-2">
1755 <Users className="w-4 h-4" />
1756 <span>8 Traveler Categories</span>
1757 </div>
1758 <div className="flex items-center gap-2">
1759 <Tag className="w-4 h-4" />
1760 <span>8 Discount Codes</span>
1761 </div>
1762 <div className="flex items-center gap-2">
1763 <Calendar className="w-4 h-4" />
1764 <span>21 Availability Dates</span>
1765 </div>
1766 <div className="flex items-center gap-2">
1767 <RefreshCw className="w-4 h-4" />
1768 <span>9 Availability Rules</span>
1769 </div>
1770 <div className="flex items-center gap-2">
1771 <FileText className="w-4 h-4" />
1772 <span>31 Itinerary Days</span>
1773 </div>
1774 <div className="flex items-center gap-2">
1775 <List className="w-4 h-4" />
1776 <span>25 Itinerary Entries</span>
1777 </div>
1778 </div>
1779 </div>
1780 </div>
1781
1782 {/* Action Buttons */}
1783 <div className="flex gap-3">
1784 <Button
1785 onClick={handleImportSampleData}
1786 disabled={isImportingSampleData}
1787 className="flex-1"
1788 size="lg"
1789 >
1790 {isImportingSampleData ? (
1791 <>
1792 <RefreshCw className="w-5 h-5 mr-2 animate-spin" />
1793 Importing...
1794 </>
1795 ) : (
1796 <>
1797 <Download className="w-5 h-5 mr-2" />
1798 Import Sample Data
1799 </>
1800 )}
1801 </Button>
1802 </div>
1803 </Card>
1804 </div>
1805 )}
1806
1807 {/* System Status Tab */}
1808 {activeTab === "system-status" && (
1809 <div className="space-y-6">
1810 {isLoadingStatus ? (
1811 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
1812 {/* System Status Skeleton */}
1813 {[...Array(4)].map((_, index) => (
1814 <Card key={index} className="p-6">
1815 <div className="space-y-4">
1816 <div className="flex items-center gap-3">
1817 <div className="w-5 h-5 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
1818 <div className="h-5 bg-gray-200 dark:bg-gray-700 rounded animate-pulse w-32"></div>
1819 </div>
1820 <div className="space-y-3">
1821 {[...Array(4)].map((_, i) => (
1822 <div
1823 key={i}
1824 className="flex justify-between items-center"
1825 >
1826 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse w-24"></div>
1827 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse w-16"></div>
1828 </div>
1829 ))}
1830 </div>
1831 </div>
1832 </Card>
1833 ))}
1834 </div>
1835 ) : systemStatus ? (
1836 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
1837 {/* PHP Information */}
1838 <Card className="p-6">
1839 <h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
1840 <Server className="w-5 h-5" />
1841 PHP Information
1842 </h3>
1843 <div className="space-y-3">
1844 <div className="flex justify-between">
1845 <span className="text-gray-600 dark:text-gray-400">
1846 Version:
1847 </span>
1848 <span className="font-medium">
1849 {systemStatus.php.version}
1850 </span>
1851 </div>
1852 <div className="flex justify-between">
1853 <span className="text-gray-600 dark:text-gray-400">
1854 Memory Limit:
1855 </span>
1856 <span className="font-medium">
1857 {systemStatus.php.memory_limit}
1858 </span>
1859 </div>
1860 <div className="flex justify-between">
1861 <span className="text-gray-600 dark:text-gray-400">
1862 Max Execution Time:
1863 </span>
1864 <span className="font-medium">
1865 {systemStatus.php.max_execution_time}s
1866 </span>
1867 </div>
1868 <div className="flex justify-between">
1869 <span className="text-gray-600 dark:text-gray-400">
1870 Upload Max Size:
1871 </span>
1872 <span className="font-medium">
1873 {systemStatus.php.upload_max_filesize}
1874 </span>
1875 </div>
1876 </div>
1877 </Card>
1878
1879 {/* WordPress Information */}
1880 <Card className="p-6">
1881 <h3 className="text-lg font-semibold mb-4">
1882 WordPress Information
1883 </h3>
1884 <div className="space-y-3">
1885 <div className="flex justify-between">
1886 <span className="text-gray-600 dark:text-gray-400">
1887 Version:
1888 </span>
1889 <span className="font-medium">
1890 {systemStatus.wordpress.version}
1891 </span>
1892 </div>
1893 <div className="flex justify-between">
1894 <span className="text-gray-600 dark:text-gray-400">
1895 Multisite:
1896 </span>
1897 <span className="font-medium">
1898 {systemStatus.wordpress.multisite ? "Yes" : "No"}
1899 </span>
1900 </div>
1901 <div className="flex justify-between">
1902 <span className="text-gray-600 dark:text-gray-400">
1903 Debug Mode:
1904 </span>
1905 <span className="font-medium">
1906 {systemStatus.wordpress.debug_mode
1907 ? "Enabled"
1908 : "Disabled"}
1909 </span>
1910 </div>
1911 </div>
1912 </Card>
1913
1914 {/* Requirements Check */}
1915 <Card className="p-6">
1916 <h3 className="text-lg font-semibold mb-4">
1917 Requirements Check
1918 </h3>
1919 <div className="space-y-3">
1920 {Object.entries(systemStatus.requirements).map(
1921 ([key, req]) => (
1922 <div
1923 key={key}
1924 className="flex items-center justify-between"
1925 >
1926 <div>
1927 <span className="font-medium capitalize">
1928 {key.replace(/_/g, " ")}
1929 </span>
1930 <div className="text-sm text-gray-600 dark:text-gray-400">
1931 Required: {req.required} | Current: {req.current}
1932 </div>
1933 </div>
1934 {getStatusBadge(req.status)}
1935 </div>
1936 ),
1937 )}
1938 </div>
1939 </Card>
1940
1941 {/* Extensions */}
1942 <Card className="p-6">
1943 <h3 className="text-lg font-semibold mb-4">PHP Extensions</h3>
1944 <div className="grid grid-cols-2 gap-3">
1945 {Object.entries(systemStatus.extensions).map(
1946 ([ext, loaded]) => (
1947 <div
1948 key={ext}
1949 className="flex items-center justify-between"
1950 >
1951 <span className="font-medium">{ext}</span>
1952 {loaded ? (
1953 <Badge variant="success">
1954 <CheckCircle className="w-3 h-3 mr-1" />
1955 Loaded
1956 </Badge>
1957 ) : (
1958 <Badge variant="error">
1959 <XCircle className="w-3 h-3 mr-1" />
1960 Missing
1961 </Badge>
1962 )}
1963 </div>
1964 ),
1965 )}
1966 </div>
1967 </Card>
1968 </div>
1969 ) : (
1970 <Card className="p-6">
1971 <div className="text-center">
1972 <XCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
1973 <p>Failed to load system status</p>
1974 <Button onClick={loadSystemStatus} className="mt-4">
1975 <RefreshCw className="w-4 h-4 mr-2" />
1976 Retry
1977 </Button>
1978 </div>
1979 </Card>
1980 )}
1981 </div>
1982 )}
1983
1984 {/* Jobs Tab */}
1985 {activeTab === "jobs" && (
1986 <div className="space-y-6">
1987 {/* Export/Import Jobs Section */}
1988 <Card className="overflow-hidden">
1989 <div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
1990 <div className="flex items-center gap-3">
1991 <div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
1992 <List className="w-5 h-5 text-purple-600 dark:text-purple-400" />
1993 </div>
1994 <div>
1995 <h3 className="text-base font-semibold text-gray-900 dark:text-white">
1996 Export & Import Jobs
1997 </h3>
1998 <p className="text-xs text-gray-500 dark:text-gray-400">
1999 {allJobs.length} job{allJobs.length !== 1 ? "s" : ""} in
2000 history
2001 </p>
2002 </div>
2003 </div>
2004 <Button
2005 variant="outline"
2006 size="sm"
2007 onClick={loadAllJobs}
2008 disabled={isLoadingJobs}
2009 >
2010 <RefreshCw
2011 className={`w-4 h-4 ${isLoadingJobs ? "animate-spin" : ""}`}
2012 />
2013 </Button>
2014 </div>
2015
2016 {isLoadingJobs ? (
2017 <div className="p-4 space-y-3">
2018 {[...Array(3)].map((_, index) => (
2019 <div
2020 key={index}
2021 className="flex items-center justify-between py-3 animate-pulse"
2022 >
2023 <div className="flex items-center gap-3 flex-1">
2024 <div className="w-8 h-8 bg-gray-200 dark:bg-gray-700 rounded"></div>
2025 <div className="flex-1">
2026 <div className="w-32 h-4 bg-gray-200 dark:bg-gray-700 rounded mb-2"></div>
2027 <div className="w-48 h-3 bg-gray-200 dark:bg-gray-700 rounded"></div>
2028 </div>
2029 </div>
2030 <div className="w-20 h-6 bg-gray-200 dark:bg-gray-700 rounded"></div>
2031 </div>
2032 ))}
2033 </div>
2034 ) : allJobs.length > 0 ? (
2035 <div className="divide-y divide-gray-100 dark:divide-gray-800">
2036 {allJobs.map((job) => (
2037 <div
2038 key={job.id}
2039 className="p-4 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
2040 >
2041 <div className="flex items-center justify-between gap-4">
2042 {/* Left: Icon + Info */}
2043 <div className="flex items-center gap-3 min-w-0 flex-1">
2044 <div
2045 className={`p-2 rounded-lg flex-shrink-0 ${
2046 job.type === "export"
2047 ? "bg-blue-100 dark:bg-blue-900/30"
2048 : "bg-green-100 dark:bg-green-900/30"
2049 }`}
2050 >
2051 {job.type === "export" ? (
2052 <Download className="w-4 h-4 text-blue-600 dark:text-blue-400" />
2053 ) : (
2054 <Upload className="w-4 h-4 text-green-600 dark:text-green-400" />
2055 )}
2056 </div>
2057 <div className="min-w-0 flex-1">
2058 <div className="flex items-center gap-2 flex-wrap">
2059 <span className="font-medium text-sm text-gray-900 dark:text-white">
2060 {job.type === "export" ? "Export" : "Import"} #
2061 {job.id.slice(-6)}
2062 </span>
2063 {/* Status Badge */}
2064 <span
2065 className={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium ${
2066 job.status === "completed"
2067 ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
2068 : job.status === "running"
2069 ? "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
2070 : job.status === "pending"
2071 ? "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
2072 : "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
2073 }`}
2074 >
2075 {job.status === "running" && (
2076 <RefreshCw className="w-3 h-3 mr-1 animate-spin" />
2077 )}
2078 {job.status.charAt(0).toUpperCase() +
2079 job.status.slice(1)}
2080 </span>
2081 </div>
2082 <div className="flex items-center gap-3 mt-1 text-xs text-gray-500 dark:text-gray-400">
2083 <span>
2084 {new Date(job.created_at).toLocaleDateString()}
2085 </span>
2086 <span>•</span>
2087 <span>
2088 {job.processed_records || 0}/
2089 {job.total_records || 0} records
2090 </span>
2091 {job.status === "running" && (
2092 <>
2093 <span>•</span>
2094 <span className="text-blue-600 dark:text-blue-400">
2095 {job.progress}%
2096 </span>
2097 </>
2098 )}
2099 </div>
2100 </div>
2101 </div>
2102
2103 {/* Right: Progress or Action */}
2104 <div className="flex items-center gap-2 flex-shrink-0">
2105 {job.status === "running" && (
2106 <div className="w-24 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
2107 <div
2108 className="h-full bg-blue-500 rounded-full transition-all"
2109 style={{ width: `${job.progress}%` }}
2110 ></div>
2111 </div>
2112 )}
2113 {job.type === "export" &&
2114 job.status === "completed" &&
2115 job.file_url && (
2116 <a
2117 href={job.file_url}
2118 download
2119 className="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 flex items-center gap-1"
2120 >
2121 <Download className="w-3 h-3" />
2122 Download
2123 </a>
2124 )}
2125 </div>
2126 </div>
2127
2128 {/* Error Message */}
2129 {job.error && (
2130 <div className="mt-2 ml-11 p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded text-xs text-red-700 dark:text-red-400">
2131 {job.error}
2132 </div>
2133 )}
2134 </div>
2135 ))}
2136 </div>
2137 ) : (
2138 <div className="text-center py-8 px-4">
2139 <List className="w-10 h-10 text-gray-300 dark:text-gray-600 mx-auto mb-2" />
2140 <p className="text-sm text-gray-500 dark:text-gray-400">
2141 No export/import jobs yet
2142 </p>
2143 </div>
2144 )}
2145 </Card>
2146
2147 {/* Cron Jobs Section */}
2148 <Card className="overflow-hidden">
2149 <div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
2150 <div className="flex items-center gap-3">
2151 <div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
2152 <Activity className="w-5 h-5 text-blue-600 dark:text-blue-400" />
2153 </div>
2154 <div>
2155 <h3 className="text-base font-semibold text-gray-900 dark:text-white">
2156 Scheduled Tasks
2157 </h3>
2158 <p className="text-xs text-gray-500 dark:text-gray-400">
2159 {cronJobs.length} active cron job
2160 {cronJobs.length !== 1 ? "s" : ""}
2161 </p>
2162 </div>
2163 </div>
2164 <Button
2165 variant="outline"
2166 size="sm"
2167 onClick={loadCronJobs}
2168 disabled={isLoadingCronJobs}
2169 >
2170 <RefreshCw
2171 className={`w-4 h-4 ${isLoadingCronJobs ? "animate-spin" : ""}`}
2172 />
2173 </Button>
2174 </div>
2175
2176 {/* WP Cron Status Warning */}
2177 {cronInfo?.wp_cron_disabled && (
2178 <div className="px-4 py-3 bg-amber-50 dark:bg-amber-900/20 border-b border-amber-200 dark:border-amber-800">
2179 <div className="flex items-center gap-2">
2180 <AlertTriangle className="w-4 h-4 text-amber-600 dark:text-amber-400 flex-shrink-0" />
2181 <span className="text-sm text-amber-800 dark:text-amber-400">
2182 WP-Cron is disabled. A server-side cron is required.
2183 </span>
2184 </div>
2185 </div>
2186 )}
2187
2188 {isLoadingCronJobs ? (
2189 <div className="p-4 space-y-3">
2190 {[...Array(3)].map((_, index) => (
2191 <div
2192 key={index}
2193 className="flex items-center justify-between py-3 animate-pulse"
2194 >
2195 <div className="flex-1">
2196 <div className="w-48 h-4 bg-gray-200 dark:bg-gray-700 rounded mb-2"></div>
2197 <div className="w-32 h-3 bg-gray-200 dark:bg-gray-700 rounded"></div>
2198 </div>
2199 <div className="w-20 h-8 bg-gray-200 dark:bg-gray-700 rounded"></div>
2200 </div>
2201 ))}
2202 </div>
2203 ) : cronJobs.length > 0 ? (
2204 <div className="divide-y divide-gray-100 dark:divide-gray-800">
2205 {cronJobs.map((cron, index) => (
2206 <div
2207 key={`${cron.hook}-${index}`}
2208 className={`flex items-center justify-between px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors ${
2209 cron.is_overdue ? "bg-red-50/50 dark:bg-red-900/10" : ""
2210 }`}
2211 >
2212 <div className="flex items-center gap-3 min-w-0 flex-1">
2213 <div
2214 className={`w-2 h-2 rounded-full flex-shrink-0 ${
2215 cron.is_overdue ? "bg-red-500" : "bg-green-500"
2216 }`}
2217 ></div>
2218 <div className="min-w-0 flex-1">
2219 <div className="flex items-center gap-2 flex-wrap">
2220 <span className="font-medium text-sm text-gray-900 dark:text-white truncate">
2221 {cron.hook
2222 .replace("yatra_", "")
2223 .replace(/_/g, " ")
2224 .replace(/\b\w/g, (l) => l.toUpperCase())}
2225 </span>
2226 <code className="text-xs font-mono text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 px-1.5 py-0.5 rounded">
2227 {cron.schedule || "once"}
2228 </code>
2229 {cron.is_overdue && (
2230 <span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400">
2231 Overdue
2232 </span>
2233 )}
2234 </div>
2235 <div className="flex items-center gap-3 mt-1 text-xs text-gray-500 dark:text-gray-400">
2236 <span
2237 className="flex items-center gap-1 cursor-help"
2238 title={cron.next_run_formatted}
2239 >
2240 <Calendar className="w-3 h-3" />
2241 {cron.next_run_relative}
2242 </span>
2243 <span className="hidden sm:inline">•</span>
2244 <span className="hidden sm:flex items-center gap-1">
2245 <RefreshCw className="w-3 h-3" />
2246 {cron.schedule_label}
2247 </span>
2248 </div>
2249 <div className="mt-0.5 text-xs text-gray-400 dark:text-gray-500 font-mono">
2250 {cron.hook}
2251 </div>
2252 </div>
2253 </div>
2254 <Button
2255 variant="ghost"
2256 size="sm"
2257 onClick={() => handleRunCronJob(cron.hook)}
2258 disabled={runningCronJob === cron.hook}
2259 className="ml-2 text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/20"
2260 >
2261 {runningCronJob === cron.hook ? (
2262 <RefreshCw className="w-4 h-4 animate-spin" />
2263 ) : (
2264 <span className="text-xs font-medium">Run</span>
2265 )}
2266 </Button>
2267 </div>
2268 ))}
2269 </div>
2270 ) : (
2271 <div className="text-center py-8 px-4">
2272 <Activity className="w-10 h-10 text-gray-300 dark:text-gray-600 mx-auto mb-2" />
2273 <p className="text-sm text-gray-500 dark:text-gray-400">
2274 No scheduled tasks
2275 </p>
2276 </div>
2277 )}
2278 </Card>
2279 </div>
2280 )}
2281
2282 {/* Migration Tab */}
2283 {activeTab === "migration" && (
2284 <Card className="p-6">
2285 {/* Header */}
2286 <div className="flex items-center justify-between mb-6">
2287 <div className="flex items-center gap-3">
2288 <div className="p-2 bg-purple-100 dark:bg-purple-900/20 rounded-lg">
2289 <Database className="w-5 h-5 text-purple-600 dark:text-purple-400" />
2290 </div>
2291 <div>
2292 <h3 className="text-lg font-semibold text-gray-900 dark:text-white">
2293 Data Migration
2294 </h3>
2295 <p className="text-sm text-gray-500 dark:text-gray-400">
2296 Migrate from Yatra 2.x to 3.0
2297 </p>
2298 </div>
2299 </div>
2300
2301 {/* Action Buttons */}
2302 {migrationStatus?.has_old_data && (
2303 <div className="flex items-center gap-2">
2304 {(isMigrating || isStartingMigration) &&
2305 !migrationProgress?.all_complete && (
2306 <div className="hidden sm:flex items-center gap-2 text-xs text-gray-600 dark:text-gray-300 mr-1">
2307 <RefreshCw className="w-4 h-4 animate-spin text-purple-600 dark:text-purple-400" />
2308 <span>Migration running</span>
2309 </div>
2310 )}
2311 <Button
2312 variant="outline"
2313 size="sm"
2314 onClick={() => {
2315 loadMigrationStatus();
2316 if (isMigrating) loadMigrationProgress();
2317 }}
2318 disabled={
2319 (isMigrating || isStartingMigration) &&
2320 !migrationProgress?.all_complete
2321 }
2322 >
2323 <RefreshCw
2324 className={`w-4 h-4 mr-2 ${isMigrating && !migrationProgress?.all_complete ? "animate-spin" : ""}`}
2325 />
2326 Refresh
2327 </Button>
2328
2329 {isMigrating && !migrationProgress?.all_complete && (
2330 <Button
2331 variant="destructive"
2332 size="sm"
2333 onClick={handleCancelMigration}
2334 >
2335 <XCircle className="w-4 h-4 mr-2" />
2336 Cancel
2337 </Button>
2338 )}
2339
2340 {!isMigrating && (
2341 <Button
2342 onClick={() => setShowMigrationConfirm(true)}
2343 className="bg-purple-600 hover:bg-purple-700"
2344 size="sm"
2345 disabled={isStartingMigration}
2346 >
2347 <Database className="w-4 h-4 mr-2" />
2348 {migrationProgress?.all_complete &&
2349 migrationProgress?.started_at
2350 ? "Migrate Again"
2351 : "Start Migration"}
2352 </Button>
2353 )}
2354 </div>
2355 )}
2356 </div>
2357
2358 {migrationStatus?.has_old_data &&
2359 (() => {
2360 const pm =
2361 migrationProgress?.pro_migration ??
2362 migrationStatus?.pro_migration;
2363 if (!pm || pm.ready) {
2364 return null;
2365 }
2366 return (
2367 <div className="mb-6 p-4 rounded-lg border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20 flex gap-3 items-start">
2368 <AlertTriangle className="w-5 h-5 text-amber-600 dark:text-amber-400 shrink-0 mt-0.5" />
2369 <div className="min-w-0">
2370 <h4 className="font-medium text-amber-900 dark:text-amber-200">
2371 {__(
2372 "Yatra Pro 3.0+ required for full migration",
2373 "yatra",
2374 )}
2375 </h4>
2376 <p className="text-sm text-amber-800 dark:text-amber-300/90 mt-1">
2377 {pm.warning_message}
2378 </p>
2379 {pm.multiple_pro_plugins &&
2380 Array.isArray(pm.active_pro_plugins) &&
2381 pm.active_pro_plugins.length > 0 && (
2382 <ul className="text-xs text-amber-800 dark:text-amber-300/80 mt-2 list-disc pl-5 space-y-0.5">
2383 {pm.active_pro_plugins.map(
2384 (p: {
2385 file: string;
2386 name?: string;
2387 version?: string | null;
2388 }) => (
2389 <li key={p.file}>
2390 {p.file}
2391 {p.version != null && p.version !== ""
2392 ? ` (${p.version})`
2393 : ""}
2394 </li>
2395 ),
2396 )}
2397 </ul>
2398 )}
2399 </div>
2400 </div>
2401 );
2402 })()}
2403
2404 {/* Loading State */}
2405 {isLoadingMigration ? (
2406 <div className="space-y-4">
2407 <div className="h-4 w-48 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
2408 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
2409 {[1, 2, 3, 4, 5, 6].map((i) => (
2410 <div
2411 key={i}
2412 className="h-32 bg-gray-100 dark:bg-gray-800 rounded-lg animate-pulse"
2413 ></div>
2414 ))}
2415 </div>
2416 </div>
2417 ) : migrationStatus?.has_old_data ? (
2418 <div className="space-y-6">
2419 {/* Migration Completion Notice */}
2420 {migrationProgress?.all_complete &&
2421 migrationProgress?.started_at &&
2422 Object.keys(migrationProgress.progress || {}).length > 0 && (
2423 <div className="bg-green-50 dark:bg-green-900/10 border border-green-200 dark:border-green-800 rounded-lg p-4">
2424 <div className="flex items-start gap-3">
2425 <CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400 mt-0.5" />
2426 <div className="flex-1">
2427 <h4 className="font-medium text-green-900 dark:text-green-300">
2428 Migration Completed Successfully!
2429 </h4>
2430 <p className="text-sm text-green-700 dark:text-green-400 mt-1">
2431 All data has been migrated from Yatra 2.x to 3.0
2432 </p>
2433 </div>
2434 <button
2435 onClick={() => {
2436 setIsMigrating(false);
2437 setMigrationProgress(null);
2438 }}
2439 className="text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-300"
2440 >
2441 <X className="w-4 h-4" />
2442 </button>
2443 </div>
2444 </div>
2445 )}
2446
2447 {/* Advanced Progress Bar (only show during migration) */}
2448 {(isMigrating || isStartingMigration) &&
2449 (!migrationProgress ||
2450 !migrationProgress.started_at ||
2451 Object.keys(migrationProgress.progress || {}).length ===
2452 0) && (
2453 <div className="bg-blue-50 dark:bg-blue-900/10 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
2454 <div className="flex items-center gap-2">
2455 <RefreshCw className="w-5 h-5 text-blue-600 dark:text-blue-400 animate-spin" />
2456 <div>
2457 <div className="font-medium text-blue-900 dark:text-blue-300">
2458 Starting migration…
2459 </div>
2460 <div className="text-sm text-blue-700 dark:text-blue-400">
2461 Initializing background tasks and loading progress.
2462 </div>
2463 </div>
2464 </div>
2465 </div>
2466 )}
2467
2468 {migrationProgress &&
2469 !migrationProgress.all_complete &&
2470 migrationProgress.started_at &&
2471 Object.keys(migrationProgress.progress || {}).length > 0 && (
2472 <div className="bg-blue-50 dark:bg-blue-900/10 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
2473 <div className="flex items-center justify-between mb-4">
2474 <div className="flex items-center gap-2">
2475 <RefreshCw className="w-5 h-5 text-blue-600 dark:text-blue-400 animate-spin" />
2476 <span className="font-medium text-blue-900 dark:text-blue-300">
2477 Migration in Progress
2478 </span>
2479 </div>
2480 <button
2481 onClick={() => {
2482 setIsMigrating(false);
2483 setMigrationProgress(null);
2484 }}
2485 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
2486 >
2487 <X className="w-4 h-4" />
2488 </button>
2489 </div>
2490
2491 {/* Overall Progress Bar */}
2492 <div className="mb-4">
2493 <div className="flex items-center justify-between text-sm mb-2">
2494 <span className="text-gray-700 dark:text-gray-300">
2495 Overall Progress
2496 </span>
2497 <span className="font-medium text-gray-900 dark:text-white">
2498 {(() => {
2499 const total = Object.values(
2500 migrationProgress.progress || {},
2501 ).reduce(
2502 (sum: number, p: any) => sum + (p.total || 0),
2503 0,
2504 );
2505 const processed = Object.values(
2506 migrationProgress.progress || {},
2507 ).reduce(
2508 (sum: number, p: any) =>
2509 sum +
2510 (p.migrated || 0) +
2511 (p.skipped || 0) +
2512 (p.failed || 0),
2513 0,
2514 );
2515 return total > 0
2516 ? Math.round((processed / total) * 100)
2517 : 0;
2518 })()}
2519 %
2520 </span>
2521 </div>
2522 <div className="w-full bg-blue-200 dark:bg-blue-800 rounded-full h-3 overflow-hidden">
2523 <div
2524 className="h-full bg-blue-600 dark:bg-blue-400 transition-all duration-500"
2525 style={{
2526 width: `${(() => {
2527 const total = Object.values(
2528 migrationProgress.progress || {},
2529 ).reduce(
2530 (sum: number, p: any) => sum + (p.total || 0),
2531 0,
2532 );
2533 const processed = Object.values(
2534 migrationProgress.progress || {},
2535 ).reduce(
2536 (sum: number, p: any) =>
2537 sum +
2538 (p.migrated || 0) +
2539 (p.skipped || 0) +
2540 (p.failed || 0),
2541 0,
2542 );
2543 return total > 0
2544 ? Math.round((processed / total) * 100)
2545 : 0;
2546 })()}%`,
2547 }}
2548 />
2549 </div>
2550 </div>
2551
2552 {/* Detailed Breakdown by Data Type */}
2553 <div className="space-y-3">
2554 <h5 className="text-sm font-medium text-gray-700 dark:text-gray-300">
2555 Progress by Data Type
2556 </h5>
2557 {Object.entries(migrationProgress.progress || {}).map(
2558 ([key, progress]: [string, any]) => {
2559 const dataInfo = migrationStatus?.old_data?.[key];
2560 if (!dataInfo || progress.total === 0) return null;
2561
2562 const percentage =
2563 progress.total > 0
2564 ? Math.round(
2565 (((progress.migrated || 0) +
2566 (progress.skipped || 0) +
2567 (progress.failed || 0)) /
2568 progress.total) *
2569 100,
2570 )
2571 : 0;
2572
2573 return (
2574 <div
2575 key={key}
2576 className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700"
2577 >
2578 <div className="flex items-center justify-between mb-2">
2579 <div className="flex items-center gap-2">
2580 {progress.status === "running" ? (
2581 <RefreshCw className="w-4 h-4 text-blue-600 dark:text-blue-400 animate-spin" />
2582 ) : progress.status === "completed" ? (
2583 <CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400" />
2584 ) : (
2585 <div className="w-4 h-4 rounded-full border-2 border-gray-300 dark:border-gray-600" />
2586 )}
2587 <span className="text-sm font-medium text-gray-900 dark:text-white">
2588 {dataInfo.label}
2589 </span>
2590 </div>
2591 <span className="text-sm text-gray-600 dark:text-gray-400">
2592 {(progress.migrated || 0) +
2593 (progress.skipped || 0) +
2594 (progress.failed || 0)}
2595 /{progress.total}
2596 </span>
2597 </div>
2598
2599 {/* Individual Progress Bar */}
2600 <div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden mb-2">
2601 <div
2602 className={`h-full transition-all duration-300 ${
2603 progress.status === "completed"
2604 ? "bg-green-500"
2605 : progress.status === "running"
2606 ? "bg-blue-500"
2607 : "bg-gray-400"
2608 }`}
2609 style={{ width: `${percentage}%` }}
2610 />
2611 </div>
2612
2613 {/* Stats */}
2614 {(progress.migrated || 0) +
2615 (progress.skipped || 0) +
2616 (progress.failed || 0) >
2617 0 && (
2618 <div className="flex items-center gap-3 text-xs">
2619 {progress.migrated > 0 && (
2620 <span className="text-green-600 dark:text-green-400">
2621 ✓ {progress.migrated} migrated
2622 </span>
2623 )}
2624 {progress.skipped > 0 && (
2625 <span className="text-yellow-600 dark:text-yellow-400">
2626 ⊘ {progress.skipped} skipped
2627 </span>
2628 )}
2629 {progress.failed > 0 && (
2630 <span className="text-red-600 dark:text-red-400">
2631 ✗ {progress.failed} failed
2632 </span>
2633 )}
2634 </div>
2635 )}
2636 </div>
2637 );
2638 },
2639 )}
2640 </div>
2641 </div>
2642 )}
2643
2644 {/* Migration Summary (show after completion) */}
2645 {migrationProgress?.all_complete &&
2646 migrationProgress?.started_at &&
2647 Object.keys(migrationProgress.progress || {}).length > 0 &&
2648 (() => {
2649 const totalMigrated = Object.values(
2650 migrationProgress.progress || {},
2651 ).reduce(
2652 (sum: number, p: any) => sum + (p.migrated || 0),
2653 0,
2654 );
2655 const totalSkipped = Object.values(
2656 migrationProgress.progress || {},
2657 ).reduce(
2658 (sum: number, p: any) => sum + (p.skipped || 0),
2659 0,
2660 );
2661 const totalFailed = Object.values(
2662 migrationProgress.progress || {},
2663 ).reduce((sum: number, p: any) => sum + (p.failed || 0), 0);
2664
2665 return (
2666 <div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
2667 <div className="flex items-center justify-between mb-4">
2668 <h4 className="font-medium text-gray-900 dark:text-white">
2669 Migration Summary
2670 </h4>
2671 <div className="flex items-center gap-3 text-sm">
2672 <span className="text-green-600 dark:text-green-400">
2673 ✓ {totalMigrated} migrated
2674 </span>
2675 {totalSkipped > 0 && (
2676 <span className="text-yellow-600 dark:text-yellow-400">
2677 ⊘ {totalSkipped} skipped
2678 </span>
2679 )}
2680 {totalFailed > 0 && (
2681 <span className="text-red-600 dark:text-red-400">
2682 ✗ {totalFailed} failed
2683 </span>
2684 )}
2685 </div>
2686 </div>
2687 <div className="space-y-2">
2688 {Object.entries(migrationProgress.progress || {}).map(
2689 ([key, progress]: [string, any]) => {
2690 const dataInfo = migrationStatus?.old_data?.[key];
2691
2692 if (!dataInfo) return null;
2693
2694 const totalProcessed =
2695 (progress.migrated || 0) +
2696 (progress.skipped || 0) +
2697 (progress.failed || 0);
2698 if (totalProcessed === 0) return null;
2699
2700 return (
2701 <div
2702 key={key}
2703 className="flex items-center justify-between py-2 border-b border-gray-100 dark:border-gray-700 last:border-0"
2704 >
2705 <span className="text-sm text-gray-700 dark:text-gray-300">
2706 {dataInfo.label}
2707 </span>
2708 <div className="flex items-center gap-3 text-xs">
2709 {progress.migrated > 0 && (
2710 <span className="text-green-600 dark:text-green-400">
2711 ✓ {progress.migrated}
2712 </span>
2713 )}
2714 {progress.skipped > 0 && (
2715 <span className="text-yellow-600 dark:text-yellow-400">
2716 ⊘ {progress.skipped}
2717 </span>
2718 )}
2719 {progress.failed > 0 && (
2720 <span className="text-red-600 dark:text-red-400">
2721 ✗ {progress.failed}
2722 </span>
2723 )}
2724 </div>
2725 </div>
2726 );
2727 },
2728 )}
2729 </div>
2730 </div>
2731 );
2732 })()}
2733
2734 {/* Data Type Cards - Simple Display */}
2735 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
2736 {Object.entries(migrationStatus.old_data || {}).map(
2737 ([key, data]: [string, any]) => (
2738 <div
2739 key={key}
2740 className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:shadow-md transition-shadow"
2741 >
2742 <div className="flex items-start justify-between">
2743 <div className="flex-1">
2744 <h4 className="font-medium text-gray-900 dark:text-white">
2745 {data.label}
2746 </h4>
2747 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
2748 {data.description}
2749 </p>
2750 <p className="text-xs text-gray-400 dark:text-gray-500 mt-2">
2751 {data.table}
2752 </p>
2753 </div>
2754 <Badge className="bg-purple-100 dark:bg-purple-900/20 text-purple-700 dark:text-purple-400">
2755 {data.count}
2756 </Badge>
2757 </div>
2758 </div>
2759 ),
2760 )}
2761 </div>
2762 </div>
2763 ) : (
2764 // No Data State
2765 <div className="text-center py-12">
2766 <CheckCircle className="w-12 h-12 text-green-500 mx-auto mb-3" />
2767 <h3 className="text-base font-medium text-gray-900 dark:text-white mb-1">
2768 No Migration Needed
2769 </h3>
2770 <p className="text-sm text-gray-500 dark:text-gray-400">
2771 Your database is up to date
2772 </p>
2773 </div>
2774 )}
2775 </Card>
2776 )}
2777
2778 {/* Logs Tab */}
2779 {activeTab === "logs" && (
2780 <div className="space-y-6">
2781 {/* Log Type Selector */}
2782 <div className="flex flex-wrap gap-2">
2783 {logTypes.map((type) => {
2784 const Icon = type.icon;
2785 return (
2786 <Button
2787 key={type.key}
2788 variant={
2789 selectedLogType === type.key ? "default" : "outline"
2790 }
2791 onClick={() => setSelectedLogType(type.key)}
2792 className="flex items-center gap-2"
2793 >
2794 <Icon className="w-4 h-4" />
2795 {type.label}
2796 </Button>
2797 );
2798 })}
2799 </div>
2800
2801 {/* Logs Display */}
2802 <Card className="p-6">
2803 <div className="flex items-center justify-between mb-4">
2804 <h3 className="text-lg font-semibold capitalize">
2805 {selectedLogType} Logs
2806 </h3>
2807 <div className="flex gap-2">
2808 <Button
2809 variant="outline"
2810 size="sm"
2811 onClick={() => loadLogs(selectedLogType)}
2812 disabled={isLoadingLogs}
2813 className="flex items-center gap-2"
2814 >
2815 <RefreshCw
2816 className={`w-4 h-4 ${isLoadingLogs ? "animate-spin" : ""}`}
2817 />
2818 Refresh
2819 </Button>
2820 <Button
2821 variant="outline"
2822 size="sm"
2823 onClick={copyAllLogsToClipboard}
2824 disabled={
2825 !logs[selectedLogType]?.logs ||
2826 logs[selectedLogType].logs.length === 0
2827 }
2828 className="flex items-center gap-2"
2829 >
2830 <Copy className="w-4 h-4" />
2831 Copy All
2832 </Button>
2833 <Button
2834 variant="destructive"
2835 size="sm"
2836 onClick={() => handleClearLogs()}
2837 >
2838 <Trash2 className="w-4 h-4 mr-2" />
2839 Clear Logs
2840 </Button>
2841 </div>
2842 </div>
2843
2844 {isLoadingLogs ? (
2845 <div className="space-y-4">
2846 {/* Logs Skeleton */}
2847 {[...Array(3)].map((_, index) => (
2848 <div
2849 key={index}
2850 className="border rounded-lg p-4 bg-gray-50 dark:bg-gray-800 animate-pulse"
2851 >
2852 <div className="flex items-start justify-between mb-3">
2853 <div className="flex items-center gap-2">
2854 <div className="w-16 h-5 bg-gray-200 dark:bg-gray-700 rounded"></div>
2855 <div className="w-32 h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
2856 </div>
2857 </div>
2858 <div className="w-3/4 h-4 bg-gray-200 dark:bg-gray-700 rounded mb-2"></div>
2859 <div className="w-1/2 h-3 bg-gray-200 dark:bg-gray-700 rounded"></div>
2860 </div>
2861 ))}
2862 </div>
2863 ) : logs[selectedLogType]?.logs?.length > 0 ? (
2864 logs[selectedLogType].logs.map((log) => (
2865 <div
2866 key={log.id}
2867 className="border rounded-lg p-4 bg-gray-50 dark:bg-gray-800"
2868 >
2869 <div className="flex items-start justify-between mb-2">
2870 <div className="flex items-center gap-2">
2871 {getLogLevelBadge(log.level)}
2872 <span className="text-sm text-gray-600 dark:text-gray-400">
2873 {new Date(log.timestamp).toLocaleString()}
2874 </span>
2875 </div>
2876 <Button
2877 variant="ghost"
2878 size="sm"
2879 onClick={() => copyLogToClipboard(log)}
2880 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 p-1 h-6 w-6"
2881 >
2882 <Copy className="w-3 h-3" />
2883 </Button>
2884 </div>
2885 <p className="text-sm font-medium mb-2">{log.message}</p>
2886 {log.context && Object.keys(log.context).length > 0 && (
2887 <details className="text-xs">
2888 <summary className="cursor-pointer text-gray-600 dark:text-gray-400">
2889 View Context
2890 </summary>
2891 <pre className="mt-2 p-2 bg-gray-100 dark:bg-gray-700 rounded text-xs overflow-auto">
2892 {JSON.stringify(log.context, null, 2)}
2893 </pre>
2894 </details>
2895 )}
2896 </div>
2897 ))
2898 ) : (
2899 <div className="text-center py-8 text-gray-500 dark:text-gray-400">
2900 <AlertCircle className="w-12 h-12 mx-auto mb-2 opacity-50" />
2901 <p>No logs found for this type</p>
2902 </div>
2903 )}
2904 </Card>
2905 </div>
2906 )}
2907
2908 {/* Export Modal */}
2909 {showExportModal && (
2910 <div
2911 className="fixed top-0 left-0 right-0 bottom-0 backdrop-blur-sm bg-white/30 dark:bg-black/30 z-50"
2912 style={{ margin: 0, padding: 0 }}
2913 >
2914 <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white dark:bg-gray-800 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 w-full max-w-2xl max-h-[85vh] overflow-hidden mx-4">
2915 <div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
2916 <h3 className="text-xl font-semibold text-gray-900 dark:text-white">
2917 Select Data Types to Export
2918 </h3>
2919 <button
2920 onClick={() => setShowExportModal(false)}
2921 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700"
2922 >
2923 <XCircle className="w-5 h-5" />
2924 </button>
2925 </div>
2926
2927 <div className="p-6 overflow-y-auto max-h-[60vh]">
2928 {/* Select All Option */}
2929 <div className="mb-4 pb-3 border-b border-gray-200 dark:border-gray-700">
2930 <label className="flex items-center gap-3 p-3 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg cursor-pointer transition-colors">
2931 <input
2932 type="checkbox"
2933 checked={
2934 selectableDataTypes.length > 0 &&
2935 selectableDataTypes.every((dt) =>
2936 selectedExportData.includes(dt.key),
2937 ) &&
2938 !selectedExportData.includes("all")
2939 }
2940 onChange={handleExportSelectAll}
2941 className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
2942 />
2943 <div className="flex items-center gap-2">
2944 <span className="text-sm font-semibold text-blue-700 dark:text-blue-400">
2945 {selectableDataTypes.length > 0 &&
2946 selectableDataTypes.every((dt) =>
2947 selectedExportData.includes(dt.key),
2948 ) &&
2949 !selectedExportData.includes("all")
2950 ? "Deselect All"
2951 : "Select All"}
2952 </span>
2953 <span className="text-xs text-gray-500 dark:text-gray-400">
2954 ({selectableDataTypes.length} individual types)
2955 </span>
2956 </div>
2957 </label>
2958 </div>
2959
2960 <div className="grid grid-cols-1 gap-1">
2961 {dataTypes.map((dataType) => (
2962 <label
2963 key={dataType.key}
2964 className="flex items-start gap-3 p-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 rounded-lg cursor-pointer transition-colors"
2965 >
2966 <input
2967 type="checkbox"
2968 checked={selectedExportData.includes(dataType.key)}
2969 onChange={() => handleExportDataToggle(dataType.key)}
2970 className="mt-1 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
2971 />
2972 <div className="flex-1 min-w-0">
2973 <div className="flex items-center gap-2">
2974 <dataType.icon className="w-4 h-4 text-blue-600 dark:text-blue-400" />
2975 <span className="text-sm font-medium text-gray-900 dark:text-white">
2976 {dataType.label}
2977 </span>
2978 </div>
2979 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
2980 {dataType.description}
2981 </p>
2982 </div>
2983 </label>
2984 ))}
2985 </div>
2986 </div>
2987
2988 <div className="flex items-center justify-between p-6 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
2989 <p className="text-sm text-gray-600 dark:text-gray-400">
2990 {selectedExportData.length} data types selected
2991 </p>
2992 <div className="flex gap-3">
2993 <Button
2994 variant="outline"
2995 onClick={() => setShowExportModal(false)}
2996 >
2997 Cancel
2998 </Button>
2999 <Button
3000 onClick={handleExport}
3001 disabled={selectedExportData.length === 0}
3002 >
3003 <Download className="w-4 h-4 mr-2" />
3004 Export Selected Data
3005 </Button>
3006 </div>
3007 </div>
3008 </div>
3009 </div>
3010 )}
3011
3012 {/* Delete Confirmation Modal */}
3013 {showDeleteModal && (
3014 <div
3015 className="fixed top-0 left-0 right-0 bottom-0 backdrop-blur-sm bg-white/30 dark:bg-black/30 z-50"
3016 style={{ margin: 0, padding: 0 }}
3017 >
3018 <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white dark:bg-gray-800 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 w-full max-w-md overflow-hidden mx-4">
3019 <div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
3020 <h3 className="text-xl font-semibold text-gray-900 dark:text-white">
3021 Confirm Deletion
3022 </h3>
3023 <button
3024 onClick={handleCancelDelete}
3025 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700"
3026 >
3027 <XCircle className="w-5 h-5" />
3028 </button>
3029 </div>
3030
3031 <div className="p-6">
3032 <div className="flex items-start gap-3 mb-4">
3033 <div className="p-2 bg-red-100 dark:bg-red-900/30 rounded-full">
3034 <AlertTriangle className="w-6 h-6 text-red-600 dark:text-red-400" />
3035 </div>
3036 <div>
3037 <h4 className="text-lg font-medium text-gray-900 dark:text-white mb-1">
3038 Delete Export File?
3039 </h4>
3040 <p className="text-sm text-gray-600 dark:text-gray-400">
3041 This action cannot be undone. The export file will be
3042 permanently deleted from the server.
3043 </p>
3044 </div>
3045 </div>
3046 </div>
3047
3048 <div className="flex items-center justify-end p-6 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
3049 <div className="flex gap-3">
3050 <Button variant="outline" onClick={handleCancelDelete}>
3051 Cancel
3052 </Button>
3053 <Button
3054 onClick={handleConfirmDelete}
3055 className="bg-red-600 hover:bg-red-700 text-white"
3056 >
3057 <Trash2 className="w-4 h-4 mr-2" />
3058 Delete File
3059 </Button>
3060 </div>
3061 </div>
3062 </div>
3063 </div>
3064 )}
3065
3066 {/* Clear Logs Confirmation Modal */}
3067 {showClearLogsModal && (
3068 <div
3069 className="fixed top-0 left-0 right-0 bottom-0 backdrop-blur-sm bg-white/30 dark:bg-black/30 z-50"
3070 style={{ margin: 0, padding: 0 }}
3071 >
3072 <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white dark:bg-gray-800 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 w-full max-w-md overflow-hidden mx-4">
3073 <div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
3074 <h3 className="text-xl font-semibold text-gray-900 dark:text-white">
3075 Clear Logs
3076 </h3>
3077 <button
3078 onClick={() => setShowClearLogsModal(false)}
3079 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700"
3080 >
3081 <XCircle className="w-5 h-5" />
3082 </button>
3083 </div>
3084
3085 <div className="p-6">
3086 <div className="flex items-start gap-3 mb-4">
3087 <div className="p-2 bg-orange-100 dark:bg-orange-900/30 rounded-full">
3088 <AlertTriangle className="w-6 h-6 text-orange-600 dark:text-orange-400" />
3089 </div>
3090 <div>
3091 <h4 className="text-lg font-medium text-gray-900 dark:text-white mb-1">
3092 Clear {selectedLogType} logs?
3093 </h4>
3094 <p className="text-sm text-gray-600 dark:text-gray-400">
3095 This action cannot be undone. All {selectedLogType} logs
3096 will be permanently deleted from the server.
3097 </p>
3098 </div>
3099 </div>
3100 </div>
3101
3102 <div className="flex items-center justify-end p-6 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
3103 <div className="flex gap-3">
3104 <Button
3105 variant="outline"
3106 onClick={() => setShowClearLogsModal(false)}
3107 >
3108 Cancel
3109 </Button>
3110 <Button
3111 onClick={confirmClearLogs}
3112 className="bg-orange-600 hover:bg-orange-700 text-white"
3113 >
3114 <Trash2 className="w-4 h-4 mr-2" />
3115 Clear Logs
3116 </Button>
3117 </div>
3118 </div>
3119 </div>
3120 </div>
3121 )}
3122
3123 {/* Import Modal */}
3124 {showImportModal && pendingImportFile && (
3125 <div
3126 className="fixed top-0 left-0 right-0 bottom-0 backdrop-blur-sm bg-white/30 dark:bg-black/30 z-50"
3127 style={{ margin: 0, padding: 0 }}
3128 >
3129 <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white dark:bg-gray-800 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 w-full max-w-2xl max-h-[85vh] overflow-hidden mx-4">
3130 <div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700">
3131 <div>
3132 <h3 className="text-xl font-semibold text-gray-900 dark:text-white">
3133 Select Data Types to Import
3134 </h3>
3135 <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
3136 File: {pendingImportFile.name}
3137 </p>
3138 </div>
3139 <button
3140 onClick={() => {
3141 setShowImportModal(false);
3142 setPendingImportFile(null);
3143 }}
3144 className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700"
3145 >
3146 <XCircle className="w-5 h-5" />
3147 </button>
3148 </div>
3149
3150 <div className="p-6 overflow-y-auto max-h-[60vh]">
3151 <div className="mb-4 pb-3 border-b border-gray-200 dark:border-gray-700">
3152 <label className="flex items-center gap-3 p-3 hover:bg-green-50 dark:hover:bg-green-900/20 rounded-lg cursor-pointer transition-colors">
3153 <input
3154 type="checkbox"
3155 checked={
3156 selectableDataTypes.length > 0 &&
3157 selectableDataTypes.every((dt) =>
3158 selectedImportData.includes(dt.key),
3159 ) &&
3160 !selectedImportData.includes("all")
3161 }
3162 onChange={handleImportSelectAll}
3163 className="rounded border-gray-300 text-green-600 focus:ring-green-500"
3164 />
3165 <div className="flex items-center gap-2">
3166 <span className="text-sm font-semibold text-green-700 dark:text-green-400">
3167 {selectableDataTypes.length > 0 &&
3168 selectableDataTypes.every((dt) =>
3169 selectedImportData.includes(dt.key),
3170 ) &&
3171 !selectedImportData.includes("all")
3172 ? "Deselect All"
3173 : "Select All"}
3174 </span>
3175 <span className="text-xs text-gray-500 dark:text-gray-400">
3176 ({selectableDataTypes.length} individual types)
3177 </span>
3178 </div>
3179 </label>
3180 </div>
3181 <div className="grid grid-cols-1 gap-1">
3182 {dataTypes.map((dataType) => (
3183 <label
3184 key={dataType.key}
3185 className="flex items-start gap-3 p-3 hover:bg-gray-50 dark:hover:bg-gray-700/50 rounded-lg cursor-pointer transition-colors"
3186 >
3187 <input
3188 type="checkbox"
3189 checked={selectedImportData.includes(dataType.key)}
3190 onChange={() => handleImportDataToggle(dataType.key)}
3191 className="mt-1 rounded border-gray-300 text-green-600 focus:ring-green-500"
3192 />
3193 <div className="flex-1 min-w-0">
3194 <div className="flex items-center gap-2">
3195 <dataType.icon className="w-4 h-4 text-green-600 dark:text-green-400" />
3196 <span className="text-sm font-medium text-gray-900 dark:text-white">
3197 {dataType.label}
3198 </span>
3199 </div>
3200 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
3201 {dataType.description}
3202 </p>
3203 </div>
3204 </label>
3205 ))}
3206 </div>
3207 </div>
3208
3209 <div className="flex items-center justify-between p-6 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
3210 <p className="text-sm text-gray-600 dark:text-gray-400">
3211 {selectedImportData.length} data types selected
3212 </p>
3213 <div className="flex gap-3">
3214 <Button
3215 variant="outline"
3216 onClick={() => {
3217 setShowImportModal(false);
3218 setPendingImportFile(null);
3219 }}
3220 >
3221 Cancel
3222 </Button>
3223 <Button
3224 onClick={handleImportConfirm}
3225 disabled={selectedImportData.length === 0}
3226 >
3227 <Upload className="w-4 h-4 mr-2" />
3228 Import Selected Data
3229 </Button>
3230 </div>
3231 </div>
3232 </div>
3233 </div>
3234 )}
3235
3236 {/* Migration Confirmation Modal */}
3237 <Modal
3238 isOpen={showMigrationConfirm}
3239 onClose={() => setShowMigrationConfirm(false)}
3240 title={__("Migrate All Data")}
3241 description={__(
3242 "This process runs in the background and may take several minutes. Please back up your database before proceeding.",
3243 )}
3244 size="md"
3245 footer={
3246 <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 w-full">
3247 <Button
3248 variant="outline"
3249 onClick={() => setShowMigrationConfirm(false)}
3250 className="w-full sm:w-auto"
3251 >
3252 {__("Cancel")}
3253 </Button>
3254 <div className="flex flex-col sm:flex-row gap-3 w-full sm:w-auto">
3255 <Button
3256 variant="destructive"
3257 onClick={() => {
3258 setShowMigrationConfirm(false);
3259 handleMigrateAll(true);
3260 }}
3261 className="w-full sm:w-auto"
3262 >
3263 {__("Re-migrate All")}
3264 </Button>
3265 <Button
3266 onClick={() => {
3267 setShowMigrationConfirm(false);
3268 handleMigrateAll(false);
3269 }}
3270 className="w-full sm:w-auto"
3271 >
3272 {__("Start Migration")}
3273 </Button>
3274 </div>
3275 </div>
3276 }
3277 >
3278 <div className="space-y-4 text-sm text-gray-600 dark:text-gray-300">
3279 <p>
3280 {__(
3281 "All legacy data types will be migrated to the new Yatra 3.0 database structure. The migration runs asynchronously via WordPress cron.",
3282 )}
3283 </p>
3284 <div className="p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-md flex items-start gap-3">
3285 <AlertCircle className="w-5 h-5 text-yellow-600 dark:text-yellow-400" />
3286 <div>
3287 <p className="font-medium text-yellow-800 dark:text-yellow-200">
3288 {__("Important")}
3289 </p>
3290 <p className="mt-1 text-sm">
3291 {__(
3292 "Keep this tab open to monitor progress. Use “Re-migrate All” only if you need to reprocess items that were already migrated.",
3293 )}
3294 </p>
3295 </div>
3296 </div>
3297 {migrationStatus?.pro_migration &&
3298 !migrationStatus.pro_migration.ready && (
3299 <div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-300 dark:border-amber-800 rounded-md flex items-start gap-3">
3300 <AlertTriangle className="w-5 h-5 text-amber-600 dark:text-amber-400 shrink-0" />
3301 <div>
3302 <p className="font-medium text-amber-900 dark:text-amber-200">
3303 {__("Yatra Pro", "yatra")}
3304 </p>
3305 <p className="mt-1 text-sm text-amber-800 dark:text-amber-300/90">
3306 {migrationStatus.pro_migration.warning_message}
3307 </p>
3308 </div>
3309 </div>
3310 )}
3311 </div>
3312 </Modal>
3313
3314 {/* Cache View Modal */}
3315 <Modal
3316 isOpen={showCacheModal}
3317 onClose={() => setShowCacheModal(false)}
3318 title="View Cache Data"
3319 size="lg"
3320 >
3321 <div className="space-y-4">
3322 {isLoadingCache ? (
3323 <div className="flex items-center justify-center py-8">
3324 <RefreshCw className="w-6 h-6 animate-spin mr-2" />
3325 <span>Loading cache data...</span>
3326 </div>
3327 ) : (
3328 <>
3329 <div className="flex items-center justify-between mb-4">
3330 <p className="text-sm text-gray-600 dark:text-gray-400">
3331 {cacheData.length} cache items found
3332 </p>
3333 <Button
3334 variant="outline"
3335 size="sm"
3336 onClick={loadCacheData}
3337 disabled={isLoadingCache}
3338 >
3339 <RefreshCw
3340 className={`w-4 h-4 mr-1 ${isLoadingCache ? "animate-spin" : ""}`}
3341 />
3342 Refresh
3343 </Button>
3344 </div>
3345
3346 {cacheData.length === 0 ? (
3347 <div className="text-center py-8 text-gray-500 dark:text-gray-400">
3348 <Database className="w-12 h-12 mx-auto mb-4 opacity-50" />
3349 <p>No cache data found</p>
3350 </div>
3351 ) : (
3352 <div className="overflow-x-auto">
3353 <table className="w-full text-sm">
3354 <thead>
3355 <tr className="border-b border-gray-200 dark:border-gray-700">
3356 <th className="text-left p-3 font-medium text-gray-900 dark:text-white">
3357 Cache Key
3358 </th>
3359 <th className="text-left p-3 font-medium text-gray-900 dark:text-white">
3360 Type
3361 </th>
3362 <th className="text-left p-3 font-medium text-gray-900 dark:text-white">
3363 Size
3364 </th>
3365 <th className="text-left p-3 font-medium text-gray-900 dark:text-white">
3366 Created
3367 </th>
3368 <th className="text-left p-3 font-medium text-gray-900 dark:text-white">
3369 Expires
3370 </th>
3371 <th className="text-left p-3 font-medium text-gray-900 dark:text-white">
3372 Actions
3373 </th>
3374 </tr>
3375 </thead>
3376 <tbody>
3377 {cacheData.map((item, index) => (
3378 <tr
3379 key={index}
3380 className="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800/50"
3381 >
3382 <td className="p-3">
3383 <div
3384 className="max-w-md truncate font-mono text-xs font-semibold"
3385 title={item.key}
3386 >
3387 {item.key}
3388 </div>
3389 <div className="text-xs text-gray-600 dark:text-gray-300 mt-2 max-h-32 overflow-auto bg-gray-50 dark:bg-gray-900 p-2 rounded border border-gray-200 dark:border-gray-700">
3390 <pre className="whitespace-pre-wrap break-words font-mono text-xs">
3391 {item.value}
3392 </pre>
3393 </div>
3394 </td>
3395 <td className="p-3">
3396 <Badge
3397 variant={
3398 item.type === "transient"
3399 ? "default"
3400 : "outline"
3401 }
3402 >
3403 {item.type}
3404 </Badge>
3405 </td>
3406 <td className="p-3 text-gray-600 dark:text-gray-400">
3407 {formatBytes(item.size)}
3408 </td>
3409 <td className="p-3 text-gray-600 dark:text-gray-400 text-xs">
3410 {item.created_at}
3411 </td>
3412 <td className="p-3 text-gray-600 dark:text-gray-400 text-xs">
3413 {item.expires_at}
3414 </td>
3415 <td className="p-3">
3416 <Button
3417 variant="outline"
3418 size="sm"
3419 onClick={() =>
3420 clearCacheItem(item.key, item.type)
3421 }
3422 className="text-red-600 hover:text-red-700 border-red-200 hover:bg-red-50"
3423 >
3424 <Trash2 className="w-3 h-3 mr-1" />
3425 Clear
3426 </Button>
3427 </td>
3428 </tr>
3429 ))}
3430 </tbody>
3431 </table>
3432 </div>
3433 )}
3434 </>
3435 )}
3436 </div>
3437 </Modal>
3438 </div>
3439 </div>
3440 );
3441 };
3442
3443 export default Tools;
3444