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

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

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