import React, { useState, useRef, useCallback, useEffect } from 'react' const STATUS_POLL_MS = 1500 // poll /api/upload/status every 1.5s while running export default function UploadModal({ open, onClose, onSuccess }) { const [phase, setPhase] = useState('idle') // idle | uploading | running | done | error const [message, setMessage] = useState('') const [logs, setLogs] = useState('') const [showLogs, setShowLogs] = useState(false) const [dragOver, setDragOver] = useState(false) const [filename, setFilename] = useState('') const fileRef = useRef(null) const pollRef = useRef(null) const logsRef = useRef(null) // Auto-scroll logs useEffect(() => { if (logsRef.current) logsRef.current.scrollTop = logsRef.current.scrollHeight }, [logs]) // Stop polling on unmount useEffect(() => () => clearInterval(pollRef.current), []) // Reset when modal opens useEffect(() => { if (open) { setPhase('idle'); setMessage(''); setLogs('') setShowLogs(false); setFilename(''); setDragOver(false) // Also reset server-side job state fetch('/api/upload/reset', { method: 'POST' }).catch(() => {}) } }, [open]) function startPolling() { clearInterval(pollRef.current) pollRef.current = setInterval(async () => { try { const [sRes, lRes] = await Promise.all([ fetch('/api/upload/status'), fetch('/api/upload/logs'), ]) const s = await sRes.json() const l = await lRes.json() setLogs(l.logs || '') setMessage(s.message || '') if (s.status === 'done') { clearInterval(pollRef.current) setPhase('done') } else if (s.status === 'error') { clearInterval(pollRef.current) setPhase('error') setShowLogs(true) } } catch { // network hiccup — keep polling } }, STATUS_POLL_MS) } async function handleFile(file) { if (!file) return if (!file.name.match(/\.(xlsx|xlsm|xls)$/i)) { setPhase('error') setMessage('Only .xlsx / .xlsm / .xls files are accepted.') return } setFilename(file.name) setPhase('uploading') setMessage('Uploading file…') const form = new FormData() form.append('file', file) try { const res = await fetch('/api/upload', { method: 'POST', body: form }) const json = await res.json() if (!res.ok) { setPhase('error') setMessage(json.detail || 'Upload failed.') return } setPhase('running') setMessage('Pipeline started — processing data…') startPolling() } catch (err) { setPhase('error') setMessage('Cannot reach upload server. Is it running on port 8000?') } } const onDrop = useCallback(e => { e.preventDefault(); setDragOver(false) handleFile(e.dataTransfer.files[0]) }, []) const onDragOver = e => { e.preventDefault(); setDragOver(true) } const onDragLeave = () => setDragOver(false) function handleReload() { onClose() onSuccess(filename) // pass filename back to caller } if (!open) return null const isRunning = phase === 'uploading' || phase === 'running' return (
{ if (e.target === e.currentTarget && !isRunning) onClose() }}>
{/* Header */}
Upload New Data
Brand Onboarding Template (.xlsx)
{!isRunning && ( )}
{/* Drop zone — only shown when idle */} {phase === 'idle' && (
fileRef.current?.click()} style={{ border: `2px dashed ${dragOver ? 'var(--gn)' : 'var(--bd)'}`, borderRadius: 10, background: dragOver ? 'rgba(26,107,47,.04)' : 'var(--sf)', padding: '36px 20px', textAlign: 'center', cursor: 'pointer', transition: 'border-color .15s, background .15s', }}>
📂
Drag & drop your Excel file here
or click to browse — accepts .xlsx / .xlsm / .xls
Required sheets: 1_Source_Data · 2_Brand_Config · 3_PPA_MT · 4_PPA_TT
handleFile(e.target.files[0])} />
)} {/* Status area */} {phase !== 'idle' && (
{/* File badge */} {filename && (
📄 {filename}
)} {/* Status pill */}
{isRunning && } {phase === 'done' && } {phase === 'error' && }
{phase === 'uploading' && 'Uploading…'} {phase === 'running' && 'Pipeline running…'} {phase === 'done' && 'Complete!'} {phase === 'error' && 'Error'}
{message}
{/* Progress steps */}
)} {/* Logs toggle */} {(phase === 'running' || phase === 'done' || phase === 'error') && (
{showLogs && (
                  {logs || '(waiting for output…)'}
                
)}
)}
{/* Footer buttons */}
{phase === 'idle' && ( )} {phase === 'error' && ( <> )} {phase === 'done' && ( )}
) } function Steps({ phase }) { const steps = [ { key: 'upload', label: 'File uploaded' }, { key: 'pipeline', label: 'Pipeline running' }, { key: 'copy', label: 'JSON files updated' }, ] const activeIdx = phase === 'uploading' ? 0 : phase === 'running' ? 1 : phase === 'done' ? 3 : -1 return (
{steps.map((s, i) => { const done = phase === 'done' || (phase === 'running' && i === 0) const active = (phase === 'uploading' && i === 0) || (phase === 'running' && i === 1) const color = done ? 'var(--gn)' : active ? 'var(--saf)' : 'var(--bd2)' return (
{done ? '✓' : i + 1}
{s.label}
{i < steps.length - 1 && (
)}
) })}
) } function Spinner() { return (
) } function btnStyle(variant) { const base = { border: 'none', borderRadius: 8, cursor: 'pointer', fontWeight: 600, fontSize: 12, padding: '8px 18px' } if (variant === 'primary') return { ...base, background: 'var(--gn)', color: '#fff' } return { ...base, background: 'var(--sf3)', color: 'var(--mt)', border: '1px solid var(--bd)' } }