/* ===== Rules configuration screen — matrix + constraint editor ===== */ const RulesScreen = ({ onOpenExcel }) => { const [matrix, setMatrix] = React.useState(window.Mojro.CAT_MATRIX); const [tab, setTab] = React.useState('matrix'); const cycleCell = (a, b) => { const cur = matrix[a][b]; const next = cur === 'allowed' ? 'warn' : cur === 'warn' ? 'blocked' : 'allowed'; setMatrix(m => ({ ...m, [a]: { ...m[a], [b]: next }, [b]: { ...m[b], [a]: next }, // symmetric })); }; return (
Rules & constraints

Loading rule library

Three rule levels: per-SKU rules, vehicle rules, and loading constraints. Save as named rule sets and apply to any session.
{[ { id:'matrix', label:'Category compatibility', count: 6 }, { id:'sku', label:'SKU rules', count: 14 }, { id:'vehicle', label:'Vehicle / container', count: 6 }, { id:'loading', label:'Loading constraints', count: 9 }, ].map(t => ( ))}
{tab === 'matrix' && } {tab === 'sku' && } {tab === 'vehicle' && } {tab === 'loading' && }
); }; const CategoryMatrix = ({ matrix, onCycleCell }) => { const cats = window.Mojro.CATEGORIES; const stateColor = { allowed: 'var(--ok-bg)', warn: 'var(--warn-bg)', blocked: 'var(--bad-bg)' }; const stateInk = { allowed: 'var(--ok)', warn: 'var(--warn)', blocked: 'var(--bad)' }; const stateLabel = { allowed: 'Allowed', warn: 'Warn', blocked: 'Blocked' }; return (

Category compatibility matrix

Click any cell to cycle AllowedWarnBlocked. Matrix is symmetric.
Allowed Warn Blocked
{cats.map(c => ( ))} {cats.map(a => ( {cats.map(b => { const s = matrix[a.id][b.id]; const isDiag = a.id === b.id; return ( ); })} ))}
{c.name}
{a.name}
Blocked pairs cause an optimization error. Warn pairs trigger a separation distance — set in Loading constraints → Segregation.
); }; const SkuRulesTab = () => { const ROWS = [ { sku:'PLT-EUR-C', desc:'Cleaning chem (palletised)', orient:'upright only', stk:'No top stack', cat:'Hazmat', pri:'Last on / first off' }, { sku:'BX-MED-04', desc:'Electronics carton', orient:'top-fixed', stk:'Max 12 kg on top', cat:'Fragile', pri:'Stop 3' }, { sku:'BR-200L', desc:'200 L drum lubricant', orient:'upright', stk:'No stack', cat:'Hazmat', pri:'Floor only' }, { sku:'PP-3M-DN50',desc:'Steel pipe 3 m', orient:'horizontal', stk:'Bundled', cat:'General', pri:'Side wall' }, { sku:'AP-1100', desc:'Apparel tee', orient:'any', stk:'Yes', cat:'General', pri:'Any' }, ]; return (

Per-SKU rules

Orientation, stackability, category, priority. Override per session if needed.
{ROWS.map(r => ( ))}
SKUDescriptionOrientationStackabilityCategoryPriority / stop
{r.sku} {r.desc} {r.orient} {r.stk} c.name===r.cat)?.color+'22', color: window.Mojro.CATEGORIES.find(c=>c.name===r.cat)?.color, }}>{r.cat} {r.pri}
); }; const VehicleRulesTab = () => (
{window.Mojro.CONTAINERS.map(c => (
{c.name}
{c.id.toUpperCase()}
Exclusion zones
wheel arch · L+R {c.id==='40hc' && reefer unit} door swing
))}
); const Stat2 = ({ label, value }) => (
{label}
{value}
); const LoadingRulesTab = () => { const RULES = [ { type:'Segregation', label:'Hazmat ↔ Food separation', detail:'600 mm minimum gap · enforced strictly', on:true }, { type:'Segregation', label:'Chilled ↔ Ambient buffer', detail:'200 mm thermal buffer · warn-only', on:true }, { type:'Weight', label:'Heaviest items on bottom', detail:'auto · within each pallet/container', on:true }, { type:'Weight', label:'CoG within 40–60% from front', detail:'rebalance if drifts', on:true }, { type:'Axle', label:'Drive axle ≤ 11.5 t', detail:'EU directive · 2015/719', on:true }, { type:'Sequence', label:'LIFO by stop', detail:'Stop 1 loaded last (rear-most)', on:true }, { type:'Sequence', label:'Priority zone for stop 1', detail:'reserve rear 30% length', on:false }, { type:'Overhang', label:'Pallet overhang ≤ 5%', detail:'all sides', on:true }, { type:'Floor', label:'Floor strength 4500 kg/m²', detail:'per axle zone', on:true }, ]; return (

Loading constraints

Segregation, weight distribution, sequence, overhang.
{RULES.map((r,i) => (
{r.type}
{r.label}
{r.detail}
))}
); }; window.RulesScreen = RulesScreen;