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>
)
}
@@ -11,10 +11,12 @@ import '../services/schedule_service.dart';
class NavigationScreen extends StatefulWidget {
final schedule.Route route;
final List<NavigationPoint>? initialStops;
const NavigationScreen({
super.key,
required this.route,
this.initialStops,
});
@override
@@ -54,13 +56,18 @@ class _NavigationScreenState extends State<NavigationScreen> {
final currentLoc = await _navigationService.getCurrentLocation();
setState(() => _currentLocation = currentLoc);
// Fetch stops for the bus line from the API (if available)
try {
_stops = await _scheduleService.getStops(widget.route.busLine.id);
// Use provided initial stops when available, otherwise fetch from API
if (widget.initialStops != null) {
_stops = widget.initialStops;
_stopsFetchError = null;
} catch (e) {
_stops = [];
_stopsFetchError = e.toString().replaceAll('Exception: ', '');
} else {
try {
_stops = await _scheduleService.getStops(widget.route.busLine.id);
_stopsFetchError = null;
} catch (e) {
_stops = [];
_stopsFetchError = e.toString().replaceAll('Exception: ', '');
}
}
// Determine start and end points: prefer stop locations if available.
@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import '../models/schedule.dart';
import '../models/schedule.dart' as sched;
import '../models/navigation.dart';
import '../services/auth_service.dart';
import '../services/schedule_service.dart';
import '../screens/navigation_screen.dart';
class ScheduleOverviewScreen extends StatefulWidget {
const ScheduleOverviewScreen({super.key});
@@ -14,7 +16,7 @@ class _ScheduleOverviewScreenState extends State<ScheduleOverviewScreen> {
final _scheduleService = ScheduleService();
final _authService = AuthService();
late Future<List<ServiceSchedule>> _scheduleFuture;
late Future<List<sched.ServiceSchedule>> _scheduleFuture;
DateTime _selectedDate = DateTime.now();
@override
@@ -61,9 +63,9 @@ class _ScheduleOverviewScreenState extends State<ScheduleOverviewScreen> {
}
}
Future<void> _showScheduleDetails(ServiceSchedule schedule) async {
Future<void> _showScheduleDetails(sched.ServiceSchedule svc) async {
try {
final stops = await _scheduleService.getScheduleStops(schedule.id);
final stops = await _scheduleService.getScheduleStops(svc.id);
if (!mounted) return;
await showModalBottomSheet<void>(
@@ -82,19 +84,19 @@ class _ScheduleOverviewScreenState extends State<ScheduleOverviewScreen> {
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
children: [
Text(
'Lijn ${schedule.lineNumber}',
'Lijn ${svc.lineNumber}',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 8),
Text(
schedule.busName,
svc.busName,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 4),
Text(
'${_formatTime(schedule.startTime)} - ${_formatTime(schedule.endTime)}',
'${_formatTime(svc.startTime)} - ${_formatTime(svc.endTime)}',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 12),
@@ -102,11 +104,56 @@ class _ScheduleOverviewScreenState extends State<ScheduleOverviewScreen> {
spacing: 8,
runSpacing: 8,
children: [
_InfoChip(label: schedule.status),
_InfoChip(label: schedule.isRecurring ? _weekdayLabel(schedule.weekdays) : 'Eenmalig'),
_InfoChip(label: svc.status),
_InfoChip(label: svc.isRecurring ? _weekdayLabel(svc.weekdays) : 'Eenmalig'),
],
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: () {
// Close the sheet and start navigation with provided stops
Navigator.of(sheetContext).pop();
final routeObj = sched.Route(
id: (svc.id is num) ? (svc.id as num).toInt() : int.tryParse(svc.id.toString()) ?? 0,
busLine: sched.BusLine(
id: svc.busLineId,
lineNumber: svc.lineNumber,
startStop: '',
endStop: '',
estimatedDuration: 0,
),
busType: sched.BusType(
id: svc.busId,
name: svc.busName,
seatCapacity: 0,
licensePlate: '',
),
startTime: svc.startTime,
endTime: svc.endTime,
status: svc.status,
);
final navStops = stops
.map((s) => NavigationPoint(
latitude: s.latitude,
longitude: s.longitude,
name: s.name,
))
.toList();
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => NavigationScreen(route: routeObj, initialStops: navStops),
),
);
},
icon: const Icon(Icons.navigation),
label: const Text('Start route'),
),
),
Text(
'Haltes',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
@@ -121,7 +168,7 @@ class _ScheduleOverviewScreenState extends State<ScheduleOverviewScreen> {
)
else
...stops.map((stop) {
final departureTime = schedule.startTime.add(
final departureTime = svc.startTime.add(
Duration(minutes: stop.estimatedArrivalMinutes),
);
return Card(
@@ -240,7 +287,7 @@ class _ScheduleOverviewScreenState extends State<ScheduleOverviewScreen> {
),
),
Expanded(
child: FutureBuilder<List<ServiceSchedule>>(
child: FutureBuilder<List<sched.ServiceSchedule>>(
future: _scheduleFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
@@ -339,7 +386,7 @@ class _ScheduleOverviewScreenState extends State<ScheduleOverviewScreen> {
}
class _ScheduleSummaryCard extends StatelessWidget {
final ServiceSchedule schedule;
final sched.ServiceSchedule schedule;
final VoidCallback onTap;
const _ScheduleSummaryCard({
+170
View File
@@ -731,9 +731,130 @@ app.post('/admin/fleet/buses', authMiddleware, adminMiddleware, async (req, res)
}
});
// ============================================
// ADMIN - Update fleet bus
// ============================================
app.patch('/admin/fleet/buses/:busId', authMiddleware, adminMiddleware, async (req, res) => {
try {
const { busId } = req.params;
const { name, seatCapacity, licensePlate } = req.body;
if (!busId || !Number.isFinite(Number(busId))) {
return res.status(400).json({ error: 'invalid_bus_id' });
}
if (DEV_MOCK) {
const bus = _mock.fleetBuses.find(b => Number(b.id) === Number(busId));
if (!bus) return res.status(404).json({ error: 'bus_not_found' });
if (name !== undefined) bus.name = name;
if (seatCapacity !== undefined) bus.seatCapacity = Number(seatCapacity);
if (licensePlate !== undefined) bus.licensePlate = licensePlate;
bus.updatedAt = new Date().toISOString();
return res.json({ bus });
}
const result = await db.query(`
UPDATE fleet.bus
SET name = COALESCE($1, name), seat_capacity = COALESCE($2, seat_capacity), license_plate = COALESCE($3, license_plate), updated_at = NOW()
WHERE id = $4
RETURNING id, name, seat_capacity, license_plate
`, [name ?? null, seatCapacity ?? null, licensePlate ?? null, busId]);
if (!result.rows[0]) return res.status(404).json({ error: 'bus_not_found' });
const bus = result.rows[0];
res.json({ bus: { id: bus.id, name: bus.name, seatCapacity: bus.seat_capacity, licensePlate: bus.license_plate } });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'internal_error', details: err.message });
}
});
// ============================================
// ADMIN - Delete fleet bus
// ============================================
app.delete('/admin/fleet/buses/:busId', authMiddleware, adminMiddleware, async (req, res) => {
try {
const { busId } = req.params;
if (!busId || !Number.isFinite(Number(busId))) {
return res.status(400).json({ error: 'invalid_bus_id' });
}
if (DEV_MOCK) {
const idx = _mock.fleetBuses.findIndex(b => Number(b.id) === Number(busId));
if (idx === -1) return res.status(404).json({ error: 'bus_not_found' });
const deleted = _mock.fleetBuses.splice(idx, 1)[0];
return res.json({ success: true, bus: deleted });
}
const result = await db.query(`DELETE FROM fleet.bus WHERE id = $1 RETURNING id, name`, [busId]);
if (result.rows.length === 0) return res.status(404).json({ error: 'bus_not_found' });
res.json({ success: true, bus: result.rows[0] });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'internal_error', details: err.message });
}
});
// ============================================
// ADMIN - Link stop to bus line
// ============================================
app.get('/admin/bus-lines/:busLineId/stops', authMiddleware, adminMiddleware, async (req, res) => {
try {
const { busLineId } = req.params;
if (!busLineId || !Number.isFinite(Number(busLineId))) {
return res.status(400).json({ error: 'invalid_bus_line_id' });
}
if (DEV_MOCK) {
const stops = _mock.routeStops
.filter(rs => String(rs.busLineId) === String(busLineId))
.sort((a, b) => a.stopOrder - b.stopOrder)
.map(rs => ({
id: rs.id,
busLineId: rs.busLineId,
stopId: rs.stopId,
stopOrder: rs.stopOrder,
estimatedArrivalMinutes: rs.estimatedArrivalMinutes,
name: rs.name,
latitude: rs.latitude,
longitude: rs.longitude,
}));
return res.json({ routeStops: stops });
}
const result = await db.query(`
SELECT
rs.id,
rs.bus_line_id,
rs.stop_id,
rs.stop_order,
rs.estimated_arrival_minutes,
s.name as stop_name,
s.latitude,
s.longitude
FROM transit.route_stops rs
JOIN transit.stops s ON s.id = rs.stop_id
WHERE rs.bus_line_id = $1
ORDER BY rs.stop_order ASC
`, [busLineId]);
res.json({ routeStops: result.rows.map(row => ({
id: row.id,
busLineId: row.bus_line_id,
stopId: row.stop_id,
stopOrder: row.stop_order,
estimatedArrivalMinutes: row.estimated_arrival_minutes,
name: row.stop_name,
latitude: parseFloat(row.latitude),
longitude: parseFloat(row.longitude),
})) });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'internal_error', details: err.message });
}
});
app.post('/admin/bus-lines/:busLineId/stops', authMiddleware, adminMiddleware, async (req, res) => {
try {
const { busLineId } = req.params;
@@ -952,6 +1073,55 @@ app.delete('/admin/stops/:stopId', authMiddleware, adminMiddleware, async (req,
}
});
// ============================================
// ADMIN - Update a stop
// ============================================
app.patch('/admin/stops/:stopId', authMiddleware, adminMiddleware, async (req, res) => {
try {
const { stopId } = req.params;
const { name, latitude, longitude } = req.body;
if (!stopId || !Number.isFinite(Number(stopId))) {
return res.status(400).json({ error: 'invalid_stop_id' });
}
if (DEV_MOCK) {
const stop = _mock.stops.find(s => Number(s.id) === Number(stopId));
if (!stop) return res.status(404).json({ error: 'stop_not_found' });
if (name !== undefined) stop.name = name;
if (latitude !== undefined) stop.latitude = parseFloat(latitude);
if (longitude !== undefined) stop.longitude = parseFloat(longitude);
stop.updatedAt = new Date().toISOString();
return res.json({ stop });
}
const result = await db.query(
`UPDATE transit.stops
SET name = COALESCE($1, name),
latitude = COALESCE($2, latitude),
longitude = COALESCE($3, longitude),
updated_at = NOW()
WHERE id = $4
RETURNING id, name, latitude, longitude, created_at, updated_at`
, [name ?? null, latitude ?? null, longitude ?? null, stopId]);
if (!result.rows[0]) return res.status(404).json({ error: 'stop_not_found' });
const stop = result.rows[0];
res.json({ stop: {
id: stop.id,
name: stop.name,
latitude: parseFloat(stop.latitude),
longitude: parseFloat(stop.longitude),
createdAt: stop.created_at,
updatedAt: stop.updated_at,
}});
} catch (err) {
console.error(err);
res.status(500).json({ error: 'internal_error', details: err.message });
}
});
// ============================================
// ADMIN - Delete a bus line
// ============================================