import { useState, useCallback, useMemo } from 'react'; import { useAppState, useAppDispatch } from '../context/AppContext.jsx'; import Header from '../components/Header.jsx'; import Loader from '../components/Loader.jsx'; import ColFilterDropdown from '../components/ColFilterDropdown.jsx'; import { showWarnToast, showInfoToast } from '../components/Toast.jsx'; import { parseCSVWithProgress, fmtSize, pKey } from '../utils/csvParser.js'; import { parsePastXLSX } from '../utils/pastResultParser.js'; const UploadIcon = () => ( ); const PanelIcon = () => ( ); const DownloadIcon = () => ( ); function FileCard({ id, label, color, hint, accept, file, onFile, onRemove, children }) { const [drag, setDrag] = useState(false); const [err, setErr] = useState(false); function handleFile(f) { setErr(false); if (id === 'r' && !f.name.match(/\.xlsx$/i)) { setErr(true); return; } if (id !== 'r' && !f.name.toLowerCase().endsWith('.csv')) { setErr(true); return; } onFile(f); } return (
{label}
{id === 'r' ? 'Optional' : 'Required'}
{!file ? (
{ e.preventDefault(); setDrag(true); }} onDragLeave={() => setDrag(false)} onDrop={e => { e.preventDefault(); setDrag(false); const f = e.dataTransfer.files[0]; if (f) handleFile(f); }} onClick={() => document.getElementById(`inp-${id}`).click()} > { const f = e.target.files[0]; if (f) handleFile(f); e.target.value = ''; }} />
{id === 'd' ? : id === 'p' ? : }
Drop {id === 'r' ? 'Excel' : 'CSV'} or browse
{hint}
) : (
{file === true ? '(from past result)' : file.name} {file === true ? '' : fmtSize(file.size)}
)} {err &&
Only {id === 'r' ? '.xlsx' : '.csv'} accepted
} {children}
); } export default function Step1Setup({ onNext, onNav }) { const state = useAppState(); const dispatch = useAppDispatch(); const { files, dictCSV, panelCSV, selectedPanels, p1ColFilters, fromPastResult } = state; const [loading, setLoading] = useState(false); const [loadMsg, setLoadMsg] = useState(''); const [loadPct, setLoadPct] = useState(null); const [restoreMsg, setRestoreMsg] = useState(null); const [ddCol, setDdCol] = useState(null); const [ddAnchor, setDdAnchor] = useState(null); const [panelSearch, setPanelSearch] = useState(''); // cascading filter options for P1 function cascadeVals(col) { if (!panelCSV) return []; const others = Object.entries(p1ColFilters).filter(([c]) => c !== col); return [...new Set( panelCSV.rows .filter(row => others.every(([c, vals]) => !vals || !vals.size || vals.has((row[c] || '').trim()))) .map(row => (row[col] || '').trim()) .filter(Boolean) )].sort(); } function panelRowPasses(row) { for (const [col, vals] of Object.entries(p1ColFilters)) { if (vals && vals.size && !vals.has((row[col] || '').trim())) return false; } if (panelSearch) { const q = panelSearch.toLowerCase(); if (!panelCSV.headers.some(h => String(row[h] || '').toLowerCase().includes(q))) return false; } return true; } const filteredRows = useMemo(() => { if (!panelCSV) return []; return panelCSV.rows.filter(panelRowPasses); }, [panelCSV, p1ColFilters, panelSearch]); async function handleDictFile(file) { setLoadMsg('Reading ' + file.name + '…'); setLoading(true); setLoadPct(0); const parsed = await parseCSVWithProgress(await file.text(), (pct) => setLoadPct(pct)); setLoading(false); // Validate const required = ['VARIABLE','PRIMARY_METRICS','SN_ACTIVITY_LEVEL_1']; const missing = required.filter(c => !parsed.headers.includes(c)); if (missing.length) showWarnToast('Dictionary missing columns: ' + missing.join(', ')); dispatch({ type: 'SET_DICT_CSV', payload: parsed }); dispatch({ type: 'SET_FILE', key: 'd', value: file }); // Apply restored vars if pending if (state._rVars) { const rows = parsed.rows.map(r => ({ ...r, _sel: state._rVars.has((r.VARIABLE || '').trim()) })); const token = rows.length + '|' + parsed.headers.join(','); dispatch({ type: 'SET_ALL_ROWS', payload: rows, token }); dispatch({ type: 'SET_RESTORE_BUFFERS', payload: { _rVars: null } }); } } async function handlePanelFile(file) { setLoadMsg('Reading ' + file.name + '…'); setLoading(true); setLoadPct(0); const parsed = await parseCSVWithProgress(await file.text(), (pct) => setLoadPct(pct)); setLoading(false); let newSelected = new Set(); if (state._rPanels && state._rPanels.length) { const pHeaders = parsed.headers; const firstCol = pHeaders[0] || ''; parsed.rows.forEach(row => { const k = pKey(row); const bestScore = state._rPanels.reduce((best, rp) => { const rv = (row[firstCol] || '').trim().toLowerCase(); const rpv = (rp[firstCol] || '').toLowerCase(); if (firstCol && rpv && rv !== rpv) return best; const nonEmpty = pHeaders.filter(h => rp[h]); if (!nonEmpty.length) return best; const matches = nonEmpty.filter(h => (row[h] || '').trim().toLowerCase() === rp[h].toLowerCase()); return Math.max(best, matches.length / nonEmpty.length); }, 0); if (bestScore >= 0.5) newSelected.add(k); }); if (newSelected.size === 0) showWarnToast('No panels matched past result — check your Panel CSV.'); dispatch({ type: 'SET_RESTORE_BUFFERS', payload: { _rPanels: null } }); } dispatch({ type: 'SET_PANEL_CSV', payload: parsed }); dispatch({ type: 'SET_SELECTED_PANELS', payload: newSelected }); dispatch({ type: 'SET_P1_COL_FILTERS', payload: {} }); dispatch({ type: 'SET_FILE', key: 'p', value: file }); } async function handleResultFile(file) { setLoadMsg('Reading past result…'); setLoading(true); const ab = await file.arrayBuffer(); setLoading(false); const result = parsePastXLSX(ab); if (!result) { showWarnToast('Failed to read past result file.'); return; } dispatch({ type: 'SET_FILE', key: 'r', value: file }); const buffers = {}; if (result.panels) buffers._rPanels = result.panels; if (result.vars) buffers._rVars = result.vars; if (result.contributions) buffers._rC = result.contributions; if (result.mapping) buffers._rMap = result.mapping; if (result.fixedVars) buffers._rFix = result.fixedVars; dispatch({ type: 'SET_RESTORE_BUFFERS', payload: buffers }); if (result.config) dispatch({ type: 'SET_CONFIG', payload: result.config }); if (result.synthPanelCSV) { dispatch({ type: 'SET_PANEL_CSV', payload: result.synthPanelCSV }); dispatch({ type: 'SET_SELECTED_PANELS', payload: new Set(result.synthPanelCSV.rows.map(r => String(r.__id))) }); dispatch({ type: 'SET_FILE', key: 'p', value: true }); dispatch({ type: 'SET_FROM_PAST_RESULT', payload: true }); } if (result.synthDictCSV) { dispatch({ type: 'SET_DICT_CSV', payload: result.synthDictCSV }); const token = result.synthDictCSV.rows.length + '|' + result.synthDictCSV.headers.join(','); dispatch({ type: 'SET_ALL_ROWS', payload: result.synthDictCSV.rows, token }); dispatch({ type: 'SET_FILE', key: 'd', value: true }); // _sel flags already applied in synthDictCSV rows — clear _rVars buffer to avoid double-apply dispatch({ type: 'SET_RESTORE_BUFFERS', payload: { _rVars: null } }); } const ok = !!(result.panels || result.vars); const hasBoth = !!(result.synthPanelCSV && result.synthDictCSV); setRestoreMsg({ ok, text: ok ? hasBoth ? '✓ All data restored — proceed directly or upload new files to override' : '✓ Pre-fill ready — upload Dictionary & Panel List CSVs to apply' : '⚠ Could not read past result' }); if (ok) showInfoToast('Past result loaded'); } function removeFile(id) { dispatch({ type: 'SET_FILE', key: id, value: null }); if (id === 'd') dispatch({ type: 'SET_DICT_CSV', payload: null }); if (id === 'p') { dispatch({ type: 'SET_PANEL_CSV', payload: null }); dispatch({ type: 'SET_SELECTED_PANELS', payload: new Set() }); dispatch({ type: 'SET_P1_COL_FILTERS', payload: {} }); } if (id === 'r') { setRestoreMsg(null); if (fromPastResult) { dispatch({ type: 'SET_FROM_PAST_RESULT', payload: false }); dispatch({ type: 'SET_PANEL_CSV', payload: null }); dispatch({ type: 'SET_DICT_CSV', payload: null }); dispatch({ type: 'SET_SELECTED_PANELS', payload: new Set() }); dispatch({ type: 'SET_P1_COL_FILTERS', payload: {} }); dispatch({ type: 'SET_FILE', key: 'd', value: null }); dispatch({ type: 'SET_FILE', key: 'p', value: null }); dispatch({ type: 'SET_RESTORE_BUFFERS', payload: { _rPanels: null, _rVars: null, _rC: null, _rMap: null, _rFix: null } }); } } } function togglePanel(k) { const next = new Set(selectedPanels); if (next.has(k)) next.delete(k); else next.add(k); dispatch({ type: 'SET_SELECTED_PANELS', payload: next }); } function selAll(on) { const next = new Set(selectedPanels); filteredRows.forEach(r => { const k = pKey(r); if (on) next.add(k); else next.delete(k); }); dispatch({ type: 'SET_SELECTED_PANELS', payload: next }); } function toggleColFilter(col, val, on) { const next = { ...p1ColFilters }; if (!next[col]) next[col] = new Set(); else next[col] = new Set(next[col]); if (on) next[col].add(val); else next[col].delete(val); if (!next[col].size) delete next[col]; dispatch({ type: 'SET_P1_COL_FILTERS', payload: next }); } function clearFilters() { dispatch({ type: 'SET_P1_COL_FILTERS', payload: {} }); setPanelSearch(''); } const canProceed = files.d && files.p && selectedPanels.size > 0; const hasFilters = Object.values(p1ColFilters).some(v => v && v.size > 0) || !!panelSearch; const headers = panelCSV?.headers || []; const allVisChecked = filteredRows.length > 0 && filteredRows.every(r => selectedPanels.has(pKey(r))); const anyVisChecked = filteredRows.some(r => selectedPanels.has(pKey(r))); return (
{loading && } {ddCol && ( toggleColFilter(ddCol, v, on)} onClose={() => { setDdCol(null); setDdAnchor(null); }} anchorRect={ddAnchor} /> )}
{/* LEFT: uploads */}
removeFile('d')} /> removeFile('p')} /> removeFile('r')}> {restoreMsg && (
{restoreMsg.text}
)}
{/* RIGHT: panel selection */}
Panel Selection
setPanelSearch(e.target.value)} style={{ paddingLeft: 22, width: 160, fontSize: 11 }} />
{hasFilters && ( )}
{selectedPanels.size} selected  ·  {panelCSV ? panelCSV.rows.length : 0} total 0 ? 'tag-g' : 'tag-gy'}`}> {selectedPanels.size > 0 ? `${selectedPanels.size} ready` : 'Select ≥ 1'}
{!panelCSV ? (
Upload a Panel List CSV to see rows here
) : ( {headers.map(h => ( ))} {filteredRows.map(row => { const k = pKey(row); return ( togglePanel(k)}> {headers.map(h => )} ); })}
{ if (el) el.indeterminate = !allVisChecked && anyVisChecked; }} onChange={e => selAll(e.target.checked)} />
{ setDdCol(h); setDdAnchor(e.currentTarget.getBoundingClientRect()); }} > {h} {p1ColFilters[h]?.size > 0 && ( {p1ColFilters[h].size} )}
togglePanel(k)} onClick={e => e.stopPropagation()} />{row[h]}
)}
{!files.d || !files.p ? 'Upload required files to continue' : !selectedPanels.size ? 'Select at least 1 panel' : <>{selectedPanels.size} panel(s) selected — ready}
); }