import React, { useEffect, useState } from 'react' import { transitClient } from '../api' const WEEKDAYS = ['Ma', 'Di', 'Wo', 'Do', 'Vr', 'Za', 'Zo'] 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 [busLines, setBusLines] = useState([]) const [buses, setBuses] = useState([]) const [drivers, setDrivers] = useState([]) const [selectedDriverFilter, setSelectedDriverFilter] = useState('') const [message, setMessage] = useState(null) const [isAdmin] = useState(Boolean(user?.userCode)) const [deleteConfirm, setDeleteConfirm] = useState(null) const [expandedSchedule, setExpandedSchedule] = useState(null) const [routeStopsByLine, setRouteStopsByLine] = useState({}) // Form state const [selectedLine, setSelectedLine] = useState('') const [selectedBus, setSelectedBus] = useState('') const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]) const [startTime, setStartTime] = useState('08:00') const [driverId, setDriverId] = useState('') const [selectedWeekdays, setSelectedWeekdays] = useState([]) const loadSchedules = async () => { try { const res = await client.get('/admin/schedules') setSchedules(res.data.schedules || []) } catch (err) { setMessage('Error loading schedules: ' + (err.response?.data?.error || err.message)) } } const loadBusLines = async () => { try { const res = await client.get('/admin/bus-lines') setBusLines(res.data.busLines || []) } catch (err) { setMessage('Error loading bus lines: ' + (err.response?.data?.error || err.message)) } } const loadBuses = async () => { try { const res = await client.get('/admin/fleet/buses') setBuses(res.data.buses || []) } catch (err) { setMessage('Error loading buses: ' + (err.response?.data?.error || err.message)) } } const loadDrivers = async () => { try { const res = await client.get('/admin/drivers') setDrivers(res.data.drivers || []) } catch (err) { setMessage('Error loading drivers: ' + (err.response?.data?.error || err.message)) } } useEffect(() => { loadSchedules() loadBusLines() loadBuses() loadDrivers() }, []) const createSchedule = async (e) => { e.preventDefault() setMessage(null) if (!selectedLine || !selectedBus || !startTime) { setMessage('Lijn, voertuig en start-tijd zijn verplicht') return } try { const startDateTime = `${selectedDate}T${startTime}:00` const normalizedWeekdays = [...selectedWeekdays].sort((a, b) => a - b) const res = await client.post('/admin/schedules', { busLineId: Number(selectedLine), busId: Number(selectedBus), driverId: driverId ? Number(driverId) : null, startTime: startDateTime, weekdays: normalizedWeekdays, }) const dayLabels = normalizedWeekdays.length > 0 ? normalizedWeekdays.map(d => WEEKDAYS[WEEKDAY_VALUES.indexOf(d)]).join(', ') : 'eenmalig' setMessage(`Dienst aangemaakt: #${res.data.schedule.id} (${dayLabels})`) setSelectedLine('') setSelectedBus('') setStartTime('08:00') setDriverId('') setSelectedWeekdays([]) await loadSchedules() } catch (err) { const errMsg = err.response?.data?.error || err.message setMessage('Error: ' + errMsg) } } const deleteSchedule = async (scheduleId, lineNumber, busName) => { setMessage(null) try { await client.delete(`/admin/schedules/${scheduleId}`) setMessage(`Dienst verwijderd: Lijn ${lineNumber}, ${busName}`) setDeleteConfirm(null) await loadSchedules() } catch (err) { const errMsg = err.response?.data?.error || err.message setMessage('Error: ' + errMsg) } } const toggleWeekday = (dayValue) => { setSelectedWeekdays(prev => prev.includes(dayValue) ? prev.filter(d => d !== dayValue) : [...prev, dayValue] ) } const getWeekdayLabel = (weekdayValues) => { if (!weekdayValues || weekdayValues.length === 0) return 'Eenmalig' if (weekdayValues.length === 7) return 'Dagelijks' const labels = weekdayValues .slice() .sort((a, b) => a - b) .map(w => WEEKDAYS[WEEKDAY_VALUES.indexOf(w)]) return labels.join(', ') } const formatTime = (timeString) => { const date = new Date(timeString) return date.toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' }) } // Fetch route stops for a given line and cache them in state const loadRouteStops = async (busLineId) => { if (!busLineId) return try { const res = await client.get(`/admin/bus-lines/${busLineId}/stops`) const routeStops = res.data.routeStops || [] setRouteStopsByLine(prev => ({ ...prev, [busLineId]: routeStops })) } catch (err) { // ignore silently, message already used elsewhere } } const getCandidateWindow = () => { const line = busLines.find(l => String(l.id) === String(selectedLine)) if (!line || !selectedDate || !startTime) return null const start = new Date(`${selectedDate}T${startTime}:00`) const end = new Date(start) end.setMinutes(end.getMinutes() + (line.estimatedDurationMinutes || 60)) return { start, end } } const hasOverlap = (candidate, existing) => { if (!candidate || !existing?.startTime || !existing?.endTime) return false const cStart = candidate.start.getTime() const cEnd = candidate.end.getTime() const eStart = new Date(existing.startTime).getTime() const eEnd = new Date(existing.endTime).getTime() const timeOverlap = cStart < eEnd && cEnd > eStart if (!timeOverlap) return false const cWeekdays = selectedWeekdays || [] const eWeekdays = Array.isArray(existing.weekdays) ? existing.weekdays : [] if (cWeekdays.length === 0 && eWeekdays.length === 0) { return candidate.start.toDateString() === new Date(existing.startTime).toDateString() } if (cWeekdays.length > 0 && eWeekdays.length > 0) { return cWeekdays.some(d => eWeekdays.includes(d)) } if (cWeekdays.length > 0 && eWeekdays.length === 0) { return cWeekdays.includes(new Date(existing.startTime).getDay()) } return eWeekdays.includes(candidate.start.getDay()) } const candidateWindow = getCandidateWindow() const busyDriverIds = new Set( schedules .filter(s => s.driverId && hasOverlap(candidateWindow, s)) .map(s => Number(s.driverId)) ) const busyBusIds = new Set( schedules .filter(s => s.busId && hasOverlap(candidateWindow, s)) .map(s => Number(s.busId)) ) const scheduleHasDriverConflict = (schedule) => { if (!schedule?.driverId) return false return schedules.some(other => other.id !== schedule.id && Number(other.driverId) === Number(schedule.driverId) && hasOverlap({ start: new Date(schedule.startTime), end: new Date(schedule.endTime) }, other) ) } const scheduleHasBusConflict = (schedule) => { if (!schedule?.busId) return false return schedules.some(other => other.id !== schedule.id && Number(other.busId) === Number(schedule.busId) && hasOverlap({ start: new Date(schedule.startTime), end: new Date(schedule.endTime) }, other) ) } // Group schedules by date and time (apply driver filter if selected) const timeSlots = generateTimeSlots() const filteredSchedules = selectedDriverFilter ? schedules.filter(s => String(s.driverId) === String(selectedDriverFilter)) : schedules const schedulesBySlot = groupSchedulesByTimeSlot(filteredSchedules, timeSlots) return (

