mmm-modeler_app / src /pages /Step4Mapping.jsx
aashish-bindal's picture
Initial commit: Dabur MMM Modeler React app
425a907
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>
);
}