From 27880b23558972d90b2ce61188da8f43e2661e5d Mon Sep 17 00:00:00 2001 From: Sven laptop Date: Tue, 12 May 2026 11:36:47 +0200 Subject: [PATCH] Add overview schedule loading and filtering functionality; implement schedule materialization for date ranges --- admin_ui/src/components/ScheduleMaker.jsx | 43 +++++++++------- transit_api/index.js | 62 ++++++++++++++++++++++- 2 files changed, 86 insertions(+), 19 deletions(-) diff --git a/admin_ui/src/components/ScheduleMaker.jsx b/admin_ui/src/components/ScheduleMaker.jsx index 0afdc47..08111f5 100644 --- a/admin_ui/src/components/ScheduleMaker.jsx +++ b/admin_ui/src/components/ScheduleMaker.jsx @@ -7,6 +7,7 @@ const WEEKDAY_VALUES = [1, 2, 3, 4, 5, 6, 0] // 0=Sun, 1=Mon... export default function ScheduleMaker({ token, user }) { const client = transitClient(token) const [schedules, setSchedules] = useState([]) + const [overviewSchedules, setOverviewSchedules] = useState([]) const [busLines, setBusLines] = useState([]) const [buses, setBuses] = useState([]) const [drivers, setDrivers] = useState([]) @@ -35,6 +36,20 @@ export default function ScheduleMaker({ token, user }) { } } + const loadOverviewSchedules = async () => { + try { + const params = new URLSearchParams({ date: overviewDate }) + if (selectedDriverFilter) { + params.set('driverId', selectedDriverFilter) + } + + const res = await client.get(`/schedules/overview?${params.toString()}`) + setOverviewSchedules(res.data.schedules || []) + } catch (err) { + setMessage('Error loading timetable: ' + (err.response?.data?.error || err.message)) + } + } + const loadBusLines = async () => { try { const res = await client.get('/admin/bus-lines') @@ -69,6 +84,10 @@ export default function ScheduleMaker({ token, user }) { loadDrivers() }, []) + useEffect(() => { + loadOverviewSchedules() + }, [overviewDate, selectedDriverFilter]) + const createSchedule = async (e) => { e.preventDefault() setMessage(null) @@ -100,6 +119,7 @@ export default function ScheduleMaker({ token, user }) { setDriverId('') setSelectedWeekdays([]) await loadSchedules() + await loadOverviewSchedules() } catch (err) { const errMsg = err.response?.data?.error || err.message setMessage('Error: ' + errMsg) @@ -113,6 +133,7 @@ export default function ScheduleMaker({ token, user }) { setMessage(`Dienst verwijderd: Lijn ${lineNumber}, ${busName}`) setDeleteConfirm(null) await loadSchedules() + await loadOverviewSchedules() } catch (err) { const errMsg = err.response?.data?.error || err.message setMessage('Error: ' + errMsg) @@ -222,19 +243,8 @@ export default function ScheduleMaker({ token, user }) { ) } - // Group schedules by date and time (apply driver filter if selected) + // Group schedules by date and time using the API-backed overview const timeSlots = generateTimeSlots() - const filteredSchedules = selectedDriverFilter - ? schedules.filter(s => String(s.driverId) === String(selectedDriverFilter)) - : schedules - const overviewSchedules = filteredSchedules.filter(schedule => { - if (!overviewDate) return true - const scheduleDate = new Date(schedule.startTime) - const selectedOverview = new Date(`${overviewDate}T00:00:00`) - return scheduleDate.getFullYear() === selectedOverview.getFullYear() - && scheduleDate.getMonth() === selectedOverview.getMonth() - && scheduleDate.getDate() === selectedOverview.getDate() - }) const schedulesBySlot = groupSchedulesByTimeSlot(overviewSchedules, timeSlots) return ( @@ -389,11 +399,9 @@ export default function ScheduleMaker({ token, user }) { Voertuig in gebruik )} - {schedule.weekdays && schedule.weekdays.length > 0 && ( -
- {getWeekdayLabel(schedule.weekdays)} -
- )} +
+ {schedule.recurrenceLabel || getWeekdayLabel(schedule.weekdays)} +
{/* Expandable stops list */} @@ -481,6 +489,7 @@ export default function ScheduleMaker({ token, user }) { setMessage('ETA updated') await loadRouteStops(lineId) await loadSchedules() + await loadOverviewSchedules() } catch (err) { setMessage('Error: ' + (err.response?.data?.error || err.message)) } diff --git a/transit_api/index.js b/transit_api/index.js index 4aec5cf..5892fdf 100644 --- a/transit_api/index.js +++ b/transit_api/index.js @@ -425,6 +425,58 @@ function mapRecurringOccurrenceRows(rows, dateKey, routeStopsByLine) { .sort((a, b) => new Date(a.startTime) - new Date(b.startTime)); } +function materializeScheduleRowsInRange(rows, fromDateKey, toDateKey, routeStopsByLine) { + const occurrences = []; + if (!fromDateKey || !toDateKey) return occurrences; + + const fromDate = new Date(`${fromDateKey}T00:00:00`); + const toDate = new Date(`${toDateKey}T23:59:59`); + + // Safety cap: don't generate more than 366 days + const maxDays = 366; + const spanDays = Math.ceil((toDate - fromDate) / (24 * 60 * 60 * 1000)); + if (spanDays > maxDays) return occurrences; + + for (const row of rows) { + const weekdays = parseWeekdaysValue(row.weekdays); + + if (!weekdays || weekdays.length === 0) { + const rowDateKey = toDateKey(row.start_time ?? row.startTime); + if (rowDateKey && rowDateKey >= fromDateKey && rowDateKey <= toDateKey) { + occurrences.push(materializeScheduleOverviewRow(row, rowDateKey, routeStopsByLine)); + } + continue; + } + + // recurring template: generate occurrences per weekday within range + const templateStart = new Date(row.start_time ?? row.startTime); + for (const wd of [...new Set(weekdays)]) { + // find first date >= fromDate that matches weekday wd and is >= templateStart + let cursor = new Date(fromDate); + const fromWeekday = cursor.getDay(); + const daysOffset = (wd - fromWeekday + 7) % 7; + cursor.setDate(cursor.getDate() + daysOffset); + + // ensure cursor >= templateStart (so recurrence doesn't start before template) + if (cursor < templateStart) { + // advance by weeks until >= templateStart + const diffDays = Math.ceil((templateStart - cursor) / (24 * 60 * 60 * 1000)); + const weeksToAdvance = Math.ceil(diffDays / 7); + cursor.setDate(cursor.getDate() + weeksToAdvance * 7); + } + + while (cursor <= toDate) { + const occurrenceKey = cursor.toISOString().slice(0, 10); + occurrences.push(materializeScheduleOverviewRow(row, occurrenceKey, routeStopsByLine)); + cursor.setDate(cursor.getDate() + 7); + } + } + } + + // sort and return + return occurrences.sort((a, b) => new Date(a.startTime) - new Date(b.startTime)); +} + /** * Check if two schedules have date/weekday overlap */ @@ -2439,8 +2491,14 @@ app.get('/schedules/overview', async (req, res) => { } } - const schedules = mapRecurringOccurrenceRows(result.rows, overviewDateKey, routeStopsByLine) - .filter(row => scheduleMatchesFilters(row, filters, routeStopsByLine)); + let schedules = []; + if (filters.fromDate && filters.toDate) { + schedules = materializeScheduleRowsInRange(result.rows, filters.fromDate, filters.toDate, routeStopsByLine) + .filter(row => scheduleMatchesFilters(row, filters, routeStopsByLine)); + } else { + schedules = mapRecurringOccurrenceRows(result.rows, overviewDateKey, routeStopsByLine) + .filter(row => scheduleMatchesFilters(row, filters, routeStopsByLine)); + } res.json({ date: overviewDateKey,