Diensten Inplannen

Maak diensten aan met automatische eindtijd, vertrektijden per halte en herhaald rooster.

{isAdmin && (
setSelectedDate(e.target.value)} /> setStartTime(e.target.value)} placeholder="Start time" />
{WEEKDAYS.map((day, idx) => ( ))}
)}

Timetable

{timeSlots.map((slot, idx) => ( ))}
Tijdvak Lijnen en Voertuigen
{slot}
{schedulesBySlot[slot]?.map(schedule => (
Lijn {schedule.lineNumber} {formatTime(schedule.startTime)} - {formatTime(schedule.endTime)}
{schedule.busName}
{schedule.licensePlate}
{schedule.driverName && (
{schedule.driverName}
)} {scheduleHasDriverConflict(schedule) && (
Chauffeur in gebruik
)} {scheduleHasBusConflict(schedule) && (
Voertuig in gebruik
)} {schedule.weekdays && schedule.weekdays.length > 0 && (
{getWeekdayLabel(schedule.weekdays)}
)}
{/* Expandable stops list */} {expandedSchedule === schedule.id && (
{(() => { const lineId = schedule.busLineId const cached = routeStopsByLine[lineId] if (!cached) { // Load and show loading text loadRouteStops(lineId) return

Laden haltes...

} if (cached.length === 0) { return

Geen haltes gekoppeld

} // Render editable list of stops with ETA inputs return ( {cached.map(rs => { const departureDate = new Date(schedule.startTime) departureDate.setMinutes(departureDate.getMinutes() + (rs.estimatedArrivalMinutes || 0)) return ( ) })}
{rs.stopOrder}. {rs.name} {formatTime(departureDate.toISOString())}
{ const val = Number(e.target.value) setRouteStopsByLine(prev => ({ ...prev, [lineId]: prev[lineId].map(item => item.id === rs.id ? { ...item, estimatedArrivalMinutes: val } : item) })) }} style={{ width: '8rem' }} /> { const val = e.target.value === '' ? 0 : Number(e.target.value) setRouteStopsByLine(prev => ({ ...prev, [lineId]: prev[lineId].map(item => item.id === rs.id ? { ...item, estimatedArrivalMinutes: val } : item) })) }} style={{ width: '4.5rem' }} />
) })()}
)} {isAdmin && ( )}
))}
{message &&
{message}
} {deleteConfirm && (

Delete Service?

Lijn {deleteConfirm.lineNumber} - {deleteConfirm.busName} ({deleteConfirm.licensePlate})

{new Date(deleteConfirm.startTime).toLocaleString()}

)}
) } function generateTimeSlots() { const slots = [] for (let hour = 0; hour < 24; hour++) { slots.push(`${String(hour).padStart(2, '0')}:00-${String((hour + 1) % 24).padStart(2, '0')}:00`) } return slots } function groupSchedulesByTimeSlot(schedules, slots) { const grouped = {} slots.forEach(slot => { grouped[slot] = [] }) schedules.forEach(schedule => { const startHour = new Date(schedule.startTime).getHours() const slot = `${String(startHour).padStart(2, '0')}:00-${String((startHour + 1) % 24).padStart(2, '0')}:00` if (grouped[slot]) { grouped[slot].push(schedule) } }) return grouped } function formatTime(timeString) { const date = new Date(timeString) return date.toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' }) } const overlay = { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, } const modal = { backgroundColor: '#fff', padding: '2rem', borderRadius: '8px', maxWidth: '400px', boxShadow: '0 4px 12px rgba(0,0,0,0.15)', }