import { useState, useMemo, useEffect } from 'react'; import { useAppState, useAppDispatch } from '../context/AppContext.jsx'; import ColFilterDropdown from '../components/ColFilterDropdown.jsx'; import { DISP_COLS, DICT_COLS, TOOLTIP_COLS, TOOLTIP_LABELS, STEP_LABELS } from '../utils/constants.js'; const CheckSvg = () => ( ); function DictTooltip({ row }) { if (!row) return null; return (
{TOOLTIP_COLS.map(col => { const val = (row[col] || '').trim(); if (!val) return null; return (
{TOOLTIP_LABELS[col] || col} {val}
); })}
); } export default function Step2Variables({ onNext, onBack, onNav }) { const state = useAppState(); const dispatch = useAppDispatch(); const { allRows, dictCSV, _dictToken, _rVars } = state; const [search, setSearch] = useState(''); const [colFilters, setColFilters] = useState({}); const [ddCol, setDdCol] = useState(null); const [ddAnchor, setDdAnchor] = useState(null); const [tooltip, setTooltip] = useState({ visible: false, x: 0, y: 0, content: null }); const [summaryWidth, setSummaryWidth] = useState(280); const [resizing, setResizing] = useState(false); // Build/rebuild allRows when entering this step useEffect(() => { if (!dictCSV) return; const token = dictCSV.rows.length + '|' + (dictCSV.headers || []).join(','); if (allRows.length && _dictToken === token) return; // preserve selections const rows = dictCSV.rows.map((r, i) => { const o = { __id: i + 1 }; DICT_COLS.forEach(c => { o[c] = (r[c] || '').trim(); }); o._sel = _rVars ? _rVars.has((r.VARIABLE || '').trim()) : (r.PRIMARY_METRICS || '').toUpperCase() === 'Y'; return o; }); dispatch({ type: 'SET_ALL_ROWS', payload: rows, token }); if (_rVars) dispatch({ type: 'SET_RESTORE_BUFFERS', payload: { _rVars: null } }); }, [dictCSV]); function rowPasses(row) { for (const [col, vals] of Object.entries(colFilters)) { if (vals && vals.size && !vals.has(row[col])) return false; } if (search) { const q = search.toLowerCase(); if (!DICT_COLS.some(c => (row[c] || '').toLowerCase().includes(q))) return false; } return true; } const visible = useMemo(() => allRows.filter(rowPasses), [allRows, colFilters, search]); function toggleSel(id) { dispatch({ type: 'TOGGLE_VAR_SEL', id }); } function selectAll(on) { const ids = new Set(visible.map(r => r.__id)); dispatch({ type: 'SET_ALL_VARS_SEL', ids, on }); } function toggleColFilter(col, val, on) { const next = { ...colFilters }; 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]; setColFilters(next); } function clearFilters() { setColFilters({}); setSearch(''); } // Summary — group by activity const { selRows, allActs, groupSel, totalPerAct } = useMemo(() => { const selRows = allRows.filter(r => r._sel); const groupSel = {}; selRows.forEach(r => { const a = r.SN_ACTIVITY_LEVEL_1 || '(Unassigned)'; if (!groupSel[a]) groupSel[a] = []; groupSel[a].push(r.VARIABLE); }); const totalPerAct = {}; allRows.forEach(r => { const a = r.SN_ACTIVITY_LEVEL_1 || '(Unassigned)'; totalPerAct[a] = (totalPerAct[a] || 0) + 1; }); const allActs = [...new Set(allRows.map(r => r.SN_ACTIVITY_LEVEL_1 || '(Unassigned)'))]; return { selRows, allActs, groupSel, totalPerAct }; }, [allRows]); const selCount = allRows.filter(r => r._sel).length; const hasFilters = Object.values(colFilters).some(v => v && v.size > 0) || !!search; // Tooltip handlers function showTT(e, row) { const x = Math.min(e.clientX + 11, window.innerWidth - 320); const y = Math.min(e.clientY + 14, window.innerHeight - 200); setTooltip({ visible: true, x, y, content: }); } function moveTT(e) { const x = Math.min(e.clientX + 11, window.innerWidth - 320); const y = Math.min(e.clientY + 14, window.innerHeight - 200); setTooltip(t => ({ ...t, x, y })); } function hideTT() { setTooltip(t => ({ ...t, visible: false })); } // Resize summary panel function startResize(e) { e.preventDefault(); const startX = e.clientX, startW = summaryWidth; function onMove(ev) { setSummaryWidth(Math.max(180, Math.min(520, startW + (startX - ev.clientX)))); } function onUp() { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); } document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); } // col values for dropdown function colVals(col) { return [...new Set(allRows.map(r => r[col]).filter(v => v))].sort(); } const zeroSel = allActs.filter(a => !groupSel[a]).sort(); const hasSel = allActs.filter(a => groupSel[a]?.length > 0).sort(); return (
{tooltip.visible && (
{tooltip.content}
)} {ddCol && ( toggleColFilter(ddCol, v, on)} onClose={() => { setDdCol(null); setDdAnchor(null); }} anchorRect={ddAnchor} /> )}
Variable Selection
Check variables to include — PRIMARY_METRICS = Y
{STEP_LABELS.map((label, i) => { const n = i + 1; const isDone = n < 2; const isActive = n === 2; return (
isDone && onNav(n)} >
{isDone ? : n}
{label}
{n < STEP_LABELS.length &&
} ); })}
{/* LEFT */}
All Variables ({allRows.length})
setSearch(e.target.value)} />
{Object.entries(colFilters).flatMap(([col, vals]) => [...vals].map(v => ( {col.replace(/^SN_/,'').replace(/_/g,' ')}: {v} )) )} {hasFilters && }
{DISP_COLS.map(col => ( ))} {visible.length === 0 ? ( ) : visible.map(row => ( toggleSel(row.__id)}> {DISP_COLS.map(col => ( ))} ))}
{ setDdCol(col); setDdAnchor(e.currentTarget.getBoundingClientRect()); }} > {col.replace(/^SN_/,'').replace(/_/g,' ')}
No results
Try clearing filters
toggleSel(row.__id)} onClick={e => e.stopPropagation()} /> showTT(e, row)) : undefined} onMouseMove={col === 'VARIABLE' ? moveTT : undefined} onMouseLeave={col === 'VARIABLE' ? hideTT : undefined} > {row[col] || ''}
{/* RESIZE HANDLE + SUMMARY */}
Variable Summary
{selCount} selected {allRows.length - selCount} not selected
{!allRows.length ?
No variables loaded
: [...zeroSel, ...hasSel].map(act => { const selVars = groupSel[act] || []; const total = totalPerAct[act] || 0; const isZero = selVars.length === 0; return (
{act} ({selVars.length}/{total}) {isZero && ' ⚠️'}
{selVars.length ? selVars.map(v => (
{v}
)) : (
No variables selected
)}
); })}
); }