// AllBookings.jsx — full history view of every booking, with collapsible // Year → Month → Day sidebar, full-text search, multi-day "Multiple" pill, // and row-click → Modify dialog (reusing the one from Bookings.jsx). function fmtAllDate(iso) { const d = new Date(iso + 'T00:00:00'); return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', weekday: 'short' }); } function ymOf(iso) { return iso.slice(0, 7); } // "2026-05" function yOf(iso) { return iso.slice(0, 4); } // "2026" function categoryIcon(cat) { if (cat === 'Mobile Crane') return Crane; if (cat === 'Skylift') return Truck; if (cat === 'Scissor Lift') return Activity; if (cat === 'Lorry Crane') return Truck; if (cat === 'Lorry') return Truck; if (cat === 'Boom Lift') return Activity; if (cat === 'Rough Terrain Crane')return Crane; if (cat === 'Cross Rent') return Layers; return Briefcase; } function AllBookingsPage({ role, setPage }) { const [search, setSearch] = React.useState(''); // sidebar selection: { kind: 'all' | 'year' | 'month' | 'day', value: '...' } const [sel, setSel] = React.useState({ kind: 'all' }); // expansion state: which years/months are open in sidebar const [openYears, setOpenYears] = React.useState({ '2026': true }); const [openMonths, setOpenMonths] = React.useState({ '2026-05': true }); const [modifyTarget, setModifyTarget] = React.useState(null); const [, forceRender] = React.useReducer(x => x + 1, 0); const isAdmin = role === 'Admin'; // ===== Build sidebar tree from a CONTINUOUS date range ===== // Show every day from the earliest booking through today + 14 days, so // the sidebar shows every date in the calendar — even those without bookings. const bookingDates = BOOKINGS.map(b => b.date).filter(Boolean); const minDate = bookingDates.length ? bookingDates.reduce((a, b) => a < b ? a : b) : TODAY; // Forward window: today + 14 days, but never earlier than the latest booking. const maxBooking = bookingDates.length ? bookingDates.reduce((a, b) => a > b ? a : b) : TODAY; const todayPlus14 = (() => { const d = new Date(TODAY + 'T00:00:00'); d.setDate(d.getDate() + 14); return d.toISOString().slice(0, 10); })(); const maxDate = maxBooking > todayPlus14 ? maxBooking : todayPlus14; // Generate every ISO date string between minDate and maxDate (inclusive). const allDates = (() => { const out = []; const cur = new Date(minDate + 'T00:00:00'); const end = new Date(maxDate + 'T00:00:00'); while (cur <= end) { const y = cur.getFullYear(); const m = String(cur.getMonth() + 1).padStart(2, '0'); const dd = String(cur.getDate()).padStart(2, '0'); out.push(`${y}-${m}-${dd}`); cur.setDate(cur.getDate() + 1); } return out.reverse(); // newest first })(); const tree = {}; // { year: { month: [days] } } allDates.forEach(d => { const y = yOf(d), mo = ymOf(d); if (!tree[y]) tree[y] = {}; if (!tree[y][mo]) tree[y][mo] = []; tree[y][mo].push(d); }); // ===== Apply filter by sidebar selection ===== let filtered = BOOKINGS.slice(); if (sel.kind === 'year') filtered = filtered.filter(b => yOf(b.date) === sel.value); if (sel.kind === 'month') filtered = filtered.filter(b => ymOf(b.date) === sel.value); if (sel.kind === 'day') filtered = filtered.filter(b => b.date === sel.value); // ===== Apply full-text search across all fields ===== if (search.trim()) { const q = search.trim().toLowerCase(); filtered = filtered.filter(b => Object.values(b).some(v => String(v ?? '').toLowerCase().includes(q)) ); } // ===== Sort newest first by date ===== filtered.sort((a, b) => (b.date || '').localeCompare(a.date || '')); // Selection label for header const selLabel = sel.kind === 'all' ? 'All Bookings' : sel.kind === 'year' ? sel.value : sel.kind === 'month' ? new Date(sel.value + '-01T00:00:00').toLocaleDateString('en-GB', {month: 'long', year: 'numeric'}) : sel.kind === 'day' ? fmtAllDate(sel.value) : 'All Bookings'; const toggleYear = (y) => setOpenYears({ ...openYears, [y]: !openYears[y] }); const toggleMonth = (mo) => setOpenMonths({ ...openMonths, [mo]: !openMonths[mo] }); return (
{/* Search + actions bar */}
setSearch(e.target.value)} placeholder="Search All Bookings — customer, plate, operator, location..." className="input" style={{height: 42, paddingLeft: 38, width: '100%', fontSize: 14}}/>
{isAdmin ? ( ) : null}
{/* Sidebar: All / Year ▸ Month ▸ Day */} {/* Main table */}
{selLabel}
{filtered.length} {filtered.length === 1 ? 'booking' : 'bookings'}{search.trim() ? ` matching "${search}"` : ''}
Chat
Shift_Start_Date
Category
Machine
Company
Location
Shift
{filtered.length === 0 ? (
No bookings match the current filter.
) : filtered.map(b => { const CatIcon = categoryIcon(b.category); const isMulti = b.multi === true; return (
{ // Open the full Create-Booking form in edit mode so the // detail fields match the create page exactly. const machineId = (b.machine || '').split(' ')[0]; const machine = MACHINES.find(m => m.id === machineId); if (machine) { window.__createPrefill = { editBooking: b }; setPage('create'); } }}>
{fmtAllDate(b.date)}
{isMulti ? Multiple : null}
{b.category}
{b.category === 'Cross Rent' ? ( {(b.machine || '').match(/\d+$/)?.[0] || '#'} {b.machine ? b.machine.replace(/^[A-Z]+-\d+\s+/, '') : ''} ) : ( {bookingMachineLabel(b)} )} {b.operator || '— no operator —'}
{b.customer}
{b.location}
{b.shift === 'morning' ? 'Custom' : b.shift === 'afternoon' ? 'Custom' : 'Custom'}
); })}
{/* Modify dialog (reused from Bookings.jsx) */} {modifyTarget ? ( { if (patch.machineStatus) { const m = MACHINES.find(x => x.id === machineId); if (m) m.status = patch.machineStatus; } if (bookingId) { const bk = BOOKINGS.find(x => x.id === bookingId); if (bk) ['customer','contact','phone','location','start','end','shift'].forEach(k => { if (patch[k] !== undefined) bk[k] = patch[k]; }); } forceRender(); }} applyDelete={(bookingId) => { const i = BOOKINGS.findIndex(x => x.id === bookingId); if (i >= 0) BOOKINGS.splice(i, 1); forceRender(); }} onClose={() => setModifyTarget(null)}/> ) : null}
); } Object.assign(window, { AllBookingsPage });