// RM.jsx — R&M Overview: Repair tickets (left) + Maintenance schedule with countdown (right) // Replaces Repairs.jsx — surfaces overdue maintenance prominently function countdownClass(daysLeft) { if (daysLeft < 0) return 'countdown-pill--overdue'; if (daysLeft <= 14) return 'countdown-pill--soon'; return 'countdown-pill--ok'; } function countdownLabel(daysLeft) { if (daysLeft < 0) return Math.abs(daysLeft) + 'd OVERDUE'; if (daysLeft === 0) return 'DUE TODAY'; return daysLeft + 'd left'; } function RMPage({ role }) { const [tab, setTab] = React.useState('overview'); // overview | repair | maintenance const [editingMt, setEditingMt] = React.useState(null); const [intervalDraft, setIntervalDraft] = React.useState(90); // Open the AppSheet-style form modal. null when closed; 'schedule' | 'ticket' when open. const [addForm, setAddForm] = React.useState(null); const [toast, setToast] = React.useState(null); const [search, setSearch] = React.useState(''); const canEdit = role === 'Admin' || role === 'Mechanic'; // ===== Full-text search across both datasets (customer/plate/operator-style: // here it spans machine, issue/type, mechanic, ticket id, priority, status). ===== const q = search.trim().toLowerCase(); const matches = (obj) => !q || Object.values(obj).some(v => String(v ?? '').toLowerCase().includes(q)); const repairRows = REPAIR_TICKETS.filter(matches); const maintRows = MAINTENANCE.filter(matches); const overdue = maintRows.filter(m => m.days_left < 0).sort((a, b) => a.days_left - b.days_left); const soon = maintRows.filter(m => m.days_left >= 0 && m.days_left <= 14); const repairsActive = repairRows.filter(r => r.status !== 'Completed'); const repairsDone = repairRows.filter(r => r.status === 'Completed'); const startEditMt = (m) => { setEditingMt(m.id); setIntervalDraft(m.interval_days); }; return (
{/* Overdue banner */} {overdue.length > 0 ? (
{overdue.length} overdue maintenance task{overdue.length === 1 ? '' : 's'}
Worst: {overdue[0].machine} — {overdue[0].type} is {Math.abs(overdue[0].days_left)} days overdue. Schedule service ASAP to avoid downtime risk.
) : null} {/* Tabs */}
{/* Full-text search — spans repair tickets + maintenance schedule */}
setSearch(e.target.value)} placeholder="Search R&M — machine, plate, issue, mechanic, status..." className="input" style={{height: 48, paddingLeft: 44, paddingRight: search ? 40 : 14, width: '100%', fontSize: 15, borderRadius: 12}}/> {search ? ( ) : null}
{tab === 'overview' ? (
{/* Left: Repair side */}

Active Repairs

Unscheduled fault tickets
{repairsActive.length} open
{repairsActive.map(r => (
{r.id}
{r.machine}
{r.issue}
Mechanic: {r.mechanic} · Opened {r.opened}
{r.priority}
))}
{/* Right: Maintenance side */}

Upcoming Maintenance

Recurring schedule · Days countdown
{overdue.length > 0 ? {overdue.length} overdue : on track}
{[...overdue, ...soon].slice(0, 8).map(m => (
{m.machine}
{m.type}
Last: {m.last} · Every {m.months} mo
{countdownLabel(m.days_left)}
))}
) : null} {tab === 'repair' ? (

All Repair Tickets

Workshop fault queue
{canEdit ? : null}
{repairRows.map(r => ( ))} {repairRows.length === 0 ? ( ) : null}
TicketMachineIssuePriority MechanicCostStatus
{r.id} {r.machine} {r.issue} {r.priority} {r.mechanic} {r.cost} {r.status}
No repair tickets match “{search}”
) : null} {tab === 'maintenance' ? (

Maintenance Schedule

Recurring · Fixed countdown period per service item (months)
{canEdit ? : null}
{maintRows.sort((a, b) => a.days_left - b.days_left).map(m => ( ))} {maintRows.length === 0 ? ( ) : null}
MachineService ItemLast Done Next DueInterval (fixed)Countdown
{m.machine} {m.type} {m.last} {m.next} {m.months} mo {countdownLabel(m.days_left)}
No maintenance schedules match “{search}”
{!canEdit ? (
Service intervals are fixed per item (in months) and apply to every crane. Adding schedules is restricted to Admin and Mechanic roles.
) : null}
) : null} {/* AppSheet-style add form */} {addForm ? ( setAddForm(null)} onSubmit={(payload) => { setAddForm(null); setToast(payload.mode === 'schedule' ? 'Schedule saved' : 'Ticket created'); window.setTimeout(() => setToast(null), 2600); }} /> ) : null} {/* Success toast */} {toast ? (
{toast}
) : null}
); } // ==================================================================== // AddRMForm — AppSheet-style modal used for both "Add Schedule" and "New // Ticket". The two modes share most fields; only labels/required-flags and // a couple of dropdown sources differ. Built to mirror the AppSheet input // the user is familiar with on mobile (label above, outlined input, red * // for required, read-only fields appear muted). // ==================================================================== function AddRMForm({ mode, onClose, onSubmit }) { const isSchedule = mode === 'schedule'; const today = new Date(); const todayISO = today.toISOString().slice(0, 10); // Form state const [date, setDate] = React.useState(todayISO); const [category, setCategory] = React.useState(''); const [machine, setMachine] = React.useState(''); const [serviceItem, setServiceItem] = React.useState(''); const [operator, setOperator] = React.useState(''); const [info, setInfo] = React.useState(''); const [supplier, setSupplier] = React.useState(''); const [mechanic, setMechanic] = React.useState(''); const [showErrors, setShowErrors] = React.useState(false); // Capture "Added" timestamp ONCE on mount so it doesn't tick while user // is filling the form. Matches AppSheet behaviour. const addedTs = React.useRef(new Date()).current; // Derived: human-readable formatted date — "24 May 2026 (Sun)" const formattedDate = React.useMemo(() => { if (!date) return ''; const d = new Date(date + 'T00:00:00'); const dd = d.getDate(); const mon = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][d.getMonth()]; const yr = d.getFullYear(); const dow = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][d.getDay()]; return `${dd} ${mon} ${yr} (${dow})`; }, [date]); // Derived: Days_Left = date - today (positive = upcoming, negative = overdue) const daysLeft = React.useMemo(() => { if (!date) return 0; const a = new Date(date + 'T00:00:00'); const b = new Date(todayISO + 'T00:00:00'); return Math.round((a - b) / 86400000); }, [date, todayISO]); // Service items — schedule mode pulls the FIXED service catalog (months); // ticket mode uses common repair issue templates. const SERVICE_ITEMS_SCHEDULE = SERVICE_ITEMS.map(s => s.item); const SERVICE_ITEMS_TICKET = [ 'Hydraulic leak', 'Engine fault', 'Slew gear noise', 'Outrigger drift', 'Boom inspection', 'Electrical fault', 'Tyre / wheel', 'Other (see Info)', ]; const serviceItems = isSchedule ? SERVICE_ITEMS_SCHEDULE : SERVICE_ITEMS_TICKET; // Machines filtered by chosen category const machinesForCat = category ? MACHINES.filter(m => m.category === category) : MACHINES; // Reset machine if category changes and current machine is no longer in list React.useEffect(() => { if (machine && category) { const m = MACHINES.find(x => x.id === machine || x.name === machine); if (!m || m.category !== category) setMachine(''); } }, [category]); // Required-field check — Date, Service_Item, Category, Machine are required. const errors = { date: !date, serviceItem: !serviceItem, category: !category, machine: !machine, }; const hasErrors = Object.values(errors).some(Boolean); const handleSubmit = () => { if (hasErrors) { setShowErrors(true); return; } if (onSubmit) onSubmit({ mode, date, formattedDate, daysLeft, serviceItem, category, machine, operator, info, supplier, mechanic, addedTs, }); }; // Esc to close React.useEffect(() => { const onKey = (e) => { if (e.key === 'Escape') onClose && onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onClose]); // === Inline styled primitives ==================================== const labelStyle = { display: 'block', fontSize: 13, color: 'var(--color-grey-700)', marginBottom: 6, fontWeight: 500 }; const reqStar = *; const inputBase = { width: '100%', height: 44, padding: '0 14px', border: '1px solid var(--color-grey-300)', borderRadius: 8, fontSize: 15, color: 'var(--color-grey-900, #111827)', background: 'white', boxSizing: 'border-box', fontFamily: 'inherit', outline: 'none', }; const inputErr = { borderColor: '#dc2626', background: '#fef2f2' }; const inputReadonly = { background: 'var(--color-grey-50)', color: 'var(--color-grey-500)' }; // Reusable "field row" — label on top, input below, validation msg under. const Field = ({ label, required, hint, error, children }) => (
{children} {hint ?
{hint}
: null} {error ?
{error}
: null}
); // Format "Added" for read-only field — dd/mm/yyyy, hh:mm:ss AM/PM const fmtAdded = (d) => { const dd = String(d.getDate()).padStart(2, '0'); const mm = String(d.getMonth() + 1).padStart(2, '0'); const yy = d.getFullYear(); let h = d.getHours(); const ap = h >= 12 ? 'PM' : 'AM'; h = h % 12; if (h === 0) h = 12; const mi = String(d.getMinutes()).padStart(2, '0'); const se = String(d.getSeconds()).padStart(2, '0'); return `${dd}/${mm}/${yy}, ${String(h).padStart(2,'0')}:${mi}:${se} ${ap}`; }; return (
e.stopPropagation()} style={{ background: 'white', width: '100%', maxWidth: 560, maxHeight: '94vh', margin: 'auto', borderRadius: 14, display: 'flex', flexDirection: 'column', boxShadow: '0 24px 56px rgba(11,31,58,0.30)', }}> {/* Header — navy bar so it matches the rest of the dashboard chrome */}
{isSchedule ? 'Maintenance' : 'Workshop'}
{isSchedule ? 'Add Schedule' : 'Add New Ticket'}
{/* Scrollable form body */}
{/* Date (editable) */}
setDate(e.target.value)} style={{...inputBase, paddingRight: 44, ...(showErrors && errors.date ? inputErr : {})}}/>
{/* Date (display, read-only — auto-formatted) */} {/* Days_Left (auto-computed) */} {/* Service_Item (dropdown) */}
{/* Interval (fixed) — auto-filled from the service catalog, read-only */} {isSchedule ? (
) : null}
{/* Machine */}
{/* Operator */}
{/* Info — multi-line for context */}