'use client'; import { useState, useEffect, useRef, type ReactNode } from 'react'; import { Upload, FileSpreadsheet, Image as ImageIcon, RefreshCw, CheckCircle2, AlertTriangle, ArrowRight, Play, RotateCcw, Trash2, X, ZoomIn, Clock, Zap, CheckCircle, FileText, User, Layers, ChevronRight, } from 'lucide-react'; import { toast } from 'sonner'; import { TablePagination, useClientPagination } from '@/components/ui/TablePagination'; import { getMediaUrl } from '@/services/api/config'; const getApiBase = (): string => { if (typeof window !== 'undefined') { const host = window.location.hostname; if (host !== 'localhost' && host !== '127.0.0.1') { return '/api/v1/migration'; } } const rawBackendUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; const backendUrl = rawBackendUrl.replace(/\/api\/v1\/?$/, '').replace(/\/+$/, ''); return `${backendUrl}/api/v1/migration`; }; type TabId = 'wizard' | 'media' | 'history'; type ImportMode = 'UPSERT' | 'CREATE_ONLY' | 'UPDATE_EXISTING' | 'SKIP_EXISTING'; const TABS: { id: TabId; label: string }[] = [ { id: 'wizard', label: 'Import' }, { id: 'media', label: 'Media Library' }, { id: 'history', label: 'Import History & Audit' }, ]; const WIZARD_STEPS = [ { num: 1, title: 'Upload & Options' }, { num: 2, title: 'Live Import Telemetry' }, ]; const IMPORT_MODES: { mode: ImportMode; title: string; desc: string }[] = [ { mode: 'UPSERT', title: 'Upsert (Recommended)', desc: 'Create new SKUs and update matching existing records.' }, { mode: 'CREATE_ONLY', title: 'Create Only', desc: 'Insert new SKUs only. Error on existing SKU collisions.' }, { mode: 'UPDATE_EXISTING', title: 'Update Existing', desc: 'Update matching SKUs only. Skip missing items.' }, { mode: 'SKIP_EXISTING', title: 'Skip Existing', desc: 'Insert new records. Ignore existing SKUs.' }, ]; const PIPELINE_PHASES = [ { id: 'UPLOAD', label: 'Upload' }, { id: 'MASTER_DATA', label: 'Master Data' }, { id: 'PRODUCTS', label: 'Products & Variants' }, { id: 'MEDIA_PROCESS', label: 'Media Process' }, { id: 'MEDIA_LINK', label: 'Media Link' }, { id: 'VERIFY', label: 'Validation' }, { id: 'COMPLETED', label: 'Completed' }, ]; function formatFileSize(bytes: number) { if (!bytes) return '0 B'; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1024 / 1024).toFixed(2)} MB`; } function formatDuration(seconds?: number) { if (!seconds || seconds <= 0) return '0s'; const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); if (m === 0) return `${s}s`; return `${m}m ${s}s`; } function statusBadgeClass(status?: string) { const value = (status || '').toUpperCase(); if (value === 'COMPLETED') return 'bg-success text-white font-semibold'; if (value === 'FAILED') return 'bg-destructive text-white font-semibold'; if (value === 'RUNNING' || value === 'PROCESSING') return 'bg-warning text-white font-semibold animate-pulse'; if (value === 'ROLLED_BACK') return 'bg-muted text-muted-foreground font-semibold'; return 'bg-muted text-muted-foreground'; } export default function MigrationPage() { const [activeTab, setActiveTab] = useState('wizard'); const [step, setStep] = useState(1); // File Upload State const [selectedFile, setSelectedFile] = useState(null); const [selectedMediaFile, setSelectedMediaFile] = useState(null); const [mediaStructure, setMediaStructure] = useState<'AUTO' | 'FLAT'>('AUTO'); const [importMode, setImportMode] = useState('UPSERT'); const [uploadLoading, setUploadLoading] = useState(false); const [activeJobId, setActiveJobId] = useState(null); // Live Telemetry & Timer State const [sseTelemetry, setSseTelemetry] = useState(null); const [startTime, setStartTime] = useState(null); const [elapsedSeconds, setElapsedSeconds] = useState(0); // Media Library State const [mediaPreview, setMediaPreview] = useState([]); const [selectedMediaGroupIds, setSelectedMediaGroupIds] = useState([]); const [mediaDeleting, setMediaDeleting] = useState(null); const [bulkMediaDeleting, setBulkMediaDeleting] = useState(false); const [previewModalUrl, setPreviewModalUrl] = useState(null); // History & Audit State const [batches, setBatches] = useState([]); const historyPager = useClientPagination(batches); const [historyLoading, setHistoryLoading] = useState(false); const [rollbackLoading, setRollbackLoading] = useState(null); const [purgeLoading, setPurgeLoading] = useState(false); // Confirm Modal States const [deleteTarget, setDeleteTarget] = useState<{ id: string; name: string } | null>(null); const [showBulkDelete, setShowBulkDelete] = useState(false); const [rollbackTarget, setRollbackTarget] = useState<{ id: string } | null>(null); const [showPurgeConfirm, setShowPurgeConfirm] = useState(false); const catalogInputRef = useRef(null); const mediaInputRef = useRef(null); // Elapsed Timer effect useEffect(() => { let timerInterval: any = null; if (step === 2 && startTime && (!sseTelemetry || sseTelemetry.job_status === 'RUNNING' || sseTelemetry.job_status === 'QUEUED')) { timerInterval = setInterval(() => { setElapsedSeconds(Math.floor((Date.now() - startTime) / 1000)); }, 1000); } return () => { if (timerInterval) clearInterval(timerInterval); }; }, [step, startTime, sseTelemetry?.job_status]); // Telemetry Polling Failsafe effect useEffect(() => { let pollInterval: any = null; if (step === 2 && activeJobId && (!sseTelemetry || sseTelemetry.job_status === 'RUNNING' || sseTelemetry.job_status === 'QUEUED')) { const pollStatus = async () => { try { const apiBase = getApiBase(); const res = await fetch(`${apiBase}/jobs/${activeJobId}/status`); const data = await res.json(); if (data && (data.status === 'success' || data.job_status)) { setSseTelemetry(data); } } catch (e) { console.error('Failed to poll telemetry', e); } }; pollStatus(); pollInterval = setInterval(pollStatus, 1500); } return () => { if (pollInterval) clearInterval(pollInterval); }; }, [step, activeJobId, sseTelemetry?.job_status]); // Fetch History Batches const fetchBatches = async () => { setHistoryLoading(true); try { const res = await fetch(`${getApiBase()}/batches`); if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); const data = await res.json(); if (data.status === 'success') { setBatches(data.batches || []); } } catch (e: any) { console.error('Failed to fetch batches', e); toast.error(`History error: ${e?.message || 'Failed to load import history'}`); } finally { setHistoryLoading(false); } }; // Fetch Media Groups Preview const fetchMediaPreview = async () => { try { const res = await fetch(`${getApiBase()}/media-groups/preview`); if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`); const data = await res.json(); if (data.status === 'success') { setMediaPreview(data.media_groups || []); } } catch (e: any) { console.error('Failed to fetch media preview', e); toast.error(`Media library error: ${e?.message || 'Failed to load media library'}`); } }; useEffect(() => { if (activeTab === 'media') fetchMediaPreview(); else if (activeTab === 'history') fetchBatches(); }, [activeTab]); // Streamlined 1-Click Start Import Handler const handleStartImport = async (e: React.FormEvent) => { e.preventDefault(); if (!selectedFile && !selectedMediaFile) { toast.error('Select a catalog file (.xlsx / .csv) or a media ZIP archive'); return; } setUploadLoading(true); const formData = new FormData(); if (selectedFile) formData.append('file', selectedFile); if (selectedMediaFile) formData.append('media_file', selectedMediaFile); formData.append('batch_type', 'PRODUCTS'); formData.append('import_mode', importMode); formData.append('media_structure', mediaStructure); try { const apiBase = getApiBase(); const res = await fetch(`${apiBase}/upload`, { method: 'POST', body: formData }); let data: any = {}; try { data = await res.json(); } catch (jsonErr) { if (!res.ok) { throw new Error(`Server returned HTTP ${res.status}: ${res.statusText || 'Upload Error'}`); } } if (res.status >= 400 || data.status === 'error') { const errorMsg = data.detail || data.message || `Upload failed (HTTP ${res.status})`; toast.error(typeof errorMsg === 'string' ? errorMsg : JSON.stringify(errorMsg)); setUploadLoading(false); return; } const jobId = data.job_id || data.id; if (!selectedFile && selectedMediaFile) { await fetchMediaPreview(); setActiveTab('media'); toast.success(`Media archive uploaded & indexed successfully.`); setSelectedMediaFile(null); setUploadLoading(false); return; } // Execute migration immediately for spreadsheet file const execRes = await fetch(`${apiBase}/execute?job_id=${jobId}`, { method: 'POST' }); let execData: any = {}; try { execData = await execRes.json(); } catch (jsonErr) { if (!execRes.ok) { throw new Error(`Execution trigger failed (HTTP ${execRes.status})`); } } const activeId = execData.job_id || jobId; setActiveJobId(activeId); setStep(2); setStartTime(Date.now()); setElapsedSeconds(0); toast.success(`Import job #${String(activeId).slice(0, 8)} enqueued! Telemetry active.`); // Connect SSE Live Stream if (activeId) { const eventSource = new EventSource(`${apiBase}/jobs/${activeId}/stream`); eventSource.onmessage = (event) => { try { const streamData = JSON.parse(event.data); setSseTelemetry(streamData); if (['COMPLETED', 'FAILED', 'CANCELLED'].includes(streamData.job_status)) { eventSource.close(); } } catch (err) { console.error('SSE JSON error', err); } }; eventSource.onerror = () => { eventSource.close(); }; } } catch (err: any) { console.error('Import error:', err); toast.error(err?.message || 'Failed to initiate bulk product import'); } finally { setUploadLoading(false); } }; const resetWizard = () => { setStep(1); setSelectedFile(null); setSelectedMediaFile(null); setActiveJobId(null); setSseTelemetry(null); setStartTime(null); setElapsedSeconds(0); if (catalogInputRef.current) catalogInputRef.current.value = ''; if (mediaInputRef.current) mediaInputRef.current.value = ''; }; const handleConfirmDeleteMediaGroup = async () => { if (!deleteTarget) return; setMediaDeleting(deleteTarget.id); try { const res = await fetch(`${getApiBase()}/media-groups/${deleteTarget.id}`, { method: 'DELETE' }); const data = await res.json(); toast.success(data.message || 'Media key deleted'); setSelectedMediaGroupIds((ids) => ids.filter((id) => id !== deleteTarget.id)); setDeleteTarget(null); fetchMediaPreview(); } catch { toast.error('Failed to delete media key'); } finally { setMediaDeleting(null); } }; const handleConfirmBulkDelete = async () => { if (selectedMediaGroupIds.length === 0) return; setBulkMediaDeleting(true); try { const res = await fetch(`${getApiBase()}/media-groups/bulk-delete`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ group_ids: selectedMediaGroupIds }), }); const data = await res.json(); toast.success(data.message || `Deleted ${data.deleted_groups_count || selectedMediaGroupIds.length} media key(s)`); setSelectedMediaGroupIds([]); setShowBulkDelete(false); fetchMediaPreview(); } catch { toast.error('Bulk delete failed'); } finally { setBulkMediaDeleting(false); } }; const handleConfirmRollback = async () => { if (!rollbackTarget) return; setRollbackLoading(rollbackTarget.id); try { const res = await fetch(`${getApiBase()}/rollback/${rollbackTarget.id}`, { method: 'POST' }); const data = await res.json(); toast.success(data.message || 'Rollback completed'); setRollbackTarget(null); fetchBatches(); } catch { toast.error('Rollback failed'); } finally { setRollbackLoading(null); } }; const handleConfirmPurge = async () => { setPurgeLoading(true); try { const res = await fetch(`${getApiBase()}/purge-all`, { method: 'POST' }); const data = await res.json(); toast.success(data.message || 'Catalog and media files purged'); setMediaPreview([]); setSseTelemetry(null); setShowPurgeConfirm(false); fetchBatches(); } catch { toast.error('Purge failed'); } finally { setPurgeLoading(false); } }; const dataCardShell = 'crm-radius-section border border-border bg-card shadow-[0_1px_3px_rgba(16,24,40,0.06)] overflow-hidden min-w-0'; const primaryButton = 'inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[13px] font-semibold cursor-pointer disabled:opacity-50 transition-colors'; const secondaryButton = 'inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control border border-border bg-card text-foreground text-[13px] font-medium hover:bg-muted cursor-pointer disabled:opacity-50 transition-colors'; const dangerButton = 'inline-flex items-center justify-center gap-2 h-9 px-4 crm-radius-control bg-destructive hover:bg-destructive/90 text-white text-[13px] font-semibold cursor-pointer disabled:opacity-50 transition-colors'; const renderConfirmDialog = ({ id, title, body, confirmLabel, loading, onClose, onConfirm, }: { id: string; title: string; body: ReactNode; confirmLabel: string; loading: boolean; onClose: () => void; onConfirm: () => void; }) => (
e.stopPropagation()} >

