Add overview schedule loading and filtering functionality; implement schedule materialization for date ranges
This commit is contained in:
@@ -7,6 +7,7 @@ const WEEKDAY_VALUES = [1, 2, 3, 4, 5, 6, 0] // 0=Sun, 1=Mon...
|
|||||||
export default function ScheduleMaker({ token, user }) {
|
export default function ScheduleMaker({ token, user }) {
|
||||||
const client = transitClient(token)
|
const client = transitClient(token)
|
||||||
const [schedules, setSchedules] = useState([])
|
const [schedules, setSchedules] = useState([])
|
||||||
|
const [overviewSchedules, setOverviewSchedules] = useState([])
|
||||||
const [busLines, setBusLines] = useState([])
|
const [busLines, setBusLines] = useState([])
|
||||||
const [buses, setBuses] = useState([])
|
const [buses, setBuses] = useState([])
|
||||||
const [drivers, setDrivers] = 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 () => {
|
const loadBusLines = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await client.get('/admin/bus-lines')
|
const res = await client.get('/admin/bus-lines')
|
||||||
@@ -69,6 +84,10 @@ export default function ScheduleMaker({ token, user }) {
|
|||||||
loadDrivers()
|
loadDrivers()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadOverviewSchedules()
|
||||||
|
}, [overviewDate, selectedDriverFilter])
|
||||||
|
|
||||||
const createSchedule = async (e) => {
|
const createSchedule = async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setMessage(null)
|
setMessage(null)
|
||||||
@@ -100,6 +119,7 @@ export default function ScheduleMaker({ token, user }) {
|
|||||||
setDriverId('')
|
setDriverId('')
|
||||||
setSelectedWeekdays([])
|
setSelectedWeekdays([])
|
||||||
await loadSchedules()
|
await loadSchedules()
|
||||||
|
await loadOverviewSchedules()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errMsg = err.response?.data?.error || err.message
|
const errMsg = err.response?.data?.error || err.message
|
||||||
setMessage('Error: ' + errMsg)
|
setMessage('Error: ' + errMsg)
|
||||||
@@ -113,6 +133,7 @@ export default function ScheduleMaker({ token, user }) {
|
|||||||
setMessage(`Dienst verwijderd: Lijn ${lineNumber}, ${busName}`)
|
setMessage(`Dienst verwijderd: Lijn ${lineNumber}, ${busName}`)
|
||||||
setDeleteConfirm(null)
|
setDeleteConfirm(null)
|
||||||
await loadSchedules()
|
await loadSchedules()
|
||||||
|
await loadOverviewSchedules()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errMsg = err.response?.data?.error || err.message
|
const errMsg = err.response?.data?.error || err.message
|
||||||
setMessage('Error: ' + errMsg)
|
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 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)
|
const schedulesBySlot = groupSchedulesByTimeSlot(overviewSchedules, timeSlots)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -389,11 +399,9 @@ export default function ScheduleMaker({ token, user }) {
|
|||||||
Voertuig in gebruik
|
Voertuig in gebruik
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{schedule.weekdays && schedule.weekdays.length > 0 && (
|
|
||||||
<div style={{ fontSize: '0.85rem', color: '#666', marginTop: '0.25rem' }}>
|
<div style={{ fontSize: '0.85rem', color: '#666', marginTop: '0.25rem' }}>
|
||||||
{getWeekdayLabel(schedule.weekdays)}
|
{schedule.recurrenceLabel || getWeekdayLabel(schedule.weekdays)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expandable stops list */}
|
{/* Expandable stops list */}
|
||||||
@@ -481,6 +489,7 @@ export default function ScheduleMaker({ token, user }) {
|
|||||||
setMessage('ETA updated')
|
setMessage('ETA updated')
|
||||||
await loadRouteStops(lineId)
|
await loadRouteStops(lineId)
|
||||||
await loadSchedules()
|
await loadSchedules()
|
||||||
|
await loadOverviewSchedules()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setMessage('Error: ' + (err.response?.data?.error || err.message))
|
setMessage('Error: ' + (err.response?.data?.error || err.message))
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-1
@@ -425,6 +425,58 @@ function mapRecurringOccurrenceRows(rows, dateKey, routeStopsByLine) {
|
|||||||
.sort((a, b) => new Date(a.startTime) - new Date(b.startTime));
|
.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
|
* 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)
|
let schedules = [];
|
||||||
|
if (filters.fromDate && filters.toDate) {
|
||||||
|
schedules = materializeScheduleRowsInRange(result.rows, filters.fromDate, filters.toDate, routeStopsByLine)
|
||||||
.filter(row => scheduleMatchesFilters(row, filters, routeStopsByLine));
|
.filter(row => scheduleMatchesFilters(row, filters, routeStopsByLine));
|
||||||
|
} else {
|
||||||
|
schedules = mapRecurringOccurrenceRows(result.rows, overviewDateKey, routeStopsByLine)
|
||||||
|
.filter(row => scheduleMatchesFilters(row, filters, routeStopsByLine));
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
date: overviewDateKey,
|
date: overviewDateKey,
|
||||||
|
|||||||
Reference in New Issue
Block a user