Add edit and delete functionality for buses and stops in admin panel
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user