Spaces:
Sleeping
Sleeping
| import { useState, useMemo, useEffect } from 'react'; | |
| import { useAppState, useAppDispatch, useSelPanelRows, useSelVars } from '../context/AppContext.jsx'; | |
| import Header from '../components/Header.jsx'; | |
| import { showWarnToast } from '../components/Toast.jsx'; | |
| export default function Step4Mapping({ onNext, onBack, onNav }) { | |
| const state = useAppState(); | |
| const dispatch = useAppDispatch(); | |
| const selPanelRows = useSelPanelRows(); | |
| const selVars = useSelVars(); | |
| const { panelMap, panelCSV, _rMap } = state; | |
| const [panelQ, setPanelQ] = useState(''); | |
| const [varQ, setVarQ] = useState(''); | |
| const labelCol = panelCSV?.headers[0] || 'Panel'; | |
| const pHeaders = panelCSV?.headers || []; | |
| // Restore from past result | |
| useEffect(() => { | |
| if (!_rMap || !_rMap.length) return; | |
| const newMap = { ...panelMap }; | |
| selPanelRows.forEach(pr => { | |
| const pk = String(pr.__id); | |
| if (!newMap[pk]) newMap[pk] = {}; | |
| const matched = _rMap.find(rec => | |
| Object.entries(rec.panelData).some(([col, val]) => { | |
| if (!val) return false; | |
| const rowKey = Object.keys(pr).find(rk => rk.toLowerCase() === col.toLowerCase()) || col; | |
| return (pr[rowKey] || '').trim().toLowerCase() === val.toLowerCase(); | |
| }) | |
| ); | |
| if (matched) { | |
| selVars.forEach(v => { newMap[pk][v] = true; }); | |
| matched.removedVars.forEach(v => { newMap[pk][v] = false; }); | |
| } else { | |
| selVars.forEach(v => { if (newMap[pk][v] === undefined) newMap[pk][v] = true; }); | |
| } | |
| }); | |
| dispatch({ type: 'SET_PANEL_MAP', payload: newMap }); | |
| dispatch({ type: 'SET_RESTORE_BUFFERS', payload: { _rMap: null } }); | |
| }, []); | |
| // Init panelMap for any missing panels/vars | |
| useEffect(() => { | |
| const newMap = { ...panelMap }; | |
| let changed = false; | |
| selPanelRows.forEach(pr => { | |
| const pk = String(pr.__id); | |
| if (!newMap[pk]) { newMap[pk] = {}; changed = true; } | |
| selVars.forEach(v => { | |
| if (newMap[pk][v] === undefined) { newMap[pk][v] = true; changed = true; } | |
| }); | |
| }); | |
| if (changed) dispatch({ type: 'SET_PANEL_MAP', payload: newMap }); | |
| }, [selPanelRows.length, selVars.length]); | |
| const visRows = useMemo(() => | |
| selPanelRows.filter(pr => !panelQ || String(pr[labelCol] || String(pr.__id)).toLowerCase().includes(panelQ.toLowerCase())) | |
| .map(pr => ({ ...pr, key: String(pr.__id) })) | |
| , [selPanelRows, panelQ, labelCol]); | |
| const visCols = useMemo(() => | |
| selVars.filter(v => !varQ || v.toLowerCase().includes(varQ.toLowerCase())) | |
| , [selVars, varQ]); | |
| function setCell(pk, varName, val) { | |
| const newMap = { ...panelMap, [pk]: { ...(panelMap[pk] || {}), [varName]: val } }; | |
| dispatch({ type: 'SET_PANEL_MAP', payload: newMap }); | |
| } | |
| function toggleRow(pk) { | |
| const cur = panelMap[pk] || {}; | |
| const allOn = visCols.every(v => cur[v] === true); | |
| const newPk = { ...cur }; | |
| visCols.forEach(v => { newPk[v] = !allOn; }); | |
| dispatch({ type: 'SET_PANEL_MAP', payload: { ...panelMap, [pk]: newPk } }); | |
| } | |
| function toggleCol(varName) { | |
| const allOn = visRows.every(r => (panelMap[r.key] || {})[varName] === true); | |
| const newMap = { ...panelMap }; | |
| visRows.forEach(r => { | |
| newMap[r.key] = { ...(newMap[r.key] || {}), [varName]: !allOn }; | |
| }); | |
| dispatch({ type: 'SET_PANEL_MAP', payload: newMap }); | |
| } | |
| function clearAll() { | |
| const newMap = { ...panelMap }; | |
| selPanelRows.forEach(pr => { | |
| const pk = String(pr.__id); | |
| newMap[pk] = {}; | |
| selVars.forEach(v => { newMap[pk][v] = false; }); | |
| }); | |
| dispatch({ type: 'SET_PANEL_MAP', payload: newMap }); | |
| } | |
| const mappedCount = useMemo(() => { | |
| let n = 0; | |
| Object.values(panelMap).forEach(row => Object.values(row).forEach(v => { if (v === true) n++; })); | |
| return n; | |
| }, [panelMap]); | |
| const anyMapped = mappedCount > 0; | |
| return ( | |
| <div className="page"> | |
| <Header step={4} onNav={onNav} sub="Panel Mapping" /> | |
| <div className="toolbar"> | |
| <div style={{ position: 'relative', display: 'flex', alignItems: 'center', gap: 5 }}> | |
| <span style={{ fontSize: 9, fontWeight: 700, color: 'var(--mt)', fontFamily: 'var(--fm)', whiteSpace: 'nowrap' }}>Panels:</span> | |
| <div style={{ position: 'relative' }}> | |
| <svg style={{ position: 'absolute', left: 7, top: '50%', transform: 'translateY(-50%)', width: 10, height: 10, color: 'var(--mt)', pointerEvents: 'none' }} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="6.5" cy="6.5" r="4.5"/><path d="M10.5 10.5l3 3"/></svg> | |
| <input className="inp" value={panelQ} onChange={e => setPanelQ(e.target.value)} placeholder="Search panels…" style={{ paddingLeft: 22, width: 150, fontSize: 11 }} /> | |
| </div> | |
| </div> | |
| <div style={{ flex: 1 }} /> | |
| <div style={{ position: 'relative', display: 'flex', alignItems: 'center', gap: 5 }}> | |
| <span style={{ fontSize: 9, fontWeight: 700, color: 'var(--mt)', fontFamily: 'var(--fm)', whiteSpace: 'nowrap' }}>Variables:</span> | |
| <div style={{ position: 'relative' }}> | |
| <svg style={{ position: 'absolute', left: 7, top: '50%', transform: 'translateY(-50%)', width: 10, height: 10, color: 'var(--mt)', pointerEvents: 'none' }} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="6.5" cy="6.5" r="4.5"/><path d="M10.5 10.5l3 3"/></svg> | |
| <input className="inp" value={varQ} onChange={e => setVarQ(e.target.value)} placeholder="Search variables…" style={{ paddingLeft: 22, width: 150, fontSize: 11 }} /> | |
| </div> | |
| </div> | |
| <span className="page-note">Mapped: <strong style={{ color: 'var(--g)' }}>{mappedCount}</strong></span> | |
| <button className="btn btn-sm btn-sec" onClick={clearAll}>Clear All</button> | |
| </div> | |
| <div className="mx-body"> | |
| {!visCols.length || !visRows.length ? ( | |
| <div style={{ padding: 36, textAlign: 'center', fontSize: 11.5, color: 'var(--mt)' }}> | |
| {!selVars.length ? 'No variables selected — go back to Page 2.' | |
| : !selPanelRows.length ? 'No panels selected — go back to Page 1.' | |
| : 'No results match filters.'} | |
| </div> | |
| ) : ( | |
| <table className="m-tbl" style={{ tableLayout: 'fixed' }}> | |
| <thead> | |
| <tr> | |
| {pHeaders.map(h => ( | |
| <th key={h} className="pcol"> | |
| <div className="pth-inner"><span className="pth-lbl">{h}</span></div> | |
| </th> | |
| ))} | |
| <th style={{ minWidth: 80, background: 'var(--bgs)', padding: 0, position: 'sticky', top: 0, zIndex: 4 }}> | |
| <div className="row-hdr"><span style={{ fontSize: 8, color: 'var(--mt)', fontFamily: 'var(--fm)' }}>Toggle</span></div> | |
| </th> | |
| {visCols.map(varName => { | |
| const allOn = visRows.every(r => (panelMap[r.key] || {})[varName] === true); | |
| return ( | |
| <th key={varName}> | |
| <div className="col-hdr"> | |
| <span className="col-hdr-nm" title={varName}>{varName}</span> | |
| <button className={`col-tog${allOn ? ' on' : ''}`} onClick={() => toggleCol(varName)}> | |
| {allOn ? 'Deselect' : 'Select all'} | |
| </button> | |
| </div> | |
| </th> | |
| ); | |
| })} | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {visRows.map(row => { | |
| const pk = row.key; | |
| const cur = panelMap[pk] || {}; | |
| const rowAllOn = visCols.every(v => cur[v] === true); | |
| return ( | |
| <tr key={pk} className={rowAllOn ? 'row-full' : ''}> | |
| {pHeaders.map(h => <td key={h} className="pdc" title={row[h]}>{row[h]}</td>)} | |
| <td style={{ padding: '3px 6px', textAlign: 'center', background: '#fff', border: '1px solid var(--bl)' }}> | |
| <button className={`row-tog${rowAllOn ? ' on' : ''}`} onClick={() => toggleRow(pk)}> | |
| {rowAllOn ? 'Deselect' : 'All vars'} | |
| </button> | |
| </td> | |
| {visCols.map(varName => { | |
| const checked = cur[varName] === true; | |
| return ( | |
| <td key={varName}> | |
| <div className="m-cell" onClick={() => setCell(pk, varName, !checked)}> | |
| <input type="checkbox" className="m-cb" checked={checked} onChange={e => setCell(pk, varName, e.target.checked)} onClick={e => e.stopPropagation()} /> | |
| </div> | |
| </td> | |
| ); | |
| })} | |
| </tr> | |
| ); | |
| })} | |
| </tbody> | |
| </table> | |
| )} | |
| </div> | |
| <div className="nav-foot"> | |
| <button className="btn btn-sec" onClick={onBack}><svg width="9" height="9" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2.3" strokeLinecap="round"><path d="M10 6H2M5 2L1 6l4 4"/></svg> Back</button> | |
| <button className="btn btn-pri" disabled={!anyMapped} | |
| onClick={() => { if (!anyMapped) { showWarnToast('Map at least 1 variable first.'); } else onNext(); }}> | |
| Proceed to Fixed Variables | |
| <svg width="10" height="10" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="2.3" strokeLinecap="round"><path d="M2 7h10M8 3l4 4-4 4"/></svg> | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| } | |