Add edit and delete functionality for buses and stops in admin panel

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Sven laptop
2026-05-07 16:16:52 +02:00
parent 31ed605cd3
commit 9ea3c9d5df
7 changed files with 511 additions and 48 deletions
+86 -11
View File
@@ -13,6 +13,7 @@ export default function BusLinesManager({ token, user }) {
const [message, setMessage] = useState(null)
const [isAdmin] = useState(Boolean(user?.userCode))
const [deleteConfirm, setDeleteConfirm] = useState(null)
const [editingLine, setEditingLine] = useState(null)
const loadBusLines = async () => {
try {
@@ -41,10 +42,14 @@ export default function BusLinesManager({ token, user }) {
e.preventDefault()
setMessage(null)
try {
// Map selected stop ids to names (server currently stores names)
const startStopName = stops.find(s => String(s.id) === String(startStop))?.name || startStop
const endStopName = stops.find(s => String(s.id) === String(endStop))?.name || endStop
const res = await client.post('/admin/bus-lines', {
lineNumber: Number(lineNumber),
startStop,
endStop,
startStop: startStopName,
endStop: endStopName,
estimatedDurationMinutes: Number(estimatedDurationMinutes),
description,
})
@@ -91,13 +96,29 @@ export default function BusLinesManager({ token, user }) {
<div>{line.estimatedDurationMinutes} min</div>
{line.description ? <div className="muted">{line.description}</div> : null}
{isAdmin && (
<button
className="danger"
onClick={() => setDeleteConfirm(line)}
style={{ marginTop: '0.5rem', width: '100%' }}
>
Delete
</button>
<>
<button
className="danger"
onClick={() => setDeleteConfirm(line)}
style={{ marginTop: '0.5rem', width: '100%' }}
>
Delete
</button>
<button
className="secondary"
onClick={() => setEditingLine({
id: line.id,
lineNumber: line.lineNumber,
startStop: (stops.find(s => s.name === line.startStop) || {}).id || line.startStop,
endStop: (stops.find(s => s.name === line.endStop) || {}).id || line.endStop,
estimatedDurationMinutes: line.estimatedDurationMinutes,
description: line.description || '',
})}
style={{ marginTop: '0.5rem', width: '100%' }}
>
Edit
</button>
</>
)}
</article>
))}
@@ -110,13 +131,13 @@ export default function BusLinesManager({ token, user }) {
<select value={startStop} onChange={e => setStartStop(e.target.value)}>
<option value="">Start stop</option>
{stops.map(stop => (
<option key={stop.id} value={stop.name}>#{stop.id} {stop.name}</option>
<option key={stop.id} value={stop.id}>#{stop.id} {stop.name}</option>
))}
</select>
<select value={endStop} onChange={e => setEndStop(e.target.value)}>
<option value="">End stop</option>
{stops.map(stop => (
<option key={stop.id} value={stop.name}>#{stop.id} {stop.name}</option>
<option key={stop.id} value={stop.id}>#{stop.id} {stop.name}</option>
))}
</select>
</div>
@@ -130,6 +151,60 @@ export default function BusLinesManager({ token, user }) {
{message && <div className="message">{message}</div>}
{editingLine && (
<div style={overlay}>
<div style={modal}>
<h3>Edit Bus Line</h3>
<form onSubmit={async (e) => {
e.preventDefault()
setMessage(null)
try {
const startStopName = stops.find(s => String(s.id) === String(editingLine.startStop))?.name || editingLine.startStop
const endStopName = stops.find(s => String(s.id) === String(editingLine.endStop))?.name || editingLine.endStop
await client.patch(`/admin/bus-lines/${editingLine.id}`, {
lineNumber: Number(editingLine.lineNumber),
startStop: startStopName,
endStop: endStopName,
estimatedDurationMinutes: Number(editingLine.estimatedDurationMinutes),
description: editingLine.description,
})
setMessage('Bus line updated')
setEditingLine(null)
await loadBusLines()
} catch (err) {
setMessage('Error: ' + (err.response?.data?.error || err.message))
}
}} className="stack-form">
<div className="form-row">
<input value={editingLine.lineNumber} onChange={e => setEditingLine({...editingLine, lineNumber: e.target.value})} />
<select value={editingLine.startStop} onChange={e => setEditingLine({...editingLine, startStop: e.target.value})}>
<option value="">Start stop</option>
{stops.map(stop => (
<option key={stop.id} value={stop.id}>#{stop.id} {stop.name}</option>
))}
</select>
<select value={editingLine.endStop} onChange={e => setEditingLine({...editingLine, endStop: e.target.value})}>
<option value="">End stop</option>
{stops.map(stop => (
<option key={stop.id} value={stop.id}>#{stop.id} {stop.name}</option>
))}
</select>
</div>
<div className="form-row">
<input value={editingLine.estimatedDurationMinutes} onChange={e => setEditingLine({...editingLine, estimatedDurationMinutes: e.target.value})} />
<input value={editingLine.description} onChange={e => setEditingLine({...editingLine, description: e.target.value})} />
</div>
<div className="form-row" style={{ marginTop: '1rem' }}>
<button className="secondary" type="button" onClick={() => setEditingLine(null)}>Cancel</button>
<button type="submit">Save</button>
</div>
</form>
</div>
</div>
)}
{deleteConfirm && (
<div style={overlay}>
<div style={modal}>
+64
View File
@@ -10,6 +10,8 @@ export default function FleetManager({ token, user }) {
const [message, setMessage] = useState(null)
const [canCreateBus, setCanCreateBus] = useState(true)
const [isAdmin] = useState(Boolean(user?.userCode))
const [editingBus, setEditingBus] = useState(null)
const [deleteConfirm, setDeleteConfirm] = useState(null)
const loadBuses = async () => {
try {
@@ -65,6 +67,12 @@ export default function FleetManager({ token, user }) {
<strong>#{bus.id} {bus.name}</strong>
<div>Seats: {bus.seatCapacity}</div>
<div>Plate: {bus.licensePlate}</div>
{isAdmin && (
<>
<button className="secondary" style={{ marginTop: '0.5rem', width: '100%' }} onClick={() => setEditingBus(bus)}>Edit</button>
<button className="danger" style={{ marginTop: '0.5rem', width: '100%' }} onClick={() => setDeleteConfirm(bus)}>Delete</button>
</>
)}
</article>
))}
</div>
@@ -83,6 +91,62 @@ export default function FleetManager({ token, user }) {
)}
{message && <div className="message">{message}</div>}
{editingBus && (
<div style={overlay}>
<div style={modal}>
<h3>Edit Bus</h3>
<form onSubmit={async (e) => {
e.preventDefault()
setMessage(null)
try {
await client.patch(`/admin/fleet/buses/${editingBus.id}`, {
name: editingBus.name,
seatCapacity: Number(editingBus.seatCapacity),
licensePlate: editingBus.licensePlate,
})
setMessage('Bus updated')
setEditingBus(null)
await loadBuses()
} catch (err) {
setMessage('Error: ' + (err.response?.data?.error || err.message))
}
}} className="stack-form">
<div className="form-row">
<input value={editingBus.name} onChange={e => setEditingBus({...editingBus, name: e.target.value})} />
<input value={editingBus.seatCapacity} onChange={e => setEditingBus({...editingBus, seatCapacity: e.target.value})} />
<input value={editingBus.licensePlate} onChange={e => setEditingBus({...editingBus, licensePlate: e.target.value})} />
</div>
<div className="form-row" style={{ marginTop: '1rem' }}>
<button className="secondary" type="button" onClick={() => setEditingBus(null)}>Cancel</button>
<button type="submit">Save</button>
</div>
</form>
</div>
</div>
)}
{deleteConfirm && (
<div style={overlay}>
<div style={modal}>
<h3>Delete Bus?</h3>
<p><strong>{deleteConfirm.name}</strong></p>
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1rem' }}>
<button className="secondary" onClick={() => setDeleteConfirm(null)}>Cancel</button>
<button className="danger" onClick={async () => {
try {
await client.delete(`/admin/fleet/buses/${deleteConfirm.id}`)
setMessage('Bus deleted')
setDeleteConfirm(null)
await loadBuses()
} catch (err) {
setMessage('Error: ' + (err.response?.data?.error || err.message))
}
}}>Delete</button>
</div>
</div>
</div>
)}
</section>
)
}
+74 -19
View File
@@ -15,6 +15,7 @@ export default function ScheduleMaker({ token, user }) {
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('')
@@ -138,6 +139,18 @@ export default function ScheduleMaker({ token, user }) {
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
@@ -385,25 +398,67 @@ export default function ScheduleMaker({ token, user }) {
{expandedSchedule === schedule.id && (
<div style={{ marginTop: '0.5rem', paddingTop: '0.5rem', borderTop: '1px solid #eee' }}>
{schedule.departureTimes && schedule.departureTimes.length > 0 ? (
<table style={{ width: '100%', fontSize: '0.8rem' }}>
<tbody>
{schedule.departureTimes.map(stop => (
<tr key={stop.stopId}>
<td style={{ paddingRight: '0.5rem' }}>
<strong>{stop.order}.</strong>
</td>
<td style={{ flex: 1 }}>{stop.stopName}</td>
<td style={{ textAlign: 'right', color: '#666' }}>
{formatTime(stop.departureTime)}
</td>
</tr>
))}
</tbody>
</table>
) : (
<p style={{ fontSize: '0.85rem', color: '#999' }}>Geen haltes gekoppeld</p>
)}
{(() => {
const lineId = schedule.busLineId
const cached = routeStopsByLine[lineId]
if (!cached) {
// Load and show loading text
loadRouteStops(lineId)
return <p style={{ fontSize: '0.85rem', color: '#999' }}>Laden haltes...</p>
}
if (cached.length === 0) {
return <p style={{ fontSize: '0.85rem', color: '#999' }}>Geen haltes gekoppeld</p>
}
// Render editable list of stops with ETA inputs
return (
<table style={{ width: '100%', fontSize: '0.8rem' }}>
<tbody>
{cached.map(rs => {
const departureDate = new Date(schedule.startTime)
departureDate.setMinutes(departureDate.getMinutes() + (rs.estimatedArrivalMinutes || 0))
return (
<tr key={rs.id}>
<td style={{ paddingRight: '0.5rem' }}><strong>{rs.stopOrder}.</strong></td>
<td style={{ flex: 1 }}>{rs.name}</td>
<td style={{ textAlign: 'right', color: '#666' }}>{formatTime(departureDate.toISOString())}</td>
<td style={{ paddingLeft: '0.5rem' }}>
<input
type="number"
value={rs.estimatedArrivalMinutes ?? 0}
onChange={e => {
const val = e.target.value === '' ? '' : Number(e.target.value)
setRouteStopsByLine(prev => ({
...prev,
[lineId]: prev[lineId].map(item => item.id === rs.id ? { ...item, estimatedArrivalMinutes: val } : item)
}))
}}
style={{ width: '5rem' }}
/>
<button type="button" onClick={async () => {
const newVal = Number((routeStopsByLine[lineId].find(i => i.id === rs.id)?.estimatedArrivalMinutes) || 0)
try {
await client.post(`/admin/bus-lines/${lineId}/stops`, {
stopId: rs.stopId,
stopOrder: rs.stopOrder,
estimatedArrivalMinutes: newVal,
})
setMessage('ETA updated')
await loadRouteStops(lineId)
await loadSchedules()
} catch (err) {
setMessage('Error: ' + (err.response?.data?.error || err.message))
}
}} style={{ marginLeft: '0.5rem' }}>Save</button>
</td>
</tr>
)
})}
</tbody>
</table>
)
})()}
</div>
)}
+45
View File
@@ -10,6 +10,8 @@ export default function StopsManager({ token, user }) {
const [message, setMessage] = useState(null)
const [isAdmin] = useState(Boolean(user?.userCode))
const [deleteConfirm, setDeleteConfirm] = useState(null)
const [editingStop, setEditingStop] = useState(null)
const loadStops = async () => {
try {
@@ -84,6 +86,15 @@ export default function StopsManager({ token, user }) {
Delete
</button>
)}
{isAdmin && (
<button
className="secondary"
onClick={() => setEditingStop(stop)}
style={{ marginTop: '0.5rem', width: '100%' }}
>
Edit
</button>
)}
</article>
))}
</div>
@@ -117,6 +128,40 @@ export default function StopsManager({ token, user }) {
</div>
</div>
)}
{editingStop && (
<div style={overlay}>
<div style={modal}>
<h3>Edit Stop</h3>
<form onSubmit={async (e) => {
e.preventDefault()
setMessage(null)
try {
const res = await client.patch(`/admin/stops/${editingStop.id}`, {
name: editingStop.name,
latitude: parseFloat(editingStop.latitude),
longitude: parseFloat(editingStop.longitude),
})
setMessage('Stop updated')
setEditingStop(null)
await loadStops()
} catch (err) {
setMessage('Error: ' + (err.response?.data?.error || err.message))
}
}} className="stack-form">
<div className="form-row">
<input value={editingStop.name} onChange={e => setEditingStop({...editingStop, name: e.target.value})} />
<input value={editingStop.latitude} onChange={e => setEditingStop({...editingStop, latitude: e.target.value})} />
<input value={editingStop.longitude} onChange={e => setEditingStop({...editingStop, longitude: e.target.value})} />
</div>
<div className="form-row" style={{ marginTop: '1rem' }}>
<button className="secondary" type="button" onClick={() => setEditingStop(null)}>Cancel</button>
<button type="submit">Save</button>
</div>
</form>
</div>
</div>
)}
</section>
)
}