/* ===== 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 }) => (
);
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 (
);
}
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 }) => (
{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 ();
case 'turned':
return ();
case 'pinwheel':
return ();
case 'mixed':
return ();
}
};
window.ContainerizationScreen = ContainerizationScreen;