PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5
Yatra – Travel Booking & Tour Operator Software v3.0.5
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.5, at resources/js/components/Tools.tsx

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