Enhance schedule management; implement recurring schedules retrieval, update TimetableViewer to display summary, and improve schedule loading logic
This commit is contained in:
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'
|
||||
|
||||
export default function TimetableViewer({ apiBaseUrl = 'http://localhost:5001' }) {
|
||||
const [schedules, setSchedules] = useState([])
|
||||
const [summary, setSummary] = useState(null)
|
||||
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0])
|
||||
const [selectedLineId, setSelectedLineId] = useState('')
|
||||
const [lines, setLines] = useState([])
|
||||
@@ -28,13 +29,14 @@ export default function TimetableViewer({ apiBaseUrl = 'http://localhost:5001' }
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
let url = `${apiBaseUrl}/schedules?date=${selectedDate}`
|
||||
let url = `${apiBaseUrl}/schedules/overview?date=${selectedDate}`
|
||||
if (selectedLineId) {
|
||||
url += `&lineId=${selectedLineId}`
|
||||
}
|
||||
const res = await fetch(url)
|
||||
const data = await res.json()
|
||||
setSchedules(data.schedules || [])
|
||||
setSummary(data.summary || null)
|
||||
} catch (err) {
|
||||
setError('Failed to load schedules: ' + err.message)
|
||||
} finally {
|
||||
@@ -72,6 +74,20 @@ export default function TimetableViewer({ apiBaseUrl = 'http://localhost:5001' }
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{summary && (
|
||||
<div style={{ marginBottom: '16px', display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
|
||||
<span style={{ padding: '6px 10px', borderRadius: '999px', backgroundColor: '#eef2ff', color: '#3730a3' }}>
|
||||
Total: {summary.total}
|
||||
</span>
|
||||
<span style={{ padding: '6px 10px', borderRadius: '999px', backgroundColor: '#ecfeff', color: '#155e75' }}>
|
||||
Vast: {summary.recurring}
|
||||
</span>
|
||||
<span style={{ padding: '6px 10px', borderRadius: '999px', backgroundColor: '#f8fafc', color: '#334155' }}>
|
||||
Eenmalig: {summary.oneTime}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && <p>Loading schedules...</p>}
|
||||
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||
|
||||
@@ -81,7 +97,8 @@ export default function TimetableViewer({ apiBaseUrl = 'http://localhost:5001' }
|
||||
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Time</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Line</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Vehicle</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Status</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Day status</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Delay</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -104,14 +121,19 @@ export default function TimetableViewer({ apiBaseUrl = 'http://localhost:5001' }
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: schedule.status === 'active' ? '#dcfce7' : '#f3f4f6',
|
||||
color: schedule.status === 'active' ? '#166534' : '#374151',
|
||||
backgroundColor: schedule.status === 'in_progress' ? '#dbeafe' : schedule.status === 'completed' ? '#dcfce7' : '#f3f4f6',
|
||||
color: schedule.status === 'in_progress' ? '#1d4ed8' : schedule.status === 'completed' ? '#166534' : '#374151',
|
||||
fontSize: '0.9rem',
|
||||
}}
|
||||
>
|
||||
{schedule.status}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '12px' }}>
|
||||
{typeof schedule.currentDelayMinutes === 'number' && schedule.currentDelayMinutes !== null
|
||||
? `${schedule.currentDelayMinutes > 0 ? '+' : ''}${schedule.currentDelayMinutes} min`
|
||||
: '—'}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
})}
|
||||
|
||||
@@ -24,10 +24,7 @@ class _RecurringSchedulesScreenState extends State<RecurringSchedulesScreen> {
|
||||
}
|
||||
|
||||
void _loadRecurringSchedules() {
|
||||
// Load schedules from a reference date, then filter for recurring ones
|
||||
_schedulesFuture = _scheduleService.getSchedules(DateTime.now()).then((schedules) {
|
||||
return schedules.where((s) => s.isRecurring).toList();
|
||||
});
|
||||
_schedulesFuture = _scheduleService.getRecurringSchedules();
|
||||
}
|
||||
|
||||
String _formatWeekdays(List<int> weekdays) {
|
||||
|
||||
@@ -38,6 +38,31 @@ class ScheduleService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all recurring schedule templates, independent of a specific date.
|
||||
Future<List<ServiceSchedule>> getRecurringSchedules({int? lineId}) async {
|
||||
try {
|
||||
final queryParameters = <String, String>{};
|
||||
if (lineId != null) {
|
||||
queryParameters['lineId'] = lineId.toString();
|
||||
}
|
||||
|
||||
final uri = Uri.parse('$_apiBaseUrl/schedules/recurring').replace(queryParameters: queryParameters);
|
||||
final response = await http.get(uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
return (data['schedules'] as List? ?? [])
|
||||
.map((schedule) => schedule as Map<String, dynamic>)
|
||||
.map(ServiceSchedule.fromJson)
|
||||
.toList();
|
||||
}
|
||||
|
||||
throw Exception('Failed to load recurring schedules: ${response.statusCode}');
|
||||
} catch (e) {
|
||||
throw Exception('Recurring schedule error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get stop list for a specific schedule.
|
||||
Future<List<ScheduleStop>> getScheduleStops(int scheduleId) async {
|
||||
try {
|
||||
|
||||
@@ -2736,6 +2736,138 @@ app.get('/schedules/overview', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// PUBLIC - Get all recurring schedule templates (no date filtering)
|
||||
// ============================================
|
||||
app.get('/schedules/recurring', async (req, res) => {
|
||||
try {
|
||||
const filters = parseScheduleFilters(req.query);
|
||||
|
||||
if (DEV_MOCK) {
|
||||
const routeStopsByLine = new Map();
|
||||
|
||||
for (const routeStop of _mock.routeStops || []) {
|
||||
const busLineId = Number(routeStop.busLineId);
|
||||
if (!routeStopsByLine.has(busLineId)) {
|
||||
routeStopsByLine.set(busLineId, []);
|
||||
}
|
||||
|
||||
routeStopsByLine.get(busLineId).push({
|
||||
bus_line_id: busLineId,
|
||||
stop_id: routeStop.stopId,
|
||||
stop_order: routeStop.stopOrder,
|
||||
estimated_arrival_minutes: routeStop.estimatedArrivalMinutes,
|
||||
stop_name: routeStop.name || '',
|
||||
});
|
||||
}
|
||||
|
||||
const baseRows = (_mock.schedules || [])
|
||||
.filter(schedule => Array.isArray(schedule.weekdays) && schedule.weekdays.length > 0)
|
||||
.map(schedule => {
|
||||
const busLine = _mock.busLines.find(line => Number(line.id) === Number(schedule.busLineId));
|
||||
const bus = _mock.fleetBuses.find(vehicle => Number(vehicle.id) === Number(schedule.busId));
|
||||
const driver = _mock.drivers.find(person => Number(person.id) === Number(schedule.driverId));
|
||||
|
||||
return {
|
||||
id: schedule.id,
|
||||
busLineId: schedule.busLineId,
|
||||
busId: schedule.busId,
|
||||
driverId: schedule.driverId ?? null,
|
||||
driverName: driver ? driver.name : null,
|
||||
startTime: schedule.startTime,
|
||||
endTime: schedule.endTime,
|
||||
weekdays: schedule.weekdays || [],
|
||||
lineNumber: busLine?.lineNumber || 0,
|
||||
busName: bus?.name || 'Unknown Bus',
|
||||
licensePlate: bus?.licensePlate || '',
|
||||
createdAt: schedule.createdAt,
|
||||
};
|
||||
});
|
||||
|
||||
const schedules = baseRows
|
||||
.filter(row => scheduleMatchesFilters(row, filters, routeStopsByLine))
|
||||
.map(row => {
|
||||
const materialized = materializeScheduleOverviewRow(row, null, routeStopsByLine);
|
||||
return {
|
||||
...materialized,
|
||||
status: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
return res.json({ schedules });
|
||||
}
|
||||
|
||||
const hasWeekdaysColumn = await db.query(`
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'transit' AND table_name = 'schedules' AND column_name = 'weekdays'
|
||||
`);
|
||||
|
||||
if (hasWeekdaysColumn.rows.length === 0) {
|
||||
return res.json({ schedules: [] });
|
||||
}
|
||||
|
||||
const result = await db.query(`
|
||||
SELECT
|
||||
s.id,
|
||||
s.bus_line_id AS "busLineId",
|
||||
s.bus_id AS "busId",
|
||||
s.driver_id AS "driverId",
|
||||
s.start_time AS "startTime",
|
||||
s.end_time AS "endTime",
|
||||
s.weekdays,
|
||||
bl.line_number AS "lineNumber",
|
||||
b.name AS "busName",
|
||||
b.license_plate AS "licensePlate",
|
||||
TRIM(COALESCE(e.firstname, '') || ' ' || COALESCE(e.lastname, '')) AS "driverName",
|
||||
s.created_at AS "createdAt"
|
||||
FROM transit.schedules s
|
||||
LEFT JOIN transit.bus_lines bl ON s.bus_line_id = bl.id
|
||||
LEFT JOIN fleet.bus b ON s.bus_id = b.id
|
||||
LEFT JOIN workers.employees e ON s.driver_id = e.id
|
||||
WHERE s.weekdays IS NOT NULL
|
||||
`);
|
||||
|
||||
const lineIds = Array.from(new Set(result.rows.map(row => Number(row.busLineId)).filter(Number.isFinite)));
|
||||
const routeStopsByLine = new Map();
|
||||
|
||||
if (lineIds.length > 0) {
|
||||
const routeStopsResult = await db.query(`
|
||||
SELECT
|
||||
rs.bus_line_id,
|
||||
rs.stop_id,
|
||||
rs.stop_order,
|
||||
rs.estimated_arrival_minutes,
|
||||
s.name AS stop_name
|
||||
FROM transit.route_stops rs
|
||||
JOIN transit.stops s ON s.id = rs.stop_id
|
||||
WHERE rs.bus_line_id = ANY($1::int[])
|
||||
ORDER BY rs.bus_line_id ASC, rs.stop_order ASC
|
||||
`, [lineIds]);
|
||||
|
||||
for (const row of routeStopsResult.rows) {
|
||||
const busLineId = Number(row.bus_line_id);
|
||||
if (!routeStopsByLine.has(busLineId)) {
|
||||
routeStopsByLine.set(busLineId, []);
|
||||
}
|
||||
routeStopsByLine.get(busLineId).push(row);
|
||||
}
|
||||
}
|
||||
|
||||
const schedules = result.rows
|
||||
.filter(row => scheduleMatchesFilters(row, filters, routeStopsByLine))
|
||||
.map(row => {
|
||||
const schedule = materializeScheduleOverviewRow(row, null, routeStopsByLine);
|
||||
delete schedule.status;
|
||||
return schedule;
|
||||
});
|
||||
|
||||
res.json({ schedules });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'internal_error', details: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/schedules/:scheduleId', async (req, res) => {
|
||||
try {
|
||||
const { scheduleId } = req.params;
|
||||
|
||||
Reference in New Issue
Block a user