PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.15
Yatra – Travel Booking & Tour Operator Software v3.0.15
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 2.0.10 All 83 releases
yatra / resources / js / components / Tools.tsx

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

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