import React, { useState, useEffect, useRef } from "react"; import { useToast } from "../components/ui/toast"; import { Card } from "./ui/card"; import { Button } from "./ui/button"; import { Badge } from "./ui/badge"; import { Modal } from "./ui/modal"; import { __ } from "../lib/i18n"; import { todayYmd } from "../lib/dateFormat"; import { apiService } from "../lib/api-client"; import { Download, Upload, Server, FileText, AlertCircle, AlertTriangle, CheckCircle, XCircle, X, Trash2, RefreshCw, MapPin, Globe, Activity, Folder, Mountain, Calendar, Users, UserCheck, CreditCard, Star, MessageSquare, Tag, BarChart3, List, Settings, Database, Play, Copy, Eye, } from "lucide-react"; interface SystemStatus { php: { version: string; memory_limit: string; max_execution_time: string; upload_max_filesize: string; post_max_size: string; }; wordpress: { version: string; multisite: boolean; debug_mode: boolean; }; yatra: { version: string; plugin_path: string; plugin_url: string; }; database: { version: string; charset: string; collate: string; }; server: { software: string; php_sapi: string; https: boolean; }; extensions: Record; requirements: Record< string, { required: string; current: string; status: "pass" | "fail" | "warning"; } >; } interface LogEntry { id: number; timestamp: string; level: string; message: string; context?: Record; } interface LogsResponse { logs: LogEntry[]; total: number; page: number; per_page: number; pages: number; } interface JobStatus { id: string; type: "export" | "import"; status: "pending" | "running" | "completed" | "failed"; progress: number; total_records: number; processed_records: number; file_path?: string; file_url?: string; error?: string; created_at: string; started_at?: string; completed_at?: string; import_stats?: Record< string, { total: number; imported: number; failed: number } >; seen_notification?: boolean; } interface CronJob { hook: string; next_run: number; next_run_formatted: string; next_run_relative: string; schedule: string; schedule_label: string; interval: number; args: any[]; is_overdue: boolean; } const MIGRATION_NOTICE_KEY = "yatra_migration_notice_dismissed_at"; const Tools: React.FC = () => { const [activeTab, setActiveTab] = useState("export-import"); const [isExporting, setIsExporting] = useState(false); const [isImporting, setIsImporting] = useState(false); const [selectedExportData, setSelectedExportData] = useState([ "trips", "destinations", "activities", "bookings", "customers", ]); const [selectedImportData, setSelectedImportData] = useState([]); const [isDragOver, setIsDragOver] = useState(false); const [showExportModal, setShowExportModal] = useState(false); const [showImportModal, setShowImportModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [showClearLogsModal, setShowClearLogsModal] = useState(false); const [pendingImportFile, setPendingImportFile] = useState(null); const [systemStatus, setSystemStatus] = useState(null); const [logs, setLogs] = useState>({}); const [selectedLogType, setSelectedLogType] = useState("error"); const [isLoadingStatus, setIsLoadingStatus] = useState(false); const [isLoadingLogs, setIsLoadingLogs] = useState(false); const [isLoadingJobs, setIsLoadingJobs] = useState(false); const [allJobs, setAllJobs] = useState([]); const [isClearingCache, setIsClearingCache] = useState(false); const [showCacheModal, setShowCacheModal] = useState(false); const [cacheData, setCacheData] = useState([]); const [isLoadingCache, setIsLoadingCache] = useState(false); const [cronJobs, setCronJobs] = useState([]); const [cronInfo, setCronInfo] = useState<{ wp_cron_disabled: boolean; alternate_cron: boolean; } | null>(null); const [isLoadingCronJobs, setIsLoadingCronJobs] = useState(false); const [runningCronJob, setRunningCronJob] = useState(null); const [migrationStatus, setMigrationStatus] = useState(null); const [isLoadingMigration, setIsLoadingMigration] = useState(false); const [migrationProgress, setMigrationProgress] = useState(null); const [isMigrating, setIsMigrating] = useState(false); const [isStartingMigration, setIsStartingMigration] = useState(false); const migrationPollingRef = useRef | null>( null, ); const [showMigrationConfirm, setShowMigrationConfirm] = useState(false); const [showMigrationCompleteNotice, setShowMigrationCompleteNotice] = useState(true); // Sample Data states const [isImportingSampleData, setIsImportingSampleData] = useState(false); const [sampleDataStatus, setSampleDataStatus] = useState(null); const [sampleDataJob, setSampleDataJob] = useState(null); const { showToast } = useToast(); // Deep-link from WP admin notice: admin.php?page=yatra&subpage=tools&tools_tab=migration useEffect(() => { const allowed = new Set([ "export-import", "jobs", "system-status", "logs", "migration", ]); try { const params = new URLSearchParams(window.location.search); const t = params.get("tools_tab"); if (t && allowed.has(t)) { setActiveTab(t); } } catch { // ignore invalid URL } }, []); // Helper function to format bytes const formatBytes = (bytes: number): string => { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; }; useEffect(() => { if (!migrationProgress?.started_at || !migrationProgress?.all_complete) { setShowMigrationCompleteNotice(false); if (typeof window !== "undefined") { window.localStorage.removeItem(MIGRATION_NOTICE_KEY); } return; } if (typeof window === "undefined") { setShowMigrationCompleteNotice(true); return; } const dismissedAt = window.localStorage.getItem(MIGRATION_NOTICE_KEY); if (dismissedAt && dismissedAt === migrationProgress.started_at) { setShowMigrationCompleteNotice(false); } else { setShowMigrationCompleteNotice(true); } }, [migrationProgress?.started_at, migrationProgress?.all_complete]); // Load sample data status on mount useEffect(() => { const loadSampleDataStatus = async () => { try { const status = await apiService.getSampleDataStatus(); setSampleDataStatus(status); } catch (error) { console.error("Failed to load sample data status:", error); } }; loadSampleDataStatus(); }, []); // Background job states const [exportJob, setExportJob] = useState(null); const [importJob, setImportJob] = useState(null); const pollingIntervalRef = useRef | null>( null, ); const logTypes = [ { key: "error", label: "Error Logs", icon: XCircle }, { key: "payment", label: "Payment Logs", icon: CheckCircle }, { key: "booking", label: "Booking Logs", icon: FileText }, { key: "system", label: "System Logs", icon: Server }, ]; // Available data types for export/import based on Yatra database structure const dataTypes = [ { key: "all", label: "All Yatra data", description: "Complete backup: all tables, settings, itinerary, payments, and Pro data (if present)", icon: Database, }, { key: "trips", label: "Trips", description: "All trip packages, itineraries, and details", icon: MapPin, }, { key: "destinations", label: "Destinations", description: "Travel destinations and locations", icon: Globe, }, { key: "activities", label: "Activities", description: "Trip activities and experiences", icon: Activity, }, { key: "categories", label: "Trip Categories", description: "Trip categorization and taxonomy", icon: Folder, }, { key: "difficulty_levels", label: "Difficulty Levels", description: "Trip difficulty classifications", icon: Mountain, }, { key: "bookings", label: "Bookings", description: "Customer bookings and reservations", icon: Calendar, }, { key: "customers", label: "Customers", description: "Customer profiles and CRM data", icon: Users, }, { key: "travelers", label: "Travelers", description: "Individual traveler information", icon: UserCheck, }, { key: "payments", label: "Payments", description: "Payment transactions and history", icon: CreditCard, }, { key: "reviews", label: "Reviews", description: "Trip reviews and ratings", icon: Star, }, { key: "enquiries", label: "Enquiries", description: "Customer enquiries and leads", icon: MessageSquare, }, { key: "discounts", label: "Discounts", description: "Discount codes and promotions", icon: Tag, }, { key: "availability", label: "Availability", description: "Trip availability and schedules", icon: BarChart3, }, { key: "itinerary", label: "Itinerary Items", description: "Itinerary items and types", icon: List, }, { key: "settings", label: "Settings", description: "Plugin configuration and settings", icon: Settings, }, ]; // Load system status const loadSystemStatus = async () => { setIsLoadingStatus(true); try { const data = await apiService.getSystemStatus(); // API returns data directly, not wrapped in success/data setSystemStatus(data); } catch (error) { console.error("Failed to load system status:", error); } finally { setIsLoadingStatus(false); } }; // Load active jobs on mount (to show status when returning to page) const loadActiveJobs = async () => { try { const jobs = await apiService.getActiveJobs(); if (Array.isArray(jobs) && jobs.length > 0) { // Find most recent export and import jobs const exportJobData = jobs.find((j: JobStatus) => j.type === "export"); const importJobData = jobs.find((j: JobStatus) => j.type === "import"); if (exportJobData) { setExportJob(exportJobData); // Resume polling if job is still running if ( exportJobData.status === "pending" || exportJobData.status === "running" ) { setIsExporting(true); pollJobStatus(exportJobData.id, "export"); } } if (importJobData) { setImportJob(importJobData); // Resume polling if job is still running if ( importJobData.status === "pending" || importJobData.status === "running" ) { setIsImporting(true); pollJobStatus(importJobData.id, "import"); } } } } catch (error) { console.error("Failed to load active jobs:", error); } }; // Load active jobs on component mount useEffect(() => { loadActiveJobs(); // We don't need to check localStorage for completed import jobs anymore // The message will only appear once after import completes and then // will be removed from localStorage when dismissed // Load system status when tab is system-status if (activeTab === "system-status") { loadSystemStatus(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeTab]); // Load logs const loadLogs = async (type: string, page = 1) => { setIsLoadingLogs(true); try { const data = await apiService.getLogs(type, page); if (data.logs && Array.isArray(data.logs)) { // The API returns: { logs: [...], total: number, page: number, per_page: number, pages: number } const logsData = data; // Use the full response, not data.data setLogs((prev) => ({ ...prev, [type]: logsData })); } } catch (error) { console.error("Failed to load logs:", error); } finally { setIsLoadingLogs(false); } }; const selectableDataTypes = dataTypes.filter((dt) => dt.key !== "all"); // Handle data type selection for export const handleExportDataToggle = (dataType: string) => { if (dataType === "all") { setSelectedExportData((prev) => (prev.includes("all") ? [] : ["all"])); return; } setSelectedExportData((prev) => { const withoutAll = prev.filter((t) => t !== "all"); return withoutAll.includes(dataType) ? withoutAll.filter((t) => t !== dataType) : [...withoutAll, dataType]; }); }; // Handle select all for export (individual types only, not the "all" preset) const handleExportSelectAll = () => { const keys = selectableDataTypes.map((dt) => dt.key); const allRegularSelected = keys.length > 0 && keys.every((k) => selectedExportData.includes(k)) && !selectedExportData.includes("all"); const onlyAll = selectedExportData.length === 1 && selectedExportData.includes("all"); if (allRegularSelected || onlyAll) { setSelectedExportData([]); } else { setSelectedExportData(keys); } }; // Handle data type selection for import const handleImportDataToggle = (dataType: string) => { if (dataType === "all") { setSelectedImportData((prev) => (prev.includes("all") ? [] : ["all"])); return; } setSelectedImportData((prev) => { const withoutAll = prev.filter((t) => t !== "all"); return withoutAll.includes(dataType) ? withoutAll.filter((t) => t !== dataType) : [...withoutAll, dataType]; }); }; const handleImportSelectAll = () => { const keys = selectableDataTypes.map((dt) => dt.key); const allRegularSelected = keys.length > 0 && keys.every((k) => selectedImportData.includes(k)) && !selectedImportData.includes("all"); const onlyAll = selectedImportData.length === 1 && selectedImportData.includes("all"); if (allRegularSelected || onlyAll) { setSelectedImportData([]); } else { setSelectedImportData(keys); } }; // Handle export button click - show modal const handleExportClick = () => { setShowExportModal(true); }; // Handle actual export after data type selection - uses background job const handleExport = async () => { if (selectedExportData.length === 0) { showToast(__("Please select at least one data type to export.", "yatra"), "error"); return; } setIsExporting(true); setShowExportModal(false); try { // Create background export job const data = await apiService.createExportJob({ data_types: selectedExportData, }); // Start polling for job status const jobId = data.job_id; pollJobStatus(jobId, "export"); } catch (error) { console.error("Export error:", error); showToast(__("Export failed. Please try again.", "yatra"), "error"); setIsExporting(false); } }; // Stop polling const stopPolling = () => { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } }; // Poll job status const pollJobStatus = (jobId: string, type: "export" | "import") => { const endpoint = type === "export" ? "export-job" : "import-job"; const poll = async () => { try { const jobData = (await apiService.performJobAction( endpoint, jobId, )) as JobStatus; if (jobData) { if (type === "export") { setExportJob(jobData); } else { setImportJob(jobData); } // Check if job is complete or failed - stop polling if (jobData.status === "completed" || jobData.status === "failed") { stopPolling(); if (type === "export") { setIsExporting(false); } else { setIsImporting(false); } if (jobData.status === "failed") { showToast( (type === "export" ? __("Export failed: %s", "yatra") : __("Import failed: %s", "yatra") ).replace( "%s", jobData.error || __("Unknown error", "yatra"), ), "error", ); } } } } catch (error) { console.error("Failed to poll job status:", error); } }; // Clear any existing polling first stopPolling(); // Poll immediately, then every 2 seconds poll(); pollingIntervalRef.current = setInterval(poll, 2000); }; // Download completed export and delete file after download const handleDownloadExport = async () => { if (!exportJob || exportJob.status !== "completed") return; try { // First download the file const blob = await apiService.downloadExportJobBlob(exportJob.id); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.style.display = "none"; a.href = url; a.download = `yatra-export-${todayYmd()}.json`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); // Then delete the file from server after download await apiService.deleteExportJob(exportJob.id); // Clear the job after download and deletion setExportJob(null); } catch (error) { console.error("Download error:", error); showToast(__("Download failed. Please try again.", "yatra"), "error"); } }; // Show delete confirmation modal const handleDeleteClick = () => { if (!exportJob) return; setShowDeleteModal(true); }; // Close delete confirmation modal const handleCancelDelete = () => { setShowDeleteModal(false); }; // Delete export job and file after confirmation const handleConfirmDelete = async () => { if (!exportJob) return; setShowDeleteModal(false); try { await apiService.deleteExportJob(exportJob.id); // Immediately clear the export job from state to remove it from UI setExportJob(null); } catch (error) { console.error("Delete error:", error); showToast(__("Failed to delete export file.", "yatra"), "error"); } }; // Load logs when logs tab becomes active useEffect(() => { if (activeTab === "logs") { loadLogs(selectedLogType); } }, [activeTab, selectedLogType]); // Handle file processing (for both input and drop) - uses background job const processFile = async (file: File) => { if (selectedImportData.length === 0) { showToast(__("Please select at least one data type to import.", "yatra"), "error"); return; } setIsImporting(true); try { const formData = new FormData(); formData.append("file", file); formData.append("data_types", JSON.stringify(selectedImportData)); // Create background import job const data = await apiService.createImportJob(formData); // Start polling for job status const jobId = data.job_id; pollJobStatus(jobId, "import"); setSelectedImportData([]); } catch (error) { console.error("Import error:", error); showToast(__("Import failed. Please check the file format and try again.", "yatra"), "error"); setIsImporting(false); } }; // Import data from file input - show modal for data type selection const handleImport = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; setPendingImportFile(file); setShowImportModal(true); event.target.value = ""; }; // Handle drag and drop events - show modal for data type selection const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsDragOver(true); }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); }; const handleDrop = async (e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); const files = Array.from(e.dataTransfer.files); const jsonFile = files.find( (file) => file.type === "application/json" || file.name.endsWith(".json"), ); if (!jsonFile) { showToast(__("Please drop a valid JSON file.", "yatra"), "error"); return; } setPendingImportFile(jsonFile); setShowImportModal(true); }; // Handle actual import after data type selection const handleImportConfirm = async () => { if (!pendingImportFile) return; setShowImportModal(false); await processFile(pendingImportFile); setPendingImportFile(null); setSelectedImportData([]); }; // Clear logs const handleClearLogs = () => { setShowClearLogsModal(true); }; // Confirm clear logs const confirmClearLogs = async () => { try { const data = await apiService.clearLogs(selectedLogType); if (data.success) { showToast(data.message || "Logs cleared successfully", "success"); setLogs((prev) => ({ ...prev, [selectedLogType]: { logs: [], total: 0, page: 1, per_page: 50, pages: 0, }, })); } else { showToast(data.message || "Failed to clear logs", "error"); } } catch (error) { console.error("Failed to clear logs:", error); showToast("Failed to clear logs. Please try again.", "error"); } }; // Copy individual log to clipboard const copyLogToClipboard = async (log: any) => { const logText = `[${log.timestamp}] ${log.level.toUpperCase()}: ${log.message}${log.context ? "\nContext: " + JSON.stringify(log.context, null, 2) : ""}`; try { await navigator.clipboard.writeText(logText); // You could add a toast notification here if you have one } catch (error) { console.error("Failed to copy log:", error); // Fallback for older browsers const textArea = document.createElement("textarea"); textArea.value = logText; document.body.appendChild(textArea); textArea.select(); document.execCommand("copy"); document.body.removeChild(textArea); } }; // Copy all logs to clipboard const copyAllLogsToClipboard = async () => { const currentLogs = logs[selectedLogType]; if (!currentLogs?.logs || currentLogs.logs.length === 0) return; const allLogsText = currentLogs.logs .map( (log: any) => `[${log.timestamp}] ${log.level.toUpperCase()}: ${log.message}${log.context ? "\nContext: " + JSON.stringify(log.context, null, 2) : ""}`, ) .join("\n\n"); try { await navigator.clipboard.writeText(allLogsText); // You could add a toast notification here if you have one } catch (error) { console.error("Failed to copy all logs:", error); // Fallback for older browsers const textArea = document.createElement("textarea"); textArea.value = allLogsText; document.body.appendChild(textArea); textArea.select(); document.execCommand("copy"); document.body.removeChild(textArea); } }; // Handle sample data import - imports all data automatically const handleImportSampleData = async () => { setIsImportingSampleData(true); setSampleDataJob({ progress: 0 }); try { // Import all sample data from JSON files automatically const response = await apiService.importSampleData({ data_types: [], // Not used anymore - imports all data types overwrite: false, }); if (response.success) { setSampleDataJob({ ...response.data, progress: 100 }); // Show detailed success message with counts const data = response.data || {}; 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`; showToast(message, "success"); // Refresh sample data status const statusResponse = await apiService.getSampleDataStatus(); setSampleDataStatus(statusResponse); } else { throw new Error(response.message || __("Import failed")); } } catch (error: any) { console.error("Sample data import error:", error); showToast( error.message || __("Failed to import sample data. Please try again."), "error", ); } finally { setIsImportingSampleData(false); setTimeout(() => setSampleDataJob(null), 3000); } }; // Get status badge const getStatusBadge = (status: string) => { switch (status) { case "pass": return ( Pass ); case "warning": return ( Warning ); case "fail": return ( Fail ); default: return Unknown; } }; // Get log level badge const getLogLevelBadge = (level: string) => { switch (level.toLowerCase()) { case "error": return Error; case "warning": return Warning; case "info": return Info; default: return {level}; } }; // Load all jobs const loadAllJobs = async () => { setIsLoadingJobs(true); try { const data = await apiService.getAllJobs(); // API returns data directly, not wrapped in {success, data} setAllJobs(Array.isArray(data) ? data : []); } catch (error) { console.error("Failed to load jobs:", error); } finally { setIsLoadingJobs(false); } }; // Load cron jobs const loadCronJobs = async () => { setIsLoadingCronJobs(true); try { const data = await apiService.getCronJobs(); // API returns data directly: {cron_jobs: [], wp_cron_disabled: bool, ...} setCronJobs(data?.cron_jobs || []); setCronInfo({ wp_cron_disabled: data?.wp_cron_disabled || false, alternate_cron: data?.alternate_cron || false, }); } catch (error) { console.error("Failed to load cron jobs:", error); } finally { setIsLoadingCronJobs(false); } }; // Run a cron job manually const handleRunCronJob = async (hook: string) => { setRunningCronJob(hook); try { const data = await apiService.runCronJob(hook); // API returns data directly: {success: true, message: ...} if (data.success) { showToast( data.message || `Cron job "${hook}" executed successfully`, "success", ); // Reload cron jobs to update next run times loadCronJobs(); } else { showToast(data.message || "Failed to run cron job", "error"); } } catch (error) { console.error("Failed to run cron job:", error); showToast("Failed to run cron job", "error"); } finally { setRunningCronJob(null); } }; // Load cache data for viewing const loadCacheData = async () => { setIsLoadingCache(true); try { const data = await apiService.getCacheView(); // API returns consistent structure: {success, message, data} if (data.success && data.data) { setCacheData(data.data.cache_data || []); showToast(data.message || "Cache data loaded successfully", "success"); } else { showToast(data.message || "Failed to load cache data", "error"); } } catch (error) { console.error("Failed to load cache data:", error); showToast("Failed to load cache data", "error"); } finally { setIsLoadingCache(false); } }; // Clear individual cache item const clearCacheItem = async (key: string, type: string) => { try { const data = await apiService.clearCacheItem(key, type); // API returns consistent structure: {success, message, data} if (data.success) { // Remove the item from the cache data setCacheData((prev) => prev.filter((item) => item.key !== key)); showToast( data.message || `Cache item "${key}" cleared successfully`, "success", ); } else { showToast(data.message || "Failed to clear cache item", "error"); } } catch (error) { console.error("Failed to clear cache item:", error); showToast("Failed to clear cache item", "error"); } }; // Clear all caches const clearAllCache = async () => { setIsClearingCache(true); try { const data = await apiService.clearCache(); // Clear React Query cache if available if ( (window as any).yatraQueryClient && typeof (window as any).yatraQueryClient.invalidateQueries === "function" ) { (window as any).yatraQueryClient.invalidateQueries(); } // Fixed handling of response if (data.success) { // Only show success message if both response.ok and data.success are true showToast( "All caches cleared successfully! Please refresh the page to see the changes.", "success", ); } else { // Create a clean error message without concatenation issues let errorMessage = "Failed to clear caches"; // Only append additional details if they don't create a contradictory message if (data.message && !data.message.includes("success")) { errorMessage += ": " + data.message; } showToast(errorMessage, "error"); } } catch (error) { console.error("Failed to clear caches:", error); showToast("Failed to clear caches. Please try again.", "error"); } finally { setIsClearingCache(false); } }; // Load migration status const loadMigrationStatus = async () => { setIsLoadingMigration(true); try { const data = await apiService.getMigrationStatus(); setMigrationStatus(data); } catch (error) { console.error("Failed to load migration status:", error); showToast("Failed to load migration status", "error"); } finally { setIsLoadingMigration(false); } }; const handleDismissMigrationNotice = async () => { try { await apiService.clearMigration(); setShowMigrationCompleteNotice(false); await loadMigrationProgress(); if (typeof window !== "undefined") { window.localStorage.removeItem(MIGRATION_NOTICE_KEY); } } catch (error) { showToast("Failed to clear migration data.", "error"); } }; // Load migration progress const loadMigrationProgress = async () => { try { const data = await apiService.getMigrationProgress(); setMigrationProgress(data); // If migration is still running, keep polling if (data.any_running && !data.all_complete) { setIsMigrating(true); // Start polling if not already polling if (!migrationPollingRef.current) { migrationPollingRef.current = setInterval( loadMigrationProgress, 3000, ); } } else if (data.all_complete && data.started_at) { setIsMigrating(false); stopMigrationPolling(); } } catch (error) { console.error("Failed to load migration progress:", error); } }; // Start polling migration progress const startMigrationPolling = () => { if (migrationPollingRef.current) { clearInterval(migrationPollingRef.current); } loadMigrationProgress(); // Poll every 1 second for more frequent progress updates migrationPollingRef.current = setInterval(loadMigrationProgress, 1000); }; // Stop polling migration progress const stopMigrationPolling = () => { if (migrationPollingRef.current) { clearInterval(migrationPollingRef.current); migrationPollingRef.current = null; } }; // Migrate all data types const handleMigrateAll = async (force = false) => { setIsMigrating(true); setIsStartingMigration(true); try { const data = await apiService.runMigrationAll({ force }); if (data.success) { showToast( "Migration started for all data types. Processing in background...", "success", ); if (Array.isArray(data.warnings) && data.warnings.length > 0) { showToast(String(data.warnings[0]), "warning"); } // Immediately load progress to initialize the display await loadMigrationProgress(); startMigrationPolling(); setIsStartingMigration(false); } else { showToast(data.error || data.message || "Migration failed", "error"); setIsMigrating(false); setIsStartingMigration(false); } } catch (error) { console.error("Migration error:", error); showToast("Migration failed. Please try again.", "error"); setIsMigrating(false); setIsStartingMigration(false); } }; // Cancel migration const handleCancelMigration = async () => { if ( !confirm( "Are you sure you want to cancel the migration? This will stop all ongoing migrations.", ) ) { return; } try { const data = await apiService.cancelMigration(); if (data.success) { showToast("Migration cancelled successfully", "success"); setIsMigrating(false); stopMigrationPolling(); loadMigrationStatus(); } else { showToast(data.error || "Failed to cancel migration", "error"); } } catch (error) { console.error("Cancel migration error:", error); showToast("Failed to cancel migration", "error"); } }; useEffect(() => { if (activeTab === "system-status") { loadSystemStatus(); } else if (activeTab === "logs") { loadLogs(selectedLogType); } else if (activeTab === "jobs") { loadAllJobs(); loadCronJobs(); } else if (activeTab === "migration") { loadMigrationStatus(); loadMigrationProgress(); } // Cleanup migration polling when leaving tab return () => { if (activeTab === "migration") { stopMigrationPolling(); } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeTab, selectedLogType]); return (

Tools

Export/Import data, check system status, and view logs

{/* Tab Navigation - Clean and Spacious Design */}
{/* Export/Import Tab */} {activeTab === "export-import" && (
{/* Export Section */}

Export Data

Download your data

Export your Yatra data for backup or migration purposes. Click the button below to select which data types to export.

{/* Export Job Progress */} {exportJob && (exportJob.status === "pending" || exportJob.status === "running") && (
{exportJob.status === "pending" ? "Queued..." : "Exporting..."} {exportJob.progress}%

{exportJob.processed_records} /{" "} {exportJob.total_records} records processed

Processing in background. You can close this page and come back later.

)} {/* Export Complete - Download Button */} {exportJob && exportJob.status === "completed" && (
Export completed successfully!
)} {/* Import Section */}

Import Data

Restore from backup

Import Yatra data from a previously exported JSON file. Drop your file below or click to browse.

{/* Drag and Drop Zone */}

{isDragOver ? "Drop your JSON file here" : "Drop JSON file or click to browse"}

Supports JSON files exported from Yatra

{/* Import Job Progress */} {importJob && (importJob.status === "pending" || importJob.status === "running") && (
{importJob.status === "pending" ? "Queued..." : "Importing..."} {importJob.progress}%

{importJob.processed_records} /{" "} {importJob.total_records} records processed

Processing in background. You can close this page and come back later.

)} {/* Import Complete - with detailed statistics and shown only once */} {importJob && importJob.status === "completed" && (
Import completed successfully!
Total: {importJob.processed_records}{" "} records imported
{/* Detailed statistics by data type */} {importJob.import_stats && Object.keys(importJob.import_stats).length > 0 && (
Import Details:
{Object.entries(importJob.import_stats).map( ([dataType, stats]: [string, any]) => { // Find the matching data type object for the icon const dataTypeObj = dataTypes.find( (dt) => dt.key === dataType, ); const Icon = dataTypeObj?.icon || FileText; return (
{dataType.replace("_", " ")}:
{stats.imported} {stats.failed > 0 && ( ({stats.failed} failed) )}
); }, )}
)} {/* We don't need to mark as seen anymore since we'll remove from localStorage on dismiss */}
)} {/* Import Failed */} {importJob && importJob.status === "failed" && (
Import failed: {importJob.error || "Unknown error"}
)} {isImporting && !importJob && (
Starting import...
)}
{/* Import Warning */}

Important Notice

Importing data will create new records. Existing data will not be overwritten. Always backup your database before importing data to prevent any potential data loss.

{/* Sample Data Section */}

Import Sample Data

Quick setup with demo content

Quickly populate your Yatra site with sample trips, categories, destinations, and more. Perfect for testing and demonstration purposes.

{/* What Will Be Imported */}

The following data will be imported automatically:

11 Sample Trips (multi-day & single-day)
8 Categories
10 Activities
10 Destinations
6 Difficulty Levels
8 Attributes
6 Itinerary Item Types
8 Traveler Categories
8 Discount Codes
21 Availability Dates
9 Availability Rules
31 Itinerary Days
25 Itinerary Entries
{/* Action Buttons */}
)} {/* System Status Tab */} {activeTab === "system-status" && (
{isLoadingStatus ? (
{/* System Status Skeleton */} {[...Array(4)].map((_, index) => (
{[...Array(4)].map((_, i) => (
))}
))}
) : systemStatus ? (
{/* PHP Information */}

PHP Information

Version: {systemStatus.php.version}
Memory Limit: {systemStatus.php.memory_limit}
Max Execution Time: {systemStatus.php.max_execution_time}s
Upload Max Size: {systemStatus.php.upload_max_filesize}
{/* WordPress Information */}

WordPress Information

Version: {systemStatus.wordpress.version}
Multisite: {systemStatus.wordpress.multisite ? "Yes" : "No"}
Debug Mode: {systemStatus.wordpress.debug_mode ? "Enabled" : "Disabled"}
{/* Requirements Check */}

Requirements Check

{Object.entries(systemStatus.requirements).map( ([key, req]) => (
{key.replace("_", " ")}
Required: {req.required} | Current: {req.current}
{getStatusBadge(req.status)}
), )}
{/* Extensions */}

PHP Extensions

{Object.entries(systemStatus.extensions).map( ([ext, loaded]) => (
{ext} {loaded ? ( Loaded ) : ( Missing )}
), )}
) : (

Failed to load system status

)}
)} {/* Jobs Tab */} {activeTab === "jobs" && (
{/* Export/Import Jobs Section */}

Export & Import Jobs

{allJobs.length} job{allJobs.length !== 1 ? "s" : ""} in history

{isLoadingJobs ? (
{[...Array(3)].map((_, index) => (
))}
) : allJobs.length > 0 ? (
{allJobs.map((job) => (
{/* Left: Icon + Info */}
{job.type === "export" ? ( ) : ( )}
{job.type === "export" ? "Export" : "Import"} # {job.id.slice(-6)} {/* Status Badge */} {job.status === "running" && ( )} {job.status.charAt(0).toUpperCase() + job.status.slice(1)}
{new Date(job.created_at).toLocaleDateString()} • {job.processed_records || 0}/ {job.total_records || 0} records {job.status === "running" && ( <> • {job.progress}% )}
{/* Right: Progress or Action */}
{job.status === "running" && (
)} {job.type === "export" && job.status === "completed" && job.file_url && ( Download )}
{/* Error Message */} {job.error && (
{job.error}
)}
))}
) : (

No export/import jobs yet

)}
{/* Cron Jobs Section */}

Scheduled Tasks

{cronJobs.length} active cron job {cronJobs.length !== 1 ? "s" : ""}

{/* WP Cron Status Warning */} {cronInfo?.wp_cron_disabled && (
WP-Cron is disabled. A server-side cron is required.
)} {isLoadingCronJobs ? (
{[...Array(3)].map((_, index) => (
))}
) : cronJobs.length > 0 ? (
{cronJobs.map((cron, index) => (
{cron.hook .replace("yatra_", "") .replace(/_/g, " ") .replace(/\b\w/g, (l) => l.toUpperCase())} {cron.schedule || "once"} {cron.is_overdue && ( Overdue )}
{cron.next_run_relative} • {cron.schedule_label}
{cron.hook}
))}
) : (

No scheduled tasks

)}
)} {/* Migration Tab */} {activeTab === "migration" && ( {/* Header */}

Data Migration

Migrate from Yatra 2.x to 3.0

{/* Action Buttons */} {migrationStatus?.has_old_data && (
{(isMigrating || isStartingMigration) && !migrationProgress?.all_complete && (
Migration running
)} {isMigrating && !migrationProgress?.all_complete && ( )} {!isMigrating && ( )}
)}
{migrationStatus?.has_old_data && (() => { const pm = migrationProgress?.pro_migration ?? migrationStatus?.pro_migration; if (!pm || pm.ready) { return null; } return (

{__( "Yatra Pro 3.0+ required for full migration", "yatra", )}

{pm.warning_message}

{pm.multiple_pro_plugins && Array.isArray(pm.active_pro_plugins) && pm.active_pro_plugins.length > 0 && (
    {pm.active_pro_plugins.map( (p: { file: string; name?: string; version?: string | null; }) => (
  • {p.file} {p.version != null && p.version !== "" ? ` (${p.version})` : ""}
  • ), )}
)}
); })()} {/* Loading State */} {isLoadingMigration ? (
{[1, 2, 3, 4, 5, 6].map((i) => (
))}
) : migrationStatus?.has_old_data ? (
{/* Migration Completion Notice */} {migrationProgress?.all_complete && migrationProgress?.started_at && Object.keys(migrationProgress.progress || {}).length > 0 && (

Migration Completed Successfully!

All data has been migrated from Yatra 2.x to 3.0

)} {/* Advanced Progress Bar (only show during migration) */} {(isMigrating || isStartingMigration) && (!migrationProgress || !migrationProgress.started_at || Object.keys(migrationProgress.progress || {}).length === 0) && (
Starting migration…
Initializing background tasks and loading progress.
)} {migrationProgress && !migrationProgress.all_complete && migrationProgress.started_at && Object.keys(migrationProgress.progress || {}).length > 0 && (
Migration in Progress
{/* Overall Progress Bar */}
Overall Progress {(() => { const total = Object.values( migrationProgress.progress || {}, ).reduce( (sum: number, p: any) => sum + (p.total || 0), 0, ); const processed = Object.values( migrationProgress.progress || {}, ).reduce( (sum: number, p: any) => sum + (p.migrated || 0) + (p.skipped || 0) + (p.failed || 0), 0, ); return total > 0 ? Math.round((processed / total) * 100) : 0; })()} %
{ const total = Object.values( migrationProgress.progress || {}, ).reduce( (sum: number, p: any) => sum + (p.total || 0), 0, ); const processed = Object.values( migrationProgress.progress || {}, ).reduce( (sum: number, p: any) => sum + (p.migrated || 0) + (p.skipped || 0) + (p.failed || 0), 0, ); return total > 0 ? Math.round((processed / total) * 100) : 0; })()}%`, }} />
{/* Detailed Breakdown by Data Type */}
Progress by Data Type
{Object.entries(migrationProgress.progress || {}).map( ([key, progress]: [string, any]) => { const dataInfo = migrationStatus?.old_data?.[key]; if (!dataInfo || progress.total === 0) return null; const percentage = progress.total > 0 ? Math.round( (((progress.migrated || 0) + (progress.skipped || 0) + (progress.failed || 0)) / progress.total) * 100, ) : 0; return (
{progress.status === "running" ? ( ) : progress.status === "completed" ? ( ) : (
)} {dataInfo.label}
{(progress.migrated || 0) + (progress.skipped || 0) + (progress.failed || 0)} /{progress.total}
{/* Individual Progress Bar */}
{/* Stats */} {(progress.migrated || 0) + (progress.skipped || 0) + (progress.failed || 0) > 0 && (
{progress.migrated > 0 && ( ✓ {progress.migrated} migrated )} {progress.skipped > 0 && ( ⊘ {progress.skipped} skipped )} {progress.failed > 0 && ( ✗ {progress.failed} failed )}
)}
); }, )}
)} {/* Migration Summary (show after completion) */} {migrationProgress?.all_complete && migrationProgress?.started_at && Object.keys(migrationProgress.progress || {}).length > 0 && (() => { const totalMigrated = Object.values( migrationProgress.progress || {}, ).reduce( (sum: number, p: any) => sum + (p.migrated || 0), 0, ); const totalSkipped = Object.values( migrationProgress.progress || {}, ).reduce( (sum: number, p: any) => sum + (p.skipped || 0), 0, ); const totalFailed = Object.values( migrationProgress.progress || {}, ).reduce((sum: number, p: any) => sum + (p.failed || 0), 0); return (

Migration Summary

✓ {totalMigrated} migrated {totalSkipped > 0 && ( ⊘ {totalSkipped} skipped )} {totalFailed > 0 && ( ✗ {totalFailed} failed )}
{Object.entries(migrationProgress.progress || {}).map( ([key, progress]: [string, any]) => { const dataInfo = migrationStatus?.old_data?.[key]; if (!dataInfo) return null; const totalProcessed = (progress.migrated || 0) + (progress.skipped || 0) + (progress.failed || 0); if (totalProcessed === 0) return null; return (
{dataInfo.label}
{progress.migrated > 0 && ( ✓ {progress.migrated} )} {progress.skipped > 0 && ( ⊘ {progress.skipped} )} {progress.failed > 0 && ( ✗ {progress.failed} )}
); }, )}
); })()} {/* Data Type Cards - Simple Display */}
{Object.entries(migrationStatus.old_data || {}).map( ([key, data]: [string, any]) => (

{data.label}

{data.description}

{data.table}

{data.count}
), )}
) : ( // No Data State

No Migration Needed

Your database is up to date

)} )} {/* Logs Tab */} {activeTab === "logs" && (
{/* Log Type Selector */}
{logTypes.map((type) => { const Icon = type.icon; return ( ); })}
{/* Logs Display */}

{selectedLogType} Logs

{isLoadingLogs ? (
{/* Logs Skeleton */} {[...Array(3)].map((_, index) => (
))}
) : logs[selectedLogType]?.logs?.length > 0 ? ( logs[selectedLogType].logs.map((log) => (
{getLogLevelBadge(log.level)} {new Date(log.timestamp).toLocaleString()}

{log.message}

{log.context && Object.keys(log.context).length > 0 && (
View Context
                          {JSON.stringify(log.context, null, 2)}
                        
)}
)) ) : (

No logs found for this type

)}
)} {/* Export Modal */} {showExportModal && (

Select Data Types to Export

{/* Select All Option */}
{dataTypes.map((dataType) => ( ))}

{selectedExportData.length} data types selected

)} {/* Delete Confirmation Modal */} {showDeleteModal && (

Confirm Deletion

Delete Export File?

This action cannot be undone. The export file will be permanently deleted from the server.

)} {/* Clear Logs Confirmation Modal */} {showClearLogsModal && (

Clear Logs

Clear {selectedLogType} logs?

This action cannot be undone. All {selectedLogType} logs will be permanently deleted from the server.

)} {/* Import Modal */} {showImportModal && pendingImportFile && (

Select Data Types to Import

File: {pendingImportFile.name}

{dataTypes.map((dataType) => ( ))}

{selectedImportData.length} data types selected

)} {/* Migration Confirmation Modal */} setShowMigrationConfirm(false)} title={__("Migrate All Data")} description={__( "This process runs in the background and may take several minutes. Please back up your database before proceeding.", )} size="md" footer={
} >

{__( "All legacy data types will be migrated to the new Yatra 3.0 database structure. The migration runs asynchronously via WordPress cron.", )}

{__("Important")}

{__( "Keep this tab open to monitor progress. Use “Re-migrate All” only if you need to reprocess items that were already migrated.", )}

{migrationStatus?.pro_migration && !migrationStatus.pro_migration.ready && (

{__("Yatra Pro", "yatra")}

{migrationStatus.pro_migration.warning_message}

)}
{/* Cache View Modal */} setShowCacheModal(false)} title="View Cache Data" size="lg" >
{isLoadingCache ? (
Loading cache data...
) : ( <>

{cacheData.length} cache items found

{cacheData.length === 0 ? (

No cache data found

) : (
{cacheData.map((item, index) => ( ))}
Cache Key Type Size Created Expires Actions
{item.key}
                                  {item.value}
                                
{item.type} {formatBytes(item.size)} {item.created_at} {item.expires_at}
)} )}
); }; export default Tools;