/* ===== Containerization module — the flagship screen ===== */ const ContainerizationScreen = ({ onOpenExcel, showStopOption = true }) => { const [container, setContainer] = React.useState(window.Mojro.CONTAINERS[2]); // 40HC // Fleet — what containers are available, and how many of each. // The currently-selected `container` above is the one being visualized. const [fleet, setFleet] = React.useState([ { id: '40hc', qty: 1 }, { id: '40ft', qty: 2 }, { id: '20ft', qty: 1 }, ]); const [addOpen, setAddOpen] = React.useState(false); const [cargo, setCargo] = React.useState(window.Mojro.SEED_CARGO); const [selectedItemId, setSelectedItemId] = React.useState(null); const [anchor, setAnchor] = React.useState(null); const [colorMode, setColorMode] = React.useState('item'); const [viewMode, setViewMode] = React.useState('3d'); const [xray, setXray] = React.useState(false); const [layerSlice, setLayerSlice] = React.useState(1.0); const [showAxleOverlay, setShowAxleOverlay] = React.useState(true); const [violations] = React.useState(window.Mojro.SEED_VIOLATIONS); const [activeTab, setActiveTab] = React.useState('manifest'); // manifest | special | constraints const [showPatterns, setShowPatterns] = React.useState(false); // If Tweaks hides the 'stop' colour mode while it's selected, fall back. React.useEffect(() => { if (!showStopOption && colorMode === 'stop') setColorMode('item'); }, [showStopOption, colorMode]); // tag violations onto items const cargoTagged = React.useMemo(() => { return cargo.map(c => { const v = violations.find(v => v.item === c.id); return v ? { ...c, violation: v.severity } : c; }); }, [cargo, violations]); const selectedItem = cargoTagged.find(c => c.id === selectedItemId); // demo KPIs / axles const kpis = { volumePct: 87, deadAir: 13, weight: 24.8, weightMax: 28.6, items: 76, skus: 8, containerCount: 1, cogX: 0.46, cogY: 0.52 }; const axles = [ { name: 'Steer', load: 5.4, limit: 7.5 }, { name: 'Drive', load: 10.6, limit: 11.5 }, { name: 'Tandem', load: 8.8, limit: 9.5 }, ]; const handleSelectFromScene = (id, pos) => { setSelectedItemId(id); setAnchor(pos); }; return (
{/* Header bar */}
Containerization
New plan · {container.name}
autosaved · 12:42 m / kg
{/* LEFT — Cargo manifest / inputs */}
{/* Container picker */}
Container / vehicle {fleet.reduce((s, r) => s + r.qty, 0)} available · {fleet.length} type{fleet.length === 1 ? '' : 's'}
{/* Selected container — visualised in the canvas */}
{container.name} {container.id.toUpperCase()} viewing
{/* Fleet — rows of {type, qty} */}
Fleet
{fleet.map(row => { const c = window.Mojro.CONTAINERS.find(x => x.id === row.id); if (!c) return null; const isViewing = c.id === container.id; return ( setContainer(c)} onQty={(next) => setFleet(f => f.map(r => r.id === row.id ? { ...r, qty: Math.max(0, next) } : r))} onRemove={() => { setFleet(f => { const nf = f.filter(r => r.id !== row.id); if (isViewing && nf.length) { const nc = window.Mojro.CONTAINERS.find(x => x.id === nf[0].id); if (nc) setContainer(nc); } return nf; }); }} /> ); })} {/* Add container — dropdown of types not yet in fleet */}
{addOpen && (
{window.Mojro.CONTAINERS .filter(c => !fleet.find(r => r.id === c.id)) .map(c => ( ))} {window.Mojro.CONTAINERS.filter(c => !fleet.find(r => r.id === c.id)).length === 0 && (
All container types added.
)}
)}
{/* Tabs */}
{[ { id:'manifest', label:'Cargo', count: cargo.length }, { id:'special', label:'Special modes', count: 3 }, { id:'constraints', label:'Constraints', count: 7 }, ].map(t => ( ))}
{/* Tab content */}
{activeTab === 'manifest' && ( )} {activeTab === 'special' && } {activeTab === 'constraints' && }
{/* CENTER — 3D canvas */}
{/* canvas toolbar */} setShowPatterns(true)} showStopOption={showStopOption} /> {/* 3D area */}
{/* layer slice slider */}
setLayerSlice(parseFloat(e.target.value))} style={{ writingMode:'bt-lr', WebkitAppearance:'slider-vertical', width:8, flex:1, margin:'8px 0' }}/> {Math.round(layerSlice*100)}%
{/* Floating axle overlay (top-view) */} {showAxleOverlay && viewMode === 'top' && ( )} {/* Color legend */} {/* coords display */}
L drag · orbit | Shift+drag · pan | scroll · zoom | click · inspect
{selectedItem && ( { setSelectedItemId(null); setAnchor(null); }} onLock={() => setCargo(c => c.map(i => i.id===selectedItem.id ? {...i, locked: !i.locked} : i))} onRemove={() => { setCargo(c => c.filter(i => i.id !== selectedItem.id)); setSelectedItemId(null); }} /> )} {showPatterns && setShowPatterns(false)}/>}
{/* RIGHT — compliance panel */}
); }; /* -------- subcomponents -------- */ const Stat = ({ label, value }) => (
{label}
{value}
); const FleetRow = ({ container, qty, viewing, onView, onQty, onRemove }) => (
{/* Qty stepper */}
{qty}
); const CargoManifestPanel = ({ cargo, selectedId, onSelect, onOpenExcel }) => { const [filter, setFilter] = React.useState('all'); const filtered = filter === 'all' ? cargo : cargo.filter(c => c.tag.toLowerCase() === filter); return (
{['all','pallet','carton','sku','barrel','pipe'].map(f => ( ))}
{filtered.map(item => ( onSelect(item.id)}/> ))}
This manifest accepts any combination of pallets, cartons, SKUs and special cargo in one session.
); }; const CargoRow = ({ item, selected, onClick }) => { const cat = window.Mojro.CATEGORIES.find(c => c.id === item.category); return (
{ if(!selected) e.currentTarget.style.background='rgba(0,0,0,0.025)';}} onMouseLeave={e=>{ if(!selected) e.currentTarget.style.background='transparent';}} >
{item.sku} {item.locked && } {item.violation === 'bad' && } {item.violation === 'warn' && }
{item.desc}
×{item.qty} {cat?.name}
); }; const CanvasToolbar = ({ colorMode, onColorMode, viewMode, onViewMode, xray, onXray, showAxleOverlay, onShowAxleOverlay, onTogglePatterns, showStopOption = true }) => ( /* `showStopOption` (Tweaks) removes the “Stop” entry from the Colour-by segment. */
{/* view modes */} {['3d','top','side'].map(v => ( onViewMode(v)}>{v.toUpperCase()} ))} {/* color mode */} Colour by {[ ['item','SKU'], ['category','Category'], ['weight','Weight'], ...(showStopOption ? [['stop','Stop']] : []) ].map(([id,label]) => ( onColorMode(id)}>{label} ))} {/* feature toggles */} onXray(!xray)} /> onShowAxleOverlay(!showAxleOverlay)} />
{/* right side */}
); const SegBtn = ({ children }) => (
{children}
); const SegItem = ({ active, children, onClick }) => ( ); const ToggleBtn = ({ icon, label, active, onClick }) => ( ); const ColorLegend = ({ mode, cargo }) => { let items = []; if (mode === 'item') { items = cargo.slice(0, 6).map(c => ({ color: c.color, label: c.sku })); } else if (mode === 'category') { items = window.Mojro.CATEGORIES.map(c => ({ color: c.color, label: c.name })); } else if (mode === 'stop') { items = [ { color:'#C13333', label:'Stop 1 (rear)' }, { color:'#E8893C', label:'Stop 2' }, { color:'#F0B830', label:'Stop 3' }, ]; } else { // weight gradient return (
Weight contribution
light
heavy
); } return (
Legend · by {mode}
{items.map(i => (
{i.label}
))}
); }; const AxleOverlay = ({ container }) => (
{/* placeholder: in real product overlays anchor on top-view of container */}
Drive axle zone · 10.6t
); const SpecialModesPanel = () => (
); const SpecialCard = ({ title, icon, desc, modes, footer }) => (
{title}
{desc}
{modes.map(m => (
{m.label}
{m.desc}
{m.pieces}
))}
{footer}
); const ConstraintsPanel = () => (
); const ConstraintRow = ({ on, type, label, detail }) => (
{type}
{label}
{detail &&
{detail}
}
); const Toggle = ({ on }) => (
); // Loading pattern picker (modal-like floating card) const PatternPicker = ({ onClose }) => { const PATTERNS = [ { id:'straight', name:'Straight', diag: , util: 78 }, { id:'turned', name:'Turned 90°', diag: , util: 73 }, { id:'pinwheel', name:'Pinwheel', diag: , util: 87, best: true }, { id:'mixed', name:'Mixed', diag: , util: 84 }, ]; const [sel, setSel] = React.useState('pinwheel'); return (
Pallet loading pattern
EUR pallets · 18 in load · 40ft HC
{PATTERNS.map(p => (
setSel(p.id)} style={{ border:'1px solid ' + (sel===p.id ? 'var(--mojro-red)' : 'var(--line)'), borderRadius:8, padding:'8px 10px', background: sel===p.id ? 'rgba(193,51,51,0.04)' : '#fff', cursor:'pointer', position:'relative', }}> {p.best && Best}
{p.diag}
{p.name}
{p.util}% utilisation
))}
Pinwheel +9% density vs straight
); }; const PatternDiag = ({ pattern }) => { // 60x48 viewBox top-view const rect = (x,y,w,h,rot=0) => ; switch(pattern) { case 'straight': return ( {[0,1,2].map(c => [0,1].map(r => rect(2+c*20, 4+r*22, 16, 18)))} ); case 'turned': return ( {[0,1,2,3,4].map(c => rect(2+c*12, 12, 10, 24))} ); case 'pinwheel': return ( {/* alternating straight + turned */} {rect(2,4,16,18)}{rect(2,26,16,18)} {rect(20,4,10,40)} {rect(32,4,16,18)}{rect(32,26,16,18)} ); case 'mixed': return ( {rect(2,4,16,18)}{rect(2,26,16,18)} {rect(20,4,16,18)}{rect(20,26,16,18)} {rect(38,4,10,18)}{rect(50,4,10,18)} {rect(40,28,16,16)} ); } }; window.ContainerizationScreen = ContainerizationScreen;