diff --git a/admin_ui/src/components/FixedScheduleEditor.jsx b/admin_ui/src/components/FixedScheduleEditor.jsx index 9a149e9..4325189 100644 --- a/admin_ui/src/components/FixedScheduleEditor.jsx +++ b/admin_ui/src/components/FixedScheduleEditor.jsx @@ -47,7 +47,18 @@ const parseTimeToMinutes = (time) => { return hours * 60 + minutes } -const buildTimeRange = (start, end, intervalMinutes) => { +const addMinutesToTime = (time, minutesToAdd) => { + const normalized = normalizeTime(time) + if (!normalized || !Number.isFinite(Number(minutesToAdd))) return null + const mins = parseTimeToMinutes(normalized) + if (mins === null) return null + const total = (mins + Number(minutesToAdd)) + const hours = Math.floor((total % (24 * 60)) / 60) + const minutes = (total % 60 + 60) % 60 + return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}` +} + +const buildTimeRange = (start, end, intervalMinutes, allowWrap = false) => { const normalizedStart = normalizeTime(start) const normalizedEnd = normalizeTime(end) const step = Number(intervalMinutes) @@ -57,21 +68,36 @@ const buildTimeRange = (start, end, intervalMinutes) => { } const startMinutes = parseTimeToMinutes(normalizedStart) - const endMinutes = parseTimeToMinutes(normalizedEnd) - if (startMinutes === null || endMinutes === null || endMinutes < startMinutes) { - return [] + let endMinutes = parseTimeToMinutes(normalizedEnd) + if (startMinutes === null || endMinutes === null) return [] + + if (endMinutes < startMinutes) { + if (!allowWrap) return [] + // wrap end to next day + endMinutes += 24 * 60 } const times = [] for (let minute = startMinutes; minute <= endMinutes; minute += step) { - const hours = Math.floor(minute / 60) - const minutes = minute % 60 + const m = minute % (24 * 60) + const hours = Math.floor(m / 60) + const minutes = m % 60 times.push(`${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`) } return times } +const addDaysToDate = (isoDate, days) => { + try { + const d = new Date(isoDate + 'T00:00:00') + d.setUTCDate(d.getUTCDate() + Number(days)) + return d.toISOString().split('T')[0] + } catch { + return isoDate + } +} + const buildCountRange = (start, intervalMinutes, count) => { const normalizedStart = normalizeTime(start) const step = Number(intervalMinutes) @@ -133,6 +159,11 @@ const FixedScheduleEditor = ({ const [batchCount, setBatchCount] = useState(6) const [batchTimes, setBatchTimes] = useState(['08:00']) const [newBatchTime, setNewBatchTime] = useState('') + const [batchWrap, setBatchWrap] = useState(false) + const [appendMode, setAppendMode] = useState(false) + const [presetsOpen, setPresetsOpen] = useState(true) + const [batchOpen, setBatchOpen] = useState(true) + const [templatesOpen, setTemplatesOpen] = useState(true) const [presetName, setPresetName] = useState('') const [savedPresets, setSavedPresets] = useState([]) const [deleteConfirm, setDeleteConfirm] = useState(null) @@ -233,20 +264,31 @@ const FixedScheduleEditor = ({ } const generateBatchTimes = () => { - const generated = buildTimeRange(batchStartTime, batchEndTime, batchInterval) - setBatchTimes(prev => dedupeTimes([...prev, ...generated])) + const generated = buildTimeRange(batchStartTime, batchEndTime, batchInterval, batchWrap) + if (appendMode) { + setBatchTimes(prev => dedupeTimes([...prev, ...generated])) + } else { + setBatchTimes(dedupeTimes(generated)) + } } const generateBatchTimesByCount = () => { const generated = buildCountRange(batchStartTime, batchInterval, batchCount) - setBatchTimes(prev => dedupeTimes([...prev, ...generated])) + if (appendMode) { + setBatchTimes(prev => dedupeTimes([...prev, ...generated])) + } else { + setBatchTimes(dedupeTimes(generated)) + } } const applyPreset = (preset) => { setBatchStartTime(preset.start) setBatchEndTime(preset.end) setBatchInterval(preset.intervalMinutes) - setBatchTimes(buildTimeRange(preset.start, preset.end, preset.intervalMinutes)) + // infer wrap when end < start if preset does not explicitly set wrap + const inferredWrap = preset.batchWrap ?? (parseTimeToMinutes(preset.end) < parseTimeToMinutes(preset.start)) + setBatchWrap(Boolean(inferredWrap)) + setBatchTimes(buildTimeRange(preset.start, preset.end, preset.intervalMinutes, Boolean(inferredWrap))) setScheduleMode('batch') } @@ -288,6 +330,8 @@ const FixedScheduleEditor = ({ setBatchStartTime(preset.batchStartTime || '06:00') setBatchEndTime(preset.batchEndTime || '09:00') setBatchInterval(Number(preset.batchInterval) || 15) + const inferredWrap = preset.batchWrap ?? (parseTimeToMinutes(preset.batchEndTime) < parseTimeToMinutes(preset.batchStartTime)) + setBatchWrap(Boolean(inferredWrap)) setBatchCount(Number(preset.batchCount) || 6) setBatchTimes(dedupeTimes(Array.isArray(preset.batchTimes) && preset.batchTimes.length > 0 ? preset.batchTimes : [preset.singleTime || '08:00'])) setMessage(`Preset geladen: ${preset.name}`) @@ -307,9 +351,14 @@ const FixedScheduleEditor = ({ setSelectedDate(toInputDate(schedule.startTime)) setSingleTime(toInputTime(schedule.startTime)) setSelectedWeekdays(Array.isArray(schedule.weekdays) ? [...schedule.weekdays] : []) - setBatchStartTime(toInputTime(schedule.startTime)) - setBatchEndTime(addMinutesToTime(toInputTime(schedule.startTime), 120) || '09:00') - setBatchTimes([toInputTime(schedule.startTime)]) + const startT = toInputTime(schedule.startTime) + const endT = toInputTime(schedule.endTime) + setBatchStartTime(startT) + setBatchEndTime(addMinutesToTime(startT, 120) || endT || '09:00') + // if template end is earlier than start, assume it wraps to next day + const inferredWrap = parseTimeToMinutes(endT) < parseTimeToMinutes(startT) + setBatchWrap(Boolean(inferredWrap)) + setBatchTimes([startT]) window.scrollTo({ top: 0, behavior: 'smooth' }) } @@ -344,15 +393,24 @@ const FixedScheduleEditor = ({ throw new Error('Voeg minstens één vertrektijd toe voor de reeks') } - const payload = { - schedules: uniqueTimes.map(time => ({ + const startBaseMinutes = parseTimeToMinutes(batchStartTime) + + const schedules = uniqueTimes.map(time => { + const tmins = parseTimeToMinutes(time) + let dateForTime = selectedDate + if (batchWrap && startBaseMinutes !== null && tmins !== null && tmins < startBaseMinutes) { + dateForTime = addDaysToDate(selectedDate, 1) + } + return { busLineId: Number(selectedLine), busId: Number(selectedBus), driverId: driverId ? Number(driverId) : null, - startTime: `${selectedDate}T${time}:00`, + startTime: `${dateForTime}T${time}:00`, weekdays: normalizedWeekdays, - })), - } + } + }) + + const payload = { schedules } const res = await client.post('/admin/schedules/bulk', payload) return { @@ -561,57 +619,75 @@ const FixedScheduleEditor = ({ {!editingScheduleId && ( -
-
- Reusable presets +
+
+ Sla huidige configuratie op en hergebruik hem later
-
- setPresetName(e.target.value)} - placeholder="Preset naam, bijvoorbeeld schooldag-ochtend" - /> - -
-
- {savedPresets.length === 0 ? ( - Nog geen presets opgeslagen - ) : savedPresets.map(preset => ( - - - - - ))} -
+
+
+ {savedPresets.length === 0 ? ( + Nog geen presets opgeslagen + ) : savedPresets.map(preset => ( + + + + + ))} +
+
+ )}
)} {!editingScheduleId && scheduleMode === 'batch' && ( -
+
+
+ +
+ +
+
+ {batchOpen && ( +
@@ -621,6 +697,11 @@ const FixedScheduleEditor = ({ setBatchEndTime(e.target.value)} />
+
+ +
- )} + )} -
+
+ )} + +
{editingScheduleId ? 'Je past één bestaande dienst aan.' @@ -719,71 +803,62 @@ const FixedScheduleEditor = ({ )}

Bestaande vaste diensten

-

- Bewerk of verwijder hieronder bestaande diensten -

-
- {scheduleTemplates.length === 0 ? ( -
Nog geen vaste diensten gevonden.
- ) : ( - scheduleTemplates - .slice() - .sort((a, b) => a.lineNumber - b.lineNumber || new Date(a.startTime) - new Date(b.startTime)) - .map(schedule => ( -
-
- Lijn {schedule.lineNumber} - - {formatTime(schedule.startTime)} - {formatTime(schedule.endTime)} - -
-
-
{schedule.busName}
-
{schedule.licensePlate}
- {schedule.driverName && ( -
- {schedule.driverName} -
- )} -
- {schedule.recurrenceLabel || getWeekdayLabel(schedule.weekdays)} -
-
- - {isAdmin && ( -
- - - -
- )} -
- )) - )} +
+ +
Bewerk of verwijder bestaande diensten
+ {templatesOpen && ( +
+ {scheduleTemplates.length === 0 ? ( +
Nog geen vaste diensten gevonden.
+ ) : ( + scheduleTemplates.slice() + .sort((a, b) => a.lineNumber - b.lineNumber || new Date(a.startTime) - new Date(b.startTime)) + .map(schedule => { + return ( +
+
+ Lijn {schedule.lineNumber} + + {formatTime(schedule.startTime)} - {formatTime(schedule.endTime)} + +
+
+
{schedule.busName}
+
{schedule.licensePlate}
+ {schedule.driverName && ( +
+ {schedule.driverName} +
+ )} +
+ {schedule.recurrenceLabel || getWeekdayLabel(schedule.weekdays)} +
+
- {message &&
{message}
} + {isAdmin && ( +
+ + + +
+ )} +
+ ) + }) + )} +
+ )} + + {message &&
{message}
} {deleteConfirm && (