import React, { useState, useRef } from 'react'; import { X, Upload, FileSpreadsheet, Loader, CheckCircle, AlertCircle, Trash2 } from 'lucide-react'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; const PURPLE = '#A78BFA'; // --------------------------------------------------------------------------- // Styles // --------------------------------------------------------------------------- const OVERLAY_STYLE = { position: 'fixed', inset: 0, background: 'rgba(0, 0, 0, 0.7)', backdropFilter: 'blur(4px)', zIndex: 100, display: 'flex', alignItems: 'center', justifyContent: 'center', }; const MODAL_STYLE = { background: 'linear-gradient(180deg, #0F1A2E 0%, #0A1628 100%)', border: `1px solid ${PURPLE}40`, borderRadius: '1rem', width: '90%', maxWidth: '900px', maxHeight: '85vh', overflow: 'auto', padding: '2rem', boxShadow: `0 0 60px rgba(167, 139, 250, 0.15)`, }; const DROP_ZONE_STYLE = { border: `2px dashed ${PURPLE}50`, borderRadius: '0.75rem', padding: '3rem 2rem', textAlign: 'center', cursor: 'pointer', transition: 'border-color 0.2s, background 0.2s', }; const DROP_ZONE_ACTIVE = { ...DROP_ZONE_STYLE, borderColor: PURPLE, background: `${PURPLE}10`, }; const FILE_ROW_STYLE = { display: 'flex', alignItems: 'center', gap: '1rem', padding: '0.75rem 1rem', borderRadius: '0.5rem', background: 'rgba(15, 23, 42, 0.6)', border: '1px solid rgba(255,255,255,0.06)', marginBottom: '0.5rem', }; // phase: idle → uploading → preview → committing → done | error export default function MultiVerticalUploadModal({ onClose, onUploadComplete }) { const [phase, setPhase] = useState('idle'); const [files, setFiles] = useState([]); const [previewData, setPreviewData] = useState(null); const [error, setError] = useState(null); const [dragOver, setDragOver] = useState(false); const [commitResult, setCommitResult] = useState(null); const fileInputRef = useRef(null); // Handle file selection const handleFiles = (fileList) => { const newFiles = Array.from(fileList).filter(f => f.name.toLowerCase().endsWith('.xlsx')); if (newFiles.length === 0) { setError('Please select .xlsx files'); return; } setFiles(prev => { const existing = new Set(prev.map(f => f.name)); const unique = newFiles.filter(f => !existing.has(f.name)); return [...prev, ...unique]; }); setError(null); }; const removeFile = (index) => { setFiles(prev => prev.filter((_, i) => i !== index)); }; // Upload and preview const handlePreview = async () => { if (files.length === 0) return; setPhase('uploading'); setError(null); const formData = new FormData(); for (const file of files) { formData.append('files', file); } try { const res = await fetch(`${API_BASE}/compliance/vcl-multi/preview`, { method: 'POST', credentials: 'include', body: formData, }); const data = await res.json(); if (!res.ok) { setError(data.error || 'Upload failed'); setPhase('idle'); return; } setPreviewData(data); setPhase('preview'); } catch (err) { setError('Network error: ' + err.message); setPhase('idle'); } }; // Commit const handleCommit = async () => { if (!previewData || !previewData.files || previewData.files.length === 0) return; setPhase('committing'); setError(null); try { const res = await fetch(`${API_BASE}/compliance/vcl-multi/commit`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ files: previewData.files.map(f => ({ tempFile: f.tempFile, vertical: f.vertical, report_date: f.report_date, filename: f.filename, })), }), }); const data = await res.json(); if (!res.ok) { setError(data.error || 'Commit failed'); setPhase('preview'); return; } setCommitResult(data); setPhase('done'); } catch (err) { setError('Network error: ' + err.message); setPhase('preview'); } }; // Remove a file from preview const removePreviewFile = (index) => { setPreviewData(prev => ({ ...prev, files: prev.files.filter((_, i) => i !== index), })); }; return (
e.stopPropagation()}> {/* Header */}

Upload Vertical Files

{/* Phase: Idle — file selection */} {phase === 'idle' && ( <> {/* Drop zone */}
{ e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={e => { e.preventDefault(); setDragOver(false); handleFiles(e.dataTransfer.files); }} onClick={() => fileInputRef.current?.click()} >
Drop xlsx files here or click to browse
Expected format: VERTICAL_YYYY_MM_DD.xlsx (up to 14 files)
handleFiles(e.target.files)} /> {/* Selected files list */} {files.length > 0 && (
Selected Files ({files.length})
{files.map((file, i) => (
{file.name} {(file.size / 1024).toFixed(0)} KB
))}
)} )} {/* Phase: Uploading */} {phase === 'uploading' && (
Parsing {files.length} file(s)...
Extracting verticals and computing diffs
)} {/* Phase: Preview */} {phase === 'preview' && previewData && ( <>
Preview — {previewData.files.length} file(s) ready
{/* Preview table */}
{previewData.files.map((f, i) => ( ))}
Vertical Date Items New Recurring Resolved
{f.vertical} {f.report_date} {f.total_items} {f.diff.new_count} {f.diff.recurring_count} {f.diff.resolved_count} {previewData.files.length > 1 && ( )}
Total {previewData.files.reduce((s, f) => s + f.total_items, 0)} {previewData.files.reduce((s, f) => s + f.diff.new_count, 0)} {previewData.files.reduce((s, f) => s + f.diff.recurring_count, 0)} {previewData.files.reduce((s, f) => s + f.diff.resolved_count, 0)}
{/* Unrecognized files */} {previewData.unrecognized && previewData.unrecognized.length > 0 && (
Unrecognized Files ({previewData.unrecognized.length})
{previewData.unrecognized.map((u, i) => (
{u.filename}: {u.error}
))}
)} {/* Actions */}
)} {/* Phase: Committing */} {phase === 'committing' && (
Committing batch...
Writing to database with vertical-scoped resolution
)} {/* Phase: Done */} {phase === 'done' && commitResult && (
Upload Complete
{commitResult.committed.length} vertical(s) committed — {commitResult.total_new} new, {commitResult.total_resolved} resolved
{/* Per-vertical summary */}
{commitResult.committed.map((c, i) => (
{c.vertical} +{c.new_count} new / {c.recurring_count} recurring / -{c.resolved_count} resolved
))}
)} {/* Error display */} {error && (
{error}
)}
); }