mmm-modeler_app / src /pages /Step7Config.jsx
aashish-bindal's picture
Initial commit: Dabur MMM Modeler React app
425a907
import { useState } from 'react';
import { useAppState, useAppDispatch } from '../context/AppContext.jsx';
import Header from '../components/Header.jsx';
import { DEFAULT_CONFIG } from '../utils/constants.js';
import { buildAndDownload } from '../utils/excelExport.js';
import { showInfoToast, showWarnToast } from '../components/Toast.jsx';
function getByPath(obj, path) {
if (!path) return obj;
return path.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : undefined), obj);
}
function setByPath(obj, path, value) {
if (!path) return value;
const keys = path.split('.');
const root = JSON.parse(JSON.stringify(obj));
let cur = root;
for (let i = 0; i < keys.length - 1; i++) { cur = cur[keys[i]]; }
cur[keys[keys.length - 1]] = value;
return root;
}
function ConfigNode({ obj, path, depth }) {
const dispatch = useAppDispatch();
const state = useAppState();
const [collapsed, setCollapsed] = useState(depth > 1);
function update(p, val) {
const newConfig = setByPath(state.config, p, val);
dispatch({ type: 'SET_CONFIG', payload: newConfig });
}
if (Array.isArray(obj)) {
return (
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 6, padding: '2px 0', minHeight: 26 }}>
<input
type="text"
className="cfg-inp cfg-inp-arr"
defaultValue={obj.map(v => JSON.stringify(v)).join(', ')}
title="Edit comma-separated values"
onBlur={e => {
try {
const raw = e.target.value;
const parsed = raw.split(',').map(s => {
const t = s.trim();
try { return JSON.parse(t); } catch { return t; }
}).filter(v => v !== '');
update(path, parsed);
} catch (err) { showWarnToast('Invalid array format'); }
}}
/>
<span className="cfg-type-tag">array[{obj.length}]</span>
</div>
);
}
if (obj !== null && typeof obj === 'object') {
const keys = Object.keys(obj);
return (
<div className="cfg-node">
{depth > 0 && (
<div
className="cfg-obj-label"
style={{ paddingBottom: 2 }}
onClick={() => setCollapsed(!collapsed)}
>
<span style={{ fontSize: 9, color: 'var(--mt)', marginRight: 2, userSelect: 'none' }}>
{collapsed ? '▶' : '▼'}
</span>
<span style={{ color: 'var(--mt)' }}>{'{' + keys.length + ' keys}'}</span>
</div>
)}
{!collapsed && (
<div className={depth > 0 ? 'cfg-children' : ''}>
{keys.map(key => {
const childPath = path ? `${path}.${key}` : key;
const childVal = obj[key];
return (
<div key={key} className="cfg-node">
<div className="cfg-row">
<span className="cfg-key" style={{ marginTop: 4 }}>{key}</span>
<span className="cfg-sep" style={{ marginTop: 4 }}>:</span>
<ConfigNode obj={childVal} path={childPath} depth={depth + 1} />
</div>
</div>
);
})}
</div>
)}
</div>
);
}
// Primitive leaf
const isNum = typeof obj === 'number';
const isBool = typeof obj === 'boolean';
if (isBool) {
return (
<select
className="cfg-inp"
defaultValue={String(obj)}
onChange={e => update(path, e.target.value === 'true')}
style={{ width: 80 }}
>
<option value="true">true</option>
<option value="false">false</option>
</select>
);
}
return (
<input
type={isNum ? 'number' : 'text'}
className={`cfg-inp${isNum ? ' cfg-inp-num' : ''}`}
defaultValue={obj}
onBlur={e => {
const raw = e.target.value;
update(path, isNum ? (isNaN(Number(raw)) ? raw : Number(raw)) : raw);
}}
/>
);
}
export default function Step7Config({ onBack, onNav }) {
const state = useAppState();
const dispatch = useAppDispatch();
const { config } = state;
function resetConfig() {
if (!window.confirm('Reset all config values to defaults?')) return;
dispatch({ type: 'SET_CONFIG', payload: JSON.parse(JSON.stringify(DEFAULT_CONFIG)) });
}
function download() {
const ok = buildAndDownload(state);
if (ok) showInfoToast('Download complete — Dabur_Model_Output.xlsx');
}
return (
<div className="page">
<Header step={7} onNav={onNav} sub="Config Editor" />
<div className="toolbar">
<span style={{ fontSize: 11, fontWeight: 600, color: 'var(--tx)' }}>Model Configuration</span>
<span className="page-note" style={{ marginLeft: 4 }}>Edit values inline · Click ▶ to expand objects</span>
<div style={{ flex: 1 }} />
<button className="btn btn-sm btn-sec" onClick={resetConfig}>Reset to Defaults</button>
</div>
<div style={{ flex: 1, overflow: 'auto', padding: '10px 14px', minHeight: 0 }}>
<ConfigNode obj={config} path="" depth={0} />
</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-or" onClick={download}>
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M8 2v8M5 7l3 3 3-3"/><path d="M2 12v1a1 1 0 001 1h10a1 1 0 001-1v-1"/></svg>
Full Excel Output
</button>
</div>
</div>
);
}