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
{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
) : (
)}
{!files.d || !files.p ? 'Upload required files to continue'
: !selectedPanels.size ? 'Select at least 1 panel'
: <>{selectedPanels.size} panel(s) selected — ready>}
);
}