/* ============================================================
Shared UI components + icon set
============================================================ */
const { useState, useRef, useEffect, useLayoutEffect } = React;
/* ---- Icon set (simple line icons) ---- */
const ICONS = {
dashboard: 'M3 3h7v8H3zM14 3h7v5h-7zM14 11h7v10h-7zM3 14h7v7H3z',
drivers: 'M16 18a4 4 0 0 0-8 0 M12 11a3.2 3.2 0 1 0 0-6.4 3.2 3.2 0 0 0 0 6.4',
schedule: 'M4 5h16v15H4zM4 9h16M8 3v4M16 3v4M8 13h2M12 13h2M8 16h2',
settings: 'M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6 M19 12a7 7 0 0 0-.1-1.2l2-1.6-2-3.4-2.4 1a7 7 0 0 0-2-1.2L16 1H8l-.5 2.6a7 7 0 0 0-2 1.2l-2.4-1-2 3.4 2 1.6A7 7 0 0 0 3 12a7 7 0 0 0 .1 1.2l-2 1.6 2 3.4 2.4-1a7 7 0 0 0 2 1.2L8 23h8l.5-2.6a7 7 0 0 0 2-1.2l2.4 1 2-3.4-2-1.6A7 7 0 0 0 19 12z',
board: 'M3 4h6v16H3zM10 4h6v16h-6zM17 4h4v16h-4z',
arrow: 'M5 12h14M13 6l6 6-6 6',
calendar: 'M4 5h16v15H4zM4 9h16M8 3v4M16 3v4',
play: 'M7 5l12 7-12 7z',
pause: 'M7 5h4v14H7zM13 5h4v14h-4z',
plus: 'M12 5v14M5 12h14',
edit: 'M4 20h4l11-11-4-4L4 16zM14 5l4 4',
trash: 'M4 7h16M9 7V4h6v3M6 7l1 14h10l1-14',
download: 'M12 3v12M7 11l5 5 5-5M4 21h16',
refresh: 'M21 12a9 9 0 1 1-3-6.7M21 4v4h-4',
check: 'M5 12l5 5 9-11',
x: 'M6 6l12 12M18 6L6 18',
menu: 'M4 6h16M4 12h16M4 18h16',
sun: 'M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8M12 2v2M12 20v2M4 12H2M22 12h-2M5 5l1.5 1.5M17.5 17.5L19 19M19 5l-1.5 1.5M6.5 17.5L5 19',
moon: 'M21 13a8 8 0 1 1-9.5-9.5 6.5 6.5 0 0 0 9.5 9.5z',
truck: 'M2 6h11v9H2zM13 9h4l4 3v3h-8zM7.5 18.5a1.6 1.6 0 1 0 0-3.2 1.6 1.6 0 0 0 0 3.2M17.5 18.5a1.6 1.6 0 1 0 0-3.2 1.6 1.6 0 0 0 0 3.2',
pin: 'M12 21s-7-6.2-7-11a7 7 0 0 1 14 0c0 4.8-7 11-7 11zM12 13a3 3 0 1 0 0-6 3 3 0 0 0 0 6',
bolt: 'M13 2L4 14h6l-1 8 9-12h-6z',
phone: 'M5 4h4l2 5-3 2a12 12 0 0 0 5 5l2-3 5 2v4a2 2 0 0 1-2 2A18 18 0 0 1 3 6a2 2 0 0 1 2-2z',
clock: 'M12 7v5l3 2M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18',
box: 'M3 7l9-4 9 4v10l-9 4-9-4zM3 7l9 4 9-4M12 11v10',
chart: 'M4 20V10M10 20V4M16 20v-7M22 20H2',
flag: 'M5 21V4M5 4h11l-2 4 2 4H5',
collapse: 'M15 6l-6 6 6 6',
filter: 'M3 5h18l-7 8v6l-4-2v-4z',
tasks: 'M4 6l1.6 1.6L8 4M4 12l1.6 1.6L8 10M4 18l1.6 1.6L8 16M11 5h9M11 11h9M11 17h9',
};
function Icon({ name, style }) {
const d = ICONS[name] || '';
const paths = d.split(' M').map((seg, i) => (i === 0 ? seg : 'M' + seg));
return (
);
}
/* ---- Status helpers ---- */
const STATUS_META = {
scheduled: { label: 'Scheduled', cls: 'b-gray' },
in_transit: { label: 'In Transit', cls: 'b-accent' },
completed: { label: 'Completed', cls: 'b-green' },
delayed: { label: 'Delayed', cls: 'b-amber' },
off: { label: 'Off Duty', cls: 'b-gray' },
};
function StatusBadge({ status }) {
const m = STATUS_META[status] || STATUS_META.scheduled;
return {m.label};
}
function Route({ from, to }) {
return (
{from}
{to}
);
}
/* ---- Sidebar ---- */
function Sidebar({ view, setView, counts, collapsed, simulating, toggleSim, mobileOpen, closeMobile }) {
const NAV = [
{ id: 'dashboard', label: 'Dashboard', icon: 'dashboard' },
{ id: 'drivers', label: 'Drivers', icon: 'drivers', count: counts.drivers },
{ id: 'trucks', label: 'Trucks', icon: 'truck', count: counts.trucks },
{ id: 'schedule', label: 'Schedule', icon: 'schedule' },
{ id: 'board', label: 'Dispatch Board', icon: 'board' },
{ id: 'dispatcher', label: "Dispatcher's Work", icon: 'tasks' },
];
return (
);
}
/* ---- NC (US Eastern) live clock ---- */
function NCClock() {
const [now, setNow] = useState(Engine.nowET());
useEffect(() => {
const id = setInterval(() => setNow(Engine.nowET()), 1000);
return () => clearInterval(id);
}, []);
const today = Engine.fmtLong(Engine.todayET());
return (
);
}
/* ---- Topbar ---- */
const PAGE_META = {
dashboard: { title: 'Dispatch Overview', sub: 'Weekly operations at a glance' },
drivers: { title: 'Driver Management', sub: 'Roster, assignments & duty status' },
trucks: { title: 'Truck Management', sub: 'Fleet & utilization tracking' },
schedule: { title: 'Schedule Viewer', sub: 'Per-driver weekly cycle' },
board: { title: 'Dispatch Board', sub: 'Live operations grid' },
dispatcher:{ title: "Dispatcher's Work", sub: 'Daily task checklist' },
settings: { title: 'Settings & Admin', sub: 'Week start, fleet & overrides' },
};
function Topbar({ view, weekStart, theme, toggleTheme, simulating, simDate, onMenu }) {
const m = PAGE_META[view] || PAGE_META.dashboard;
return (
);
}
/* ---- KPI card ---- */
function Kpi({ label, value, icon, tone, foot }) {
return (
);
}
/* ---- Modal shell ---- */
function Modal({ title, onClose, children, footer }) {
useEffect(() => {
const h = e => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', h);
return () => window.removeEventListener('keydown', h);
}, []);
return (
e.stopPropagation()}>
{title}
{children}
{footer &&
{footer}
}
);
}
/* ---- Toast ---- */
function Toast({ msg }) {
if (!msg) return null;
return {msg}
;
}
/* ---- Login gate ---- */
function LoginGate({ expected, onSuccess }) {
const [pw, setPw] = useState('');
const [err, setErr] = useState(false);
const [show, setShow] = useState(false);
const inputRef = useRef(null);
useEffect(() => { inputRef.current && inputRef.current.focus(); }, []);
const submit = (e) => {
e.preventDefault();
if (pw === expected) {
try { sessionStorage.setItem('ejay_auth', '1'); } catch (_) {}
onSuccess();
} else {
setErr(true);
setPw('');
}
};
return (
);
}
Object.assign(window, {
Icon, ICONS, StatusBadge, STATUS_META, Route,
Sidebar, Topbar, Kpi, Modal, Toast, LoginGate, NCClock,
useState, useRef, useEffect, useLayoutEffect,
});