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,