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,6 +96,7 @@ 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)}
|
||||
@@ -98,6 +104,21 @@ export default function BusLinesManager({ token, user }) {
|
||||
>
|
||||
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 ? (
|
||||
{(() => {
|
||||
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>
|
||||
{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)}
|
||||
{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>
|
||||
) : (
|
||||
<p style={{ fontSize: '0.85rem', color: '#999' }}>Geen haltes gekoppeld</p>
|
||||
)}
|
||||
)
|
||||
})()}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,7 +56,11 @@ class _NavigationScreenState extends State<NavigationScreen> {
|
||||
final currentLoc = await _navigationService.getCurrentLocation();
|
||||
setState(() => _currentLocation = currentLoc);
|
||||
|
||||
// Fetch stops for the bus line from the API (if available)
|
||||
// Use provided initial stops when available, otherwise fetch from API
|
||||
if (widget.initialStops != null) {
|
||||
_stops = widget.initialStops;
|
||||
_stopsFetchError = null;
|
||||
} else {
|
||||
try {
|
||||
_stops = await _scheduleService.getStops(widget.route.busLine.id);
|
||||
_stopsFetchError = null;
|
||||
@@ -62,6 +68,7 @@ class _NavigationScreenState extends State<NavigationScreen> {
|
||||
_stops = [];
|
||||
_stopsFetchError = e.toString().replaceAll('Exception: ', '');
|
||||
}
|
||||
}
|
||||
|
||||
// Determine start and end points: prefer stop locations if available.
|
||||
// If no stops are returned from the API, geocode the stop names.
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
// ============================================
|
||||
|
||||
Reference in New Issue
Block a user