Enhance FixedScheduleEditor; add time manipulation functions, support for batch wrapping, and improve preset management

This commit is contained in:
Sven laptop
2026-05-14 01:04:50 +02:00
parent b04dfba4c7
commit ad48e79db2
+120 -45
View File
@@ -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)
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)
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,12 +619,16 @@ const FixedScheduleEditor = ({
</div>
{!editingScheduleId && (
<div style={{ marginBottom: '1rem', padding: '1rem', border: '1px solid #e5e7eb', borderRadius: '12px', background: '#fbfdff' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '1rem', flexWrap: 'wrap' }}>
<strong>Reusable presets</strong>
<div style={{ marginBottom: '1rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<button type="button" onClick={() => setPresetsOpen(p => !p)} style={{ background: 'transparent', border: 'none', cursor: 'pointer', fontSize: '1rem' }}>
{presetsOpen ? '▾' : '▸'} Reusable presets
</button>
<span style={{ color: '#666', fontSize: '0.9rem' }}>Sla huidige configuratie op en hergebruik hem later</span>
</div>
<div className="form-row" style={{ marginTop: '0.75rem' }}>
{presetsOpen && (
<div style={{ marginTop: '0.5rem', padding: '1rem', border: '1px solid #e5e7eb', borderRadius: '12px', background: '#fbfdff' }}>
<div className="form-row" style={{ marginTop: '0.25rem' }}>
<input
type="text"
value={presetName}
@@ -609,9 +671,23 @@ const FixedScheduleEditor = ({
</div>
</div>
)}
</div>
)}
{!editingScheduleId && scheduleMode === 'batch' && (
<div style={{ marginBottom: '1rem', padding: '1rem', border: '1px solid #e5e7eb', borderRadius: '12px', background: '#fafafa' }}>
<div style={{ marginBottom: '1rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<button type="button" onClick={() => setBatchOpen(b => !b)} style={{ background: 'transparent', border: 'none', cursor: 'pointer', fontSize: '1rem' }}>
{batchOpen ? '▾' : '▸'} Dienstenreeks opties
</button>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<label style={{ display: 'flex', gap: '0.25rem', alignItems: 'center', fontSize: '0.9rem' }}>
<input type="checkbox" checked={appendMode} onChange={e => setAppendMode(e.target.checked)} /> Voeg toe
</label>
</div>
</div>
{batchOpen && (
<div style={{ marginTop: '0.5rem', padding: '1rem', border: '1px solid #e5e7eb', borderRadius: '12px', background: '#fafafa' }}>
<div className="form-row" style={{ alignItems: 'end' }}>
<div>
<label style={{ display: 'block', marginBottom: '0.35rem', fontWeight: 500 }}>Van</label>
@@ -621,6 +697,11 @@ const FixedScheduleEditor = ({
<label style={{ display: 'block', marginBottom: '0.35rem', fontWeight: 500 }}>Tot</label>
<input type="time" value={batchEndTime} onChange={e => setBatchEndTime(e.target.value)} />
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<label style={{ display: 'flex', gap: '0.25rem', alignItems: 'center', fontSize: '0.9rem' }}>
<input type="checkbox" checked={batchWrap} onChange={e => setBatchWrap(e.target.checked)} /> Over middernacht
</label>
</div>
<div>
<label style={{ display: 'block', marginBottom: '0.35rem', fontWeight: 500 }}>Interval</label>
<input
@@ -699,6 +780,9 @@ const FixedScheduleEditor = ({
</div>
)}
</div>
)}
<div className="form-row" style={{ alignItems: 'center', justifyContent: 'space-between', gap: '1rem' }}>
<div style={{ color: '#666', fontSize: '0.9rem' }}>
{editingScheduleId
@@ -719,17 +803,21 @@ const FixedScheduleEditor = ({
)}
<h4 style={{ marginTop: '2rem', marginBottom: '0.5rem' }}>Bestaande vaste diensten</h4>
<p style={{ margin: 0, color: '#666', fontSize: '0.9rem', marginBottom: '1rem' }}>
Bewerk of verwijder hieronder bestaande diensten
</p>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<button type="button" onClick={() => setTemplatesOpen(t => !t)} style={{ background: 'transparent', border: 'none', cursor: 'pointer', fontSize: '1rem' }}>
{templatesOpen ? '▾' : '▸'} Bestaande vaste diensten
</button>
<div style={{ color: '#666', fontSize: '0.9rem' }}>Bewerk of verwijder bestaande diensten</div>
</div>
{templatesOpen && (
<div style={{ display: 'grid', gap: '1rem' }}>
{scheduleTemplates.length === 0 ? (
<div className="message">Nog geen vaste diensten gevonden.</div>
) : (
scheduleTemplates
.slice()
scheduleTemplates.slice()
.sort((a, b) => a.lineNumber - b.lineNumber || new Date(a.startTime) - new Date(b.startTime))
.map(schedule => (
.map(schedule => {
return (
<article key={`template-${schedule.id}`} className="schedule-card" style={{ position: 'relative' }}>
<div className="schedule-header">
<strong>Lijn {schedule.lineNumber}</strong>
@@ -752,36 +840,23 @@ const FixedScheduleEditor = ({
{isAdmin && (
<div className="form-row" style={{ marginTop: '0.75rem', gap: '0.5rem' }}>
<button
type="button"
className="secondary"
onClick={() => startEditingSchedule(schedule)}
style={{ flex: 1 }}
>
<button type="button" className="secondary" onClick={() => startEditingSchedule(schedule)} style={{ flex: 1 }}>
Bewerken
</button>
<button
type="button"
className="secondary"
onClick={() => cloneTemplateIntoForm(schedule)}
style={{ flex: 1 }}
>
<button type="button" className="secondary" onClick={() => cloneTemplateIntoForm(schedule)} style={{ flex: 1 }}>
Klonen
</button>
<button
type="button"
className="danger"
onClick={() => setDeleteConfirm(schedule)}
style={{ flex: 1 }}
>
<button type="button" className="danger" onClick={() => setDeleteConfirm(schedule)} style={{ flex: 1 }}>
Verwijderen
</button>
</div>
)}
</article>
))
)
})
)}
</div>
)}
{message && <div className="message">{message}</div>}