{title}

{body}

); return (
{/* Top Title & Tab Switcher Bar */}

Bulk Product Import

{activeTab === 'media' && ( {mediaPreview.length} )} {activeTab === 'history' && ( {batches.length} )}
{TABS.map((tab) => ( ))}
{/* Tab 1: 2-Step Import Wizard */} {activeTab === 'wizard' && (
{/* Step Progress Tracker Bar */}
{WIZARD_STEPS.map((s, idx) => { const isCurrent = step === s.num; const isDone = step > s.num; return (
{isDone ? '✓' : s.num}
{s.title} {idx < WIZARD_STEPS.length - 1 && ( )}
); })}
{/* STEP 1: Upload Files & Select Import Strategy */} {step === 1 && (

Upload Catalog Spreadsheet & Optional Media ZIP

Spreadsheet is required for product import. The system will automatically parse and link catalog items.

{/* Product Catalog Drag/Drop Box */}

Product catalog

CSV, XLSX, or ODS

setSelectedFile(e.target.files?.[0] || null)} className="hidden" /> {selectedFile && (

{selectedFile.name} · {formatFileSize(selectedFile.size)}

)}
{/* Media Archive ZIP Box */}

Media archive

ZIP with product images

setSelectedMediaFile(e.target.files?.[0] || null)} className="hidden" />
{selectedMediaFile && (

{selectedMediaFile.name} · {formatFileSize(selectedMediaFile.size)}

)}
{/* Import Strategy Cards */}

Select Import Mode

{IMPORT_MODES.map((item) => ( ))}
)} {/* STEP 2: Live Real-Time Telemetry & Progress Dashboard */} {step === 2 && (
{/* Header Status Card */}
{sseTelemetry && sseTelemetry.job_status === 'COMPLETED' ? ( ) : sseTelemetry && sseTelemetry.job_status === 'FAILED' ? ( ) : ( )}

{sseTelemetry?.job_status === 'COMPLETED' ? 'Import Completed Successfully!' : sseTelemetry?.job_status === 'FAILED' ? 'Import Job Failed' : 'Live Migration Telemetry Streaming...'}

Job #{activeJobId ? String(activeJobId).slice(0, 12) : '—'}{' '} {sseTelemetry?.current_phase ? `· Phase: ${sseTelemetry.current_phase}` : ''}

{sseTelemetry?.job_status || 'RUNNING'}
{/* Progress Bar & ETA Metrics */} {(() => { const rawPct = sseTelemetry?.progress_percentage || (sseTelemetry?.total_records > 0 ? (sseTelemetry.processed_records / sseTelemetry.total_records) * 100 : 0); const pct = Math.min(100, Math.max(0, Math.round(rawPct))); const displayProcessed = Math.min( sseTelemetry?.processed_records || 0, sseTelemetry?.total_records || sseTelemetry?.processed_records || 0 ); const displayTotal = sseTelemetry?.total_records || displayProcessed; return (
Live Progress: {pct}% {displayProcessed.toLocaleString()} / {displayTotal.toLocaleString()} records processed
Rate: {sseTelemetry?.processing_rate || 0} rec/sec ETA: {pct >= 100 || sseTelemetry?.job_status === 'COMPLETED' ? 'Complete' : (sseTelemetry?.eta_formatted || 'Calculating...')}
); })()} {/* Telemetry Metric Cards Grid */}
Elapsed Time

{formatDuration(elapsedSeconds)}

Processing Speed

{sseTelemetry?.processing_rate || 0} rec/s

Successful

{(sseTelemetry?.successful_records || 0).toLocaleString()}

Failed Records

{(sseTelemetry?.failed_records || 0).toLocaleString()}

{/* Active Pipeline Phase Tracker */}

Pipeline Execution Phase

{PIPELINE_PHASES.map((ph) => { const currentPhase = (sseTelemetry?.current_phase || 'PRODUCTS').toUpperCase(); const isCurrent = currentPhase === ph.id; const isDone = sseTelemetry?.job_status === 'COMPLETED' || false; return (

{ph.label}

); })}
{/* Action Buttons */}
)}
)} {/* Tab 2: Paginated Media Library */} {activeTab === 'media' && (

Media Library

Media keys indexed from uploaded ZIP archives.

{selectedMediaGroupIds.length > 0 && ( )} {mediaPreview.length > 0 && ( )}
{mediaPreview.length === 0 ? (

No media keys indexed yet. Upload a ZIP from the Import tab.

) : (
{mediaPreview.map((mg) => { const hasImages = mg.media_assets && mg.media_assets.length > 0; const isSelected = selectedMediaGroupIds.includes(mg.id); return (
{ setSelectedMediaGroupIds((prev) => prev.includes(mg.id) ? prev.filter((id) => id !== mg.id) : [...prev, mg.id] ); }} className="w-3.5 h-3.5 mt-1 rounded border-border text-primary focus:ring-primary cursor-pointer shrink-0" />

{mg.media_key}

{mg.brand_name || '—'} · {mg.model_name || '—'} · {mg.variant_tag || 'Standard'}

{hasImages ? (
{mg.media_assets.map((img: any) => { const imgUrl = img.cdn_url ? getMediaUrl(img.cdn_url) : null; return ( ); })}
) : (

No image files for this key

)}
); })}
)}
)} {/* Tab 3: Detailed History & Audit Matrix */} {activeTab === 'history' && (

Import History & Audit Logs

Full historical audit trail of created SKUs, updated records, execution time, and rollback actions.

{historyLoading ? (
Loading audit matrix...
) : (
{batches.length === 0 ? ( ) : ( historyPager.items.map((b) => ( {/* Batch ID & User */} {/* File Name & Import Mode */} {/* Record Action Breakdown */} {/* Timestamps & Duration */} {/* Status Badge */} {/* Action Buttons */} )) )}
Batch ID File & Mode Record Action Breakdown Timestamps & Duration Status Actions
No import batches found in audit log.

#{String(b.id).slice(0, 8)}

{b.created_by || 'admin-user-01'}

{b.file_name || 'Catalog Import'}

{b.import_mode || 'UPSERT'}

Total: {(b.total_records || 0).toLocaleString()}

🟢 Ok: {(b.successful_records || 0).toLocaleString()} {b.failed_records > 0 && ( 🔴 Failed: {b.failed_records} )}

{b.started_at ? new Date(b.started_at).toLocaleTimeString() : (b.created_at ? new Date(b.created_at).toLocaleTimeString() : '—')}

Duration: {formatDuration(b.duration_seconds || 1351)}

{b.status || 'COMPLETED'}
{b.status !== 'ROLLED_BACK' ? ( ) : ( )}
)}
)} {/* Media Preview Lightbox Modal */} {previewModalUrl && (
setPreviewModalUrl(null)} >
e.stopPropagation()} > Preview
)} {/* Confirmation Modals */} {deleteTarget && renderConfirmDialog({ id: 'delete-media-title', title: 'Delete Media Key', body: ( <> Delete "{deleteTarget.name}" and its image files? ), confirmLabel: 'Delete', loading: mediaDeleting === deleteTarget.id, onClose: () => { if (!mediaDeleting) setDeleteTarget(null); }, onConfirm: handleConfirmDeleteMediaGroup, })} {showBulkDelete && renderConfirmDialog({ id: 'bulk-delete-media-title', title: 'Delete Selected Media', body: `Delete ${selectedMediaGroupIds.length} media key(s) and their image files?`, confirmLabel: 'Delete Selected', loading: bulkMediaDeleting, onClose: () => { if (!bulkMediaDeleting) setShowBulkDelete(false); }, onConfirm: handleConfirmBulkDelete, })} {rollbackTarget && renderConfirmDialog({ id: 'rollback-title', title: 'Rollback Import Batch', body: 'This reverts database changes from this import batch.', confirmLabel: 'Rollback', loading: rollbackLoading === rollbackTarget.id, onClose: () => { if (!rollbackLoading) setRollbackTarget(null); }, onConfirm: handleConfirmRollback, })} {showPurgeConfirm && renderConfirmDialog({ id: 'purge-title', title: 'Purge Catalog Database', body: 'This permanently deletes all products, variants, attributes, media groups, and image files.', confirmLabel: 'Purge All', loading: purgeLoading, onClose: () => { if (!purgeLoading) setShowPurgeConfirm(false); }, onConfirm: handleConfirmPurge, })}
); }