PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.7
Yatra – Travel Booking & Tour Operator Software v3.0.2.7
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 2.0.1 All 84 releases
yatra / resources / js / components / Tools.tsx

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

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