// Dashboard.jsx function StatCard({ icon, iconKind, label, value, delta, deltaDir }) { const Ico = window[icon]; return (
{delta ? (
{deltaDir === 'down' ? : } {delta}
) : null}
{label}
{value}
); } function QABtn({ icon, title, sub, onClick, primary }) { const Ico = window[icon]; return ( ); } function MiniBarChart() { const data = [ { day: 'Mon', v: 6 }, { day: 'Tue', v: 8 }, { day: 'Wed', v: 5 }, { day: 'Thu', v: 9 }, { day: 'Fri', v: 11, accent: true }, { day: 'Sat', v: 7 }, { day: 'Sun', v: 3 }, ]; const max = Math.max(...data.map(d => d.v)); return (
{data.map(d => (
{d.day}
))}
); } // ===== Highest Earning Crane ============================================ // "What's happening now": which machine has generated the most completed-job // revenue this calendar month. If too few jobs are completed early in the // month (<5), fall back to a rolling last-30-days window instead. const _parseRM = (s) => Number(String(s || '').replace(/[^0-9.]/g, '')) || 0; const _fmtMoney = (n) => 'RM ' + Math.round(n).toLocaleString('en-MY'); const _ymd = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; const _addDays = (iso, n) => { const d = new Date(iso + 'T12:00:00'); d.setDate(d.getDate() + n); return _ymd(d); }; const _firstOfMonth = (iso) => iso.slice(0, 7) + '-01'; const _lastOfMonth = (iso) => { const d = new Date(iso + 'T12:00:00'); return _ymd(new Date(d.getFullYear(), d.getMonth() + 1, 0)); }; // Plate as stored on bookings (short) → full registration plate from master. const _machineForPlate = (plate) => MACHINES.find(x => x.plate && x.plate.split(' ').pop() === String(plate)) || null; const _fullPlate = (plate) => { const m = _machineForPlate(plate); return m ? m.plate : plate; }; // Unique completed jobs — dedupe multi-day rows by booking id so a single // booking's price isn't counted once per day. Attribute to its end date. function _completedJobs() { const seen = {}; BOOKINGS.forEach(b => { if (b.status !== 'Job Completed') return; if (!seen[b.id]) seen[b.id] = { plate: b.plate || '—', revenue: _parseRM(b.price), date: b.dateTo || b.date }; }); return Object.values(seen); } function _earningsByCrane(jobs, startISO, endISO) { const m = {}; jobs.forEach(j => { if (j.date < startISO || j.date > endISO) return; if (!m[j.plate]) m[j.plate] = { plate: j.plate, revenue: 0, jobs: 0 }; m[j.plate].revenue += j.revenue; m[j.plate].jobs += 1; }); return Object.values(m).sort((a, b) => b.revenue - a.revenue); } function computeHighestEarner() { const jobs = _completedJobs(); const monthStart = _firstOfMonth(TODAY); const monthRows = _earningsByCrane(jobs, monthStart, TODAY); const monthJobCount = monthRows.reduce((a, c) => a + c.jobs, 0); const useMonth = monthJobCount >= 5; let curStart, curEnd, prevStart, prevEnd, periodLabel, compareLabel; if (useMonth) { curStart = monthStart; curEnd = TODAY; prevStart = _firstOfMonth(_addDays(monthStart, -1)); prevEnd = _lastOfMonth(prevStart); periodLabel = 'this month'; compareLabel = 'vs last month'; } else { curStart = _addDays(TODAY, -29); curEnd = TODAY; prevStart = _addDays(TODAY, -59); prevEnd = _addDays(TODAY, -30); periodLabel = 'last 30 days'; compareLabel = 'vs prior 30 days'; } const cur = _earningsByCrane(jobs, curStart, curEnd); const prevByPlate = Object.fromEntries(_earningsByCrane(jobs, prevStart, prevEnd).map(r => [r.plate, r.revenue])); const winner = cur[0] || null; let deltaPct = null, deltaDir = 'up'; if (winner) { const p = prevByPlate[winner.plate] || 0; if (p > 0) { deltaPct = Math.round((winner.revenue - p) / p * 100); deltaDir = deltaPct >= 0 ? 'up' : 'down'; } } return { useMonth, periodLabel, compareLabel, winner, top5: cur.slice(0, 5), deltaPct, deltaDir }; } function HighestEarnerCard({ info, onOpen }) { const { winner, periodLabel, compareLabel, deltaPct, deltaDir } = info; const plate = winner ? _fullPlate(winner.plate) : '—'; const mac = winner ? _machineForPlate(winner.plate) : null; const rev = winner ? winner.revenue : 0; const Trend = deltaDir === 'down' ? TrendDown : TrendUp; return ( ); } function TopEarnersModal({ info, onClose }) { const { top5, periodLabel } = info; const max = top5.length ? top5[0].revenue : 1; return (
e.stopPropagation()} style={{maxWidth: 540, width: '94vw'}}>
Top 5 Earning Cranes
Completed-job revenue · {periodLabel}
{top5.map((r, i) => { const mac = _machineForPlate(r.plate); return (
{i + 1}
{_fullPlate(r.plate)} {_fmtMoney(r.revenue)}
{mac ? machineLabel(mac) : 'Cross-rented'} · {r.jobs} completed job{r.jobs === 1 ? '' : 's'}
); })} {top5.length === 0 ?
No completed jobs in this period.
: null}
); } // ===== Jobs This Month & Top Crane Type ================================ // Workload (not just completed work): count every BOOKED booking whose // job_date falls in the current calendar month. "Booked" = anything that // isn't Cancelled. Dedupe by booking id so multi-day rows count once, and // attribute the booking to its start date. const _BOOKED_OUT = new Set(['Cancelled']); function _bookedJobs() { const seen = {}; BOOKINGS.forEach(b => { if (_BOOKED_OUT.has(b.status)) return; if (!seen[b.id]) seen[b.id] = { id: b.id, date: b.date, category: b.category || '—', tonnage: b.tonnage || '' }; }); return Object.values(seen); } function _countInMonth(jobs, startISO, endISO) { return jobs.filter(j => j.date >= startISO && j.date <= endISO).length; } function computeJobsThisMonth() { const jobs = _bookedJobs(); const monthStart = _firstOfMonth(TODAY); const monthEnd = _lastOfMonth(TODAY); const prevStart = _firstOfMonth(_addDays(monthStart, -1)); const prevEnd = _lastOfMonth(prevStart); const cur = _countInMonth(jobs, monthStart, monthEnd); const prev = _countInMonth(jobs, prevStart, prevEnd); let deltaPct = null, deltaDir = 'up'; if (prev > 0) { deltaPct = Math.round((cur - prev) / prev * 100); deltaDir = deltaPct >= 0 ? 'up' : 'down'; } return { cur, prev, deltaPct, deltaDir }; } // Pretty tonnage/size: '50t' → '50T', '20m' → '20M', '—' stays. const _fmtSize = (s) => String(s || '').replace(/t$/i, 'T').replace(/m$/i, 'M'); function computeTopCraneType() { const jobs = _bookedJobs(); const monthStart = _firstOfMonth(TODAY); const monthEnd = _lastOfMonth(TODAY); const counts = {}; jobs.forEach(j => { if (j.date < monthStart || j.date > monthEnd) return; const size = _fmtSize(j.tonnage); const key = j.category + (size && size !== '—' ? '||' + size : ''); if (!counts[key]) counts[key] = { category: j.category, size: (size && size !== '—') ? size : '', jobs: 0 }; counts[key].jobs += 1; }); const ranked = Object.values(counts).sort((a, b) => b.jobs - a.jobs); return { top: ranked[0] || null, ranked }; } function TopCraneTypeCard({ info, monthLabel }) { const top = info.top; return (
{top ?
Most requested
: null}
Top Crane Type in Demand
{top ? top.category : '—'} {top && top.size ? · {top.size} : null}
{top ? top.jobs : 0} job{top && top.jobs === 1 ? '' : 's'} in {monthLabel}
); } // ===== Monthly Revenue (card 5) ======================================== // Earned Revenue = sum of price for jobs that are *Job Completed* this // calendar month, attributed to the completion date. Multi-day rows share // a booking id and one price, so dedupe by id (via _completedJobs). Trend = // revenue growth % vs the previous calendar month. const _fmtMoneyShort = (n) => { if (n >= 1000000) return 'RM ' + (n / 1000000).toFixed(n >= 10000000 ? 0 : 1).replace(/\.0$/, '') + 'M'; if (n >= 1000) return 'RM ' + Math.round(n / 1000) + 'k'; return 'RM ' + Math.round(n); }; function computeMonthlyRevenue() { const jobs = _completedJobs(); const monthStart = _firstOfMonth(TODAY); const monthEnd = _lastOfMonth(TODAY); const prevStart = _firstOfMonth(_addDays(monthStart, -1)); const prevEnd = _lastOfMonth(prevStart); const sumIn = (s, e) => jobs.reduce((a, j) => (j.date >= s && j.date <= e ? a + j.revenue : a), 0); const cur = sumIn(monthStart, monthEnd); const prev = sumIn(prevStart, prevEnd); let deltaPct = null, deltaDir = 'up'; if (prev > 0) { deltaPct = Math.round((cur - prev) / prev * 100); deltaDir = deltaPct >= 0 ? 'up' : 'down'; } return { cur, prev, deltaPct, deltaDir }; } // ===== Crane Utilization (card 6) ====================================== // Utilization % = booked crane-hours ÷ available crane-hours, over the // current calendar month. // Booked = Σ(end−start) across every non-cancelled booking row this // month. Multi-day bookings store one row per day, so each row // correctly contributes that day's hours (no dedupe here). // Available= active cranes × working-hours/day × working-days-in-month. // "Cranes" = the owned lifting fleet (mobile/RT cranes, skylifts, scissor // lift) — cross-rent units and company cars are excluded. Split out by // machine type and by individual unit for the drill-down. const _CRANE_CATS = new Set(['Mobile Crane', 'Rough Terrain Crane', 'Skylift', 'Scissor Lift']); const HOURS_PER_DAY = 8; const _hoursOfBooking = (b) => { if (!b.start || !b.end) return 0; const [sh, sm] = b.start.split(':').map(Number); const [eh, em] = b.end.split(':').map(Number); return Math.max(0, (eh + (em || 0) / 60) - (sh + (sm || 0) / 60)); }; // Working days = Mon–Sat (Sundays off). Counted from `startISO` through // `endISO` inclusive — used for month-to-date, so we never divide elapsed // booked hours by a whole month's capacity mid-month (which would make // every month look empty until the 30th). function _workingDaysBetween(startISO, endISO) { const d = new Date(startISO + 'T12:00:00'); let n = 0; while (_ymd(d) <= endISO) { if (d.getDay() !== 0) n += 1; d.setDate(d.getDate() + 1); } return n; } // The Nth working day on/after startISO (for a fair previous-month MTD window). function _nthWorkingDay(startISO, k) { const d = new Date(startISO + 'T12:00:00'); let n = 0; while (true) { if (d.getDay() !== 0) { n += 1; if (n === k) return _ymd(d); } d.setDate(d.getDate() + 1); } } function _bookedHoursByMachine(startISO, endISO) { const byId = {}; let total = 0; BOOKINGS.forEach(b => { if (b.status === 'Cancelled') return; if (b.date < startISO || b.date > endISO) return; const id = (b.machine || '').split(' ')[0]; const h = _hoursOfBooking(b); if (!h) return; byId[id] = (byId[id] || 0) + h; total += h; }); return { byId, total }; } function computeUtilization() { const cranes = MACHINES.filter(m => _CRANE_CATS.has(m.category)); const active = cranes.filter(m => m.status !== 'unavailable'); const monthStart = _firstOfMonth(TODAY); const workingDays = _workingDaysBetween(monthStart, TODAY); // month-to-date const availPerCrane = HOURS_PER_DAY * workingDays; const totalAvail = active.length * availPerCrane; const { byId, total: totalBooked } = _bookedHoursByMachine(monthStart, TODAY); const pct = totalAvail > 0 ? Math.round(totalBooked / totalAvail * 100) : 0; // By machine type const typeMap = {}; active.forEach(m => { if (!typeMap[m.category]) typeMap[m.category] = { category: m.category, booked: 0, count: 0 }; typeMap[m.category].count += 1; typeMap[m.category].booked += byId[m.id] || 0; }); const byType = Object.values(typeMap).map(t => { const avail = t.count * availPerCrane; return { ...t, avail, pct: avail > 0 ? Math.round(t.booked / avail * 100) : 0 }; }).sort((a, b) => b.pct - a.pct); // By individual unit (active only) const byUnit = active.map(m => { const booked = byId[m.id] || 0; return { machine: m, booked, avail: availPerCrane, pct: availPerCrane > 0 ? Math.round(booked / availPerCrane * 100) : 0 }; }).sort((a, b) => b.pct - a.pct); // Previous-month utilization over the SAME number of working days, for a // like-for-like trend. const prevStart = _firstOfMonth(_addDays(monthStart, -1)); const prevEnd = _nthWorkingDay(prevStart, workingDays); const prevAvail = active.length * availPerCrane; const prevBooked = _bookedHoursByMachine(prevStart, prevEnd).total; const prevPct = prevAvail > 0 ? Math.round(prevBooked / prevAvail * 100) : 0; return { pct, totalBooked, totalAvail, workingDays, activeCount: active.length, totalCount: cranes.length, byType, byUnit, diff: pct - prevPct, }; } function UtilizationCard({ info, onOpen }) { const diff = info.diff; const Trend = diff >= 0 ? TrendUp : TrendDown; return ( ); } function _UtilRow({ label, sub, pct }) { const tone = pct >= 75 ? 'var(--color-green, #1f9d55)' : pct >= 45 ? 'var(--color-yellow-700)' : 'var(--color-navy)'; return (
{label} {pct}%
{sub ?
{sub}
: null}
); } function UtilizationModal({ info, monthLabel, onClose }) { const fmtH = (n) => Math.round(n).toLocaleString('en-MY') + 'h'; return (
e.stopPropagation()} style={{maxWidth: 560, width: '94vw', maxHeight: '88vh', overflowY: 'auto'}}>
Crane Utilization · {monthLabel}
Booked ÷ available hours, month-to-date · Mon–Sat, {HOURS_PER_DAY}h/day
{info.pct}%
Fleet utilization
{fmtH(info.totalBooked)} / {fmtH(info.totalAvail)}
{info.activeCount} active cranes · {info.workingDays} working day{info.workingDays === 1 ? '' : 's'} so far
By machine type
{info.byType.map(t => ( <_UtilRow key={t.category} label={t.category} pct={t.pct} sub={`${t.count} unit${t.count === 1 ? '' : 's'} · ${fmtH(t.booked)} booked`}/> ))}
By unit · busiest first
{info.byUnit.map(u => ( <_UtilRow key={u.machine.id} label={machineLabel(u.machine)} pct={u.pct} sub={`${u.machine.category} · ${fmtH(u.booked)} booked`}/> ))}
); } function Dashboard({ setPage }) { const [showEarners, setShowEarners] = React.useState(false); const [showUtil, setShowUtil] = React.useState(false); const earnerInfo = React.useMemo(computeHighestEarner, []); const jobsMonth = React.useMemo(computeJobsThisMonth, []); const topType = React.useMemo(computeTopCraneType, []); const revenue = React.useMemo(computeMonthlyRevenue, []); const util = React.useMemo(computeUtilization, []); const _monthLabel = new Date(TODAY + 'T12:00:00').toLocaleDateString('en-GB', { month: 'short' }); const jobsMonthDelta = jobsMonth.deltaPct !== null ? (jobsMonth.deltaPct >= 0 ? '+' : '−') + Math.abs(jobsMonth.deltaPct) + '% vs last month' : null; const revenueDelta = revenue.deltaPct !== null ? (revenue.deltaPct >= 0 ? '+' : '−') + Math.abs(revenue.deltaPct) + '% vs last month' : null; // ===== Today's Jobs ===================================================== // Shown as fleet utilisation %: booked crane-days today ÷ operable cranes. // Each booking contributes hours/8 of a crane-day (8h = a full day's job). // Cancelled bookings don't count. Trend = today's % − yesterday's %, in pts. const NON_BOOKED = new Set(['Cancelled']); const hoursOf = (b) => { if (!b.start || !b.end) return 0; const [sh, sm] = b.start.split(':').map(Number); const [eh, em] = b.end.split(':').map(Number); return Math.max(0, (eh + (em || 0) / 60) - (sh + (sm || 0) / 60)); }; const jobUnitsOn = (dateISO) => BOOKINGS .filter(b => b.date === dateISO && !NON_BOOKED.has(b.status)) .reduce((sum, b) => sum + hoursOf(b) / 8, 0); const yesterdayISO = (() => { // Use noon + local date parts so the result isn't shifted a day by the // UTC conversion in toISOString(). const d = new Date(TODAY + 'T12:00:00'); d.setDate(d.getDate() - 1); const y = d.getFullYear(), m = String(d.getMonth() + 1).padStart(2, '0'), dd = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${dd}`; })(); // Daily capacity = number of operable cranes (anything not flagged unavailable). const fleetCapacity = Math.max(1, MACHINES.filter(m => m.status !== 'unavailable').length); const pctOn = (dateISO) => Math.round((jobUnitsOn(dateISO) / fleetCapacity) * 100); const todayPct = pctOn(TODAY); const yPct = pctOn(yesterdayISO); const jobsDiff = todayPct - yPct; const todayJobsValue = todayPct + '%'; const todayJobsDelta = (jobsDiff >= 0 ? '+' : '−') + Math.abs(jobsDiff) + ' pts vs yesterday'; const todayJobsDir = jobsDiff >= 0 ? 'up' : 'down'; return (
setShowEarners(true)}/> setShowUtil(true)}/>

