Spaces:
Sleeping
Sleeping
| 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> | |
| ); | |
| } | |