/* ============================================================
Views: Schedule Viewer, Dispatch Board, Settings
============================================================ */
/* =================== SCHEDULE VIEWER =================== */
function ScheduleView({ ctx }) {
const activeDrivers = ctx.drivers.filter(d => d.status === 'Active');
const [sel, setSel] = useState(activeDrivers[0] ? activeDrivers[0].id : (ctx.drivers[0] && ctx.drivers[0].id));
const driver = ctx.drivers.find(d => d.id === sel);
const legs = ctx.schedule.filter(l => l.driverId === sel).sort((a, b) => a.date < b.date ? -1 : 1);
const trk = legs[0] ? ctx.trucks.find(t => t.id === legs[0].truckId) : null;
// build display rows incl. night rest
const rows = [];
legs.forEach((l, i) => {
rows.push({ kind: 'leg', leg: l });
if (l.leg === 'outbound') {
rows.push({ kind: 'rest', date: l.date });
}
});
return (
{driver && (
{driver.id}
{driver.name}
{driver.phone || 'No phone'} · {driver.license || 'CDL-A'}
Truck
{trk ? trk.name : 'Unassigned'}
)}
Weekly Cycle
{legs.length ? 'NC ⇄ NJ round trip' : 'Not scheduled this week'}
{legs.length === 0 ? (
This driver has no loads this week.
Generate the schedule or set the driver to Active.
) : (
| Date | Activity | Route | Status |
{rows.map((r, i) => {
if (r.kind === 'rest') {
return (
| {Engine.fmtLong(r.date)} · PM |
Mandatory 10-hour DOT rest — NJ
|
NJ overnight |
Rest |
);
}
const l = r.leg;
return (
| {Engine.fmtLong(l.date)} |
{l.leg === 'outbound' ? 'Outbound — Pickup NC' : 'Backhaul — Return NC'}
{l.delayed && Weekend delay}
|
|
|
);
})}
)}
);
}
/* =================== DISPATCH BOARD =================== */
function BoardView({ ctx }) {
const [pop, setPop] = useState(null); // {leg, x, y}
const dates = Engine.scheduleDates(ctx.schedule);
const trucks = ctx.trucks;
// index legs by truck+date
const cellMap = {};
ctx.schedule.forEach(l => {
const key = l.truckId + '|' + l.date;
(cellMap[key] = cellMap[key] || []).push(l);
});
const gridCols = '150px repeat(' + Math.max(dates.length, 1) + ', minmax(108px, 1fr))';
const openPop = (e, leg) => {
e.stopPropagation();
const r = e.currentTarget.getBoundingClientRect();
let x = r.left, y = r.bottom + 6;
if (x + 230 > window.innerWidth) x = window.innerWidth - 240;
if (y + 230 > window.innerHeight) y = r.top - 236;
setPop({ leg, x, y });
};
return (
setPop(null)}>
Outbound (NC→NJ)
Backhaul (NJ→NC)
Delayed return
Off duty
Click a load to override status
{/* header row */}
{dates.map(dt => {
const wd = Engine.dow(dt);
const weekend = wd === 0 || wd === 6;
return (
{Engine.weekdayName(dt)}
{Engine.fmtShort(dt)}
);
})}
{/* truck rows */}
{trucks.map(t => (
{dates.map(dt => {
const legs = cellMap[t.id + '|' + dt] || [];
if (legs.length === 0) return ;
return (
{legs.map(l => {
const drv = ctx.drivers.find(d => d.id === l.driverId);
const cls = l.delayed ? 'l-delayed' : (l.leg === 'outbound' ? 'l-out' : 'l-back');
return (
openPop(e, l)}>
{drv ? drv.name.split(' ')[0] : 'Driver'}
{l.from}→{l.to}
{l.delayed && l.status !== 'completed' ? 'DELAYED · ' : ''}{(STATUS_META[l.status] || {}).label}
);
})}
);
})}
))}
{pop &&
setPop(null)} />}
);
}
function STATUS_COLOR(l) {
if (l.delayed && l.status !== 'completed') return 'var(--amber)';
if (l.status === 'completed') return 'var(--green)';
if (l.status === 'in_transit') return 'var(--accent-2)';
return 'var(--text-faint)';
}
function CellPopover({ pop, ctx, close }) {
const drv = ctx.drivers.find(d => d.id === pop.leg.driverId);
const opts = [
{ v: 'scheduled', label: 'Scheduled', color: 'var(--gray)' },
{ v: 'in_transit', label: 'In Transit', color: 'var(--accent)' },
{ v: 'completed', label: 'Completed', color: 'var(--green)' },
{ v: 'delayed', label: 'Delayed Return', color: 'var(--amber)' },
];
const choose = (v) => {
if (v === 'delayed') ctx.overrideLeg(pop.leg.id, { delayed: true });
else ctx.overrideLeg(pop.leg.id, { status: v, delayed: v === 'completed' ? pop.leg.delayed : false });
close();
};
return (
e.stopPropagation()}>
{drv ? drv.name : 'Load'} · {pop.leg.from}→{pop.leg.to}
{opts.map(o => {
const sel = (o.v === 'delayed') ? (pop.leg.delayed && pop.leg.status !== 'completed') : (pop.leg.status === o.v && !(pop.leg.delayed && pop.leg.status !== 'completed'));
return (
choose(o.v)}>
{o.label}
{sel && }
);
})}
);
}
/* =================== SETTINGS =================== */
function SettingsView({ ctx }) {
const [localDate, setLocalDate] = useState(ctx.weekStart);
const [showPw, setShowPw] = useState(false);
const snappedNote = Engine.dow(localDate) !== 1;
return (
Week Start Date
Editable every Monday
{snappedNote && (
Not a Monday — the scheduler will snap to the Monday of that week ({Engine.fmtFull(Engine.snapToMonday(localDate))}).
)}
Appearance
Quick Admin
{showPw &&
setShowPw(false)} />}
);
}
function ChangePasswordModal({ ctx, close }) {
const [cur, setCur] = useState('');
const [next, setNext] = useState('');
const [conf, setConf] = useState('');
const [err, setErr] = useState('');
const save = () => {
if (next !== conf) { setErr('New passwords do not match.'); return; }
const res = ctx.changePassword(cur, next);
if (res === 'wrong-current') { setErr('Current password is incorrect.'); return; }
if (res === 'too-short') { setErr('New password must be at least 4 characters.'); return; }
close();
};
return (
>}>
{ setCur(e.target.value); setErr(''); }} placeholder="Enter current password" />
{ setNext(e.target.value); setErr(''); }} placeholder="At least 4 characters" />
{ setConf(e.target.value); setErr(''); }} placeholder="Re-enter new password" />
{err && {err}
}
The new password is saved on this device and required next time the dashboard is locked or reopened.
);
}
function RuleRow({ icon, title, desc }) {
return (
);
}
Object.assign(window, { ScheduleView, BoardView, SettingsView });