Quick Actions

Most-used dispatcher tools
setPage('create')}/> setPage('workorders')}/> setPage('export')}/> setPage('analytics')}/> setPage('notifications')}/>
Today's Schedule
{WORK_ORDERS.filter(w => w.date === TODAY).map(w => { const id = (w.machine || w.crane || '').split(' ')[0]; const m = MACHINES.find(x => x.id === id); const dot = m ? m.status : 'unavailable'; const ms = dot === 'available' ? { color: 'green', label: 'Available' } : dot === 'occupied' ? { color: 'red', label: 'Booked' } : { color: 'gray', label: 'Not available' }; return ( ); })}
WO IDCustomerCraneOperatorStatus
{w.id} {w.customer} {m ? machineLabel(m) : w.tonnage} {w.operator} {ms.label}
Jobs This Week
Live Fleet Status
{[ { id: 'PLC-001', loc: 'Pasir Gudang', status: 'active' }, { id: 'PLC-009', loc: 'Medini', status: 'active' }, { id: 'PLC-011', loc: 'Yard — Skudai', status: 'idle' }, { id: 'PLC-015', loc: 'Workshop', status: 'repair' }, ].map(c => { const m = MACHINES.find(x => x.id === c.id); const name = m ? machineLabel(m) : c.id; return (
{name}
{c.loc}
{c.status === 'active' ? 'On site' : c.status === 'idle' ? 'Idle' : 'Repair'}
); })}
{showEarners ? setShowEarners(false)}/> : null} {showUtil ? setShowUtil(false)}/> : null}
); } Object.assign(window, { Dashboard });