Public API
This commit is contained in:
@@ -92,20 +92,34 @@ class _NavigationScreenState extends State<NavigationScreen> {
|
|||||||
late NavigationPoint endPoint;
|
late NavigationPoint endPoint;
|
||||||
|
|
||||||
if (_stops != null && _stops!.length >= 2) {
|
if (_stops != null && _stops!.length >= 2) {
|
||||||
final optimized = await _navigationService.getOptimizedRouteThroughStops(stops: _stops!);
|
final hasScheduleStops = widget.initialStops != null;
|
||||||
_stops = optimized.orderedStops;
|
|
||||||
|
if (!hasScheduleStops) {
|
||||||
|
final optimized = await _navigationService.getOptimizedRouteThroughStops(stops: _stops!);
|
||||||
|
_stops = optimized.orderedStops;
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_currentRoute = optimized.route;
|
||||||
|
_alternativeRoutes = null;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _fitMapToRoute(optimized.route));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final navRoute = await _navigationService.getRouteThroughStops(stops: _stops!);
|
||||||
startPoint = _stops!.first;
|
startPoint = _stops!.first;
|
||||||
endPoint = _stops!.last;
|
endPoint = _stops!.last;
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentRoute = optimized.route;
|
_currentRoute = navRoute;
|
||||||
// limit alternatives to first two to avoid rendering many polylines
|
|
||||||
_alternativeRoutes = null;
|
_alternativeRoutes = null;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _fitMapToRoute(optimized.route));
|
WidgetsBinding.instance.addPostFrameCallback((_) => _fitMapToRoute(navRoute));
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -286,6 +300,23 @@ class _NavigationScreenState extends State<NavigationScreen> {
|
|||||||
if (_stops == null || _stops!.isEmpty) return;
|
if (_stops == null || _stops!.isEmpty) return;
|
||||||
if (index < 0 || index >= _stops!.length) return;
|
if (index < 0 || index >= _stops!.length) return;
|
||||||
|
|
||||||
|
// When this screen is opened from schedule overview, route.id maps to scheduleId.
|
||||||
|
if (widget.initialStops != null) {
|
||||||
|
try {
|
||||||
|
await _scheduleService.markScheduleStopPassed(
|
||||||
|
scheduleId: widget.route.id,
|
||||||
|
stopOrder: index + 1,
|
||||||
|
actualPassedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = 'Stop update mislukt: ${e.toString().replaceAll('Exception: ', '')}';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_completedStops.add(index);
|
_completedStops.add(index);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -177,4 +177,35 @@ class ScheduleService {
|
|||||||
throw Exception('Get stops error: $e');
|
throw Exception('Get stops error: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mark a schedule stop as passed by the driver.
|
||||||
|
Future<void> markScheduleStopPassed({
|
||||||
|
required int scheduleId,
|
||||||
|
required int stopOrder,
|
||||||
|
DateTime? actualPassedAt,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final headers = await _authService.getAuthHeaders();
|
||||||
|
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse('$_apiBaseUrl/driver/schedules/$scheduleId/stops/$stopOrder/passed'),
|
||||||
|
headers: headers,
|
||||||
|
body: jsonEncode({
|
||||||
|
if (actualPassedAt != null) 'actualPassedAt': actualPassedAt.toIso8601String(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 401 || response.statusCode == 403) {
|
||||||
|
await _authService.logout();
|
||||||
|
throw UnauthorizedException('Session expired - Please login again');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||||
|
throw Exception('Failed to mark stop as passed: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e is UnauthorizedException) rethrow;
|
||||||
|
throw Exception('Mark stop passed error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ const _mock = {
|
|||||||
busLines: [],
|
busLines: [],
|
||||||
fleetBuses: [],
|
fleetBuses: [],
|
||||||
schedules: [],
|
schedules: [],
|
||||||
|
scheduleStopProgress: [],
|
||||||
drivers: [],
|
drivers: [],
|
||||||
nextStopId: 1,
|
nextStopId: 1,
|
||||||
nextRouteStopId: 1,
|
nextRouteStopId: 1,
|
||||||
nextBusLineId: 1,
|
nextBusLineId: 1,
|
||||||
nextFleetBusId: 1,
|
nextFleetBusId: 1,
|
||||||
nextScheduleId: 1,
|
nextScheduleId: 1,
|
||||||
|
nextScheduleStopProgressId: 1,
|
||||||
nextDriverId: 1,
|
nextDriverId: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -249,6 +251,33 @@ function checkBusConflict(busId, startTime, endTime, weekdays, allSchedules, exc
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureScheduleStopProgressTable() {
|
||||||
|
if (DEV_MOCK) return;
|
||||||
|
|
||||||
|
await db.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS transit.schedule_stop_progress (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
schedule_id INTEGER NOT NULL,
|
||||||
|
bus_line_id INTEGER NOT NULL,
|
||||||
|
stop_id INTEGER NOT NULL,
|
||||||
|
stop_order INTEGER NOT NULL,
|
||||||
|
stop_name TEXT,
|
||||||
|
estimated_arrival_minutes INTEGER NOT NULL DEFAULT 0,
|
||||||
|
scheduled_passed_at TIMESTAMPTZ NOT NULL,
|
||||||
|
actual_passed_at TIMESTAMPTZ NOT NULL,
|
||||||
|
delay_minutes INTEGER NOT NULL DEFAULT 0,
|
||||||
|
marked_by_user_id INTEGER,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE (schedule_id, stop_order)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_schedule_stop_progress_schedule_order
|
||||||
|
ON transit.schedule_stop_progress (schedule_id, stop_order)
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// ROUTES - Get daily schedule
|
// ROUTES - Get daily schedule
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -1947,6 +1976,376 @@ app.get('/schedules/:scheduleId/stops', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// DRIVER - Mark stop as passed (updates realtime state)
|
||||||
|
// ============================================
|
||||||
|
app.post('/driver/schedules/:scheduleId/stops/:stopOrder/passed', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const scheduleId = Number(req.params.scheduleId);
|
||||||
|
const stopOrder = Number(req.params.stopOrder);
|
||||||
|
|
||||||
|
if (!Number.isFinite(scheduleId) || !Number.isFinite(stopOrder) || stopOrder < 1) {
|
||||||
|
return res.status(400).json({ error: 'invalid_schedule_or_stop_order' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const actualPassedAt = req.body?.actualPassedAt ? new Date(req.body.actualPassedAt) : new Date();
|
||||||
|
if (Number.isNaN(actualPassedAt.getTime())) {
|
||||||
|
return res.status(400).json({ error: 'invalid_actual_passed_at' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DEV_MOCK) {
|
||||||
|
const schedule = (_mock.schedules || []).find(s => Number(s.id) === scheduleId);
|
||||||
|
if (!schedule) {
|
||||||
|
return res.status(404).json({ error: 'schedule_not_found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeStop = (_mock.routeStops || [])
|
||||||
|
.find(rs => Number(rs.busLineId) === Number(schedule.busLineId) && Number(rs.stopOrder) === stopOrder);
|
||||||
|
|
||||||
|
if (!routeStop) {
|
||||||
|
return res.status(404).json({ error: 'stop_not_found_for_schedule' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const stop = (_mock.stops || []).find(s => Number(s.id) === Number(routeStop.stopId));
|
||||||
|
const scheduledPassedAt = new Date(schedule.startTime);
|
||||||
|
scheduledPassedAt.setMinutes(scheduledPassedAt.getMinutes() + (Number(routeStop.estimatedArrivalMinutes) || 0));
|
||||||
|
const delayMinutes = Math.round((actualPassedAt.getTime() - scheduledPassedAt.getTime()) / 60000);
|
||||||
|
|
||||||
|
const existingIndex = (_mock.scheduleStopProgress || []).findIndex(
|
||||||
|
p => Number(p.scheduleId) === scheduleId && Number(p.stopOrder) === stopOrder
|
||||||
|
);
|
||||||
|
|
||||||
|
const progressRow = {
|
||||||
|
id: existingIndex >= 0 ? _mock.scheduleStopProgress[existingIndex].id : _mock.nextScheduleStopProgressId++,
|
||||||
|
scheduleId,
|
||||||
|
busLineId: Number(schedule.busLineId),
|
||||||
|
stopId: Number(routeStop.stopId),
|
||||||
|
stopOrder,
|
||||||
|
stopName: stop?.name || routeStop.name || 'Unknown Stop',
|
||||||
|
estimatedArrivalMinutes: Number(routeStop.estimatedArrivalMinutes) || 0,
|
||||||
|
scheduledPassedAt: scheduledPassedAt.toISOString(),
|
||||||
|
actualPassedAt: actualPassedAt.toISOString(),
|
||||||
|
delayMinutes,
|
||||||
|
markedByUserId: Number(req.user?.userId) || null,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
_mock.scheduleStopProgress[existingIndex] = progressRow;
|
||||||
|
} else {
|
||||||
|
_mock.scheduleStopProgress.push(progressRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalStops = (_mock.routeStops || []).filter(rs => Number(rs.busLineId) === Number(schedule.busLineId)).length;
|
||||||
|
const passedCount = (_mock.scheduleStopProgress || []).filter(p => Number(p.scheduleId) === scheduleId).length;
|
||||||
|
|
||||||
|
if (passedCount >= totalStops) {
|
||||||
|
schedule.status = 'completed';
|
||||||
|
schedule.endTime = actualPassedAt.toISOString();
|
||||||
|
} else if (schedule.status === 'planned') {
|
||||||
|
schedule.status = 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
scheduleId,
|
||||||
|
stopOrder,
|
||||||
|
status: schedule.status,
|
||||||
|
delayMinutes,
|
||||||
|
passedStops: passedCount,
|
||||||
|
totalStops,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureScheduleStopProgressTable();
|
||||||
|
|
||||||
|
const scheduleResult = await db.query(`
|
||||||
|
SELECT s.id, s.bus_line_id, s.start_time, s.status
|
||||||
|
FROM transit.schedules s
|
||||||
|
WHERE s.id = $1
|
||||||
|
`, [scheduleId]);
|
||||||
|
|
||||||
|
if (scheduleResult.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'schedule_not_found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const schedule = scheduleResult.rows[0];
|
||||||
|
|
||||||
|
const routeStopResult = await db.query(`
|
||||||
|
SELECT
|
||||||
|
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 = $1
|
||||||
|
AND rs.stop_order = $2
|
||||||
|
LIMIT 1
|
||||||
|
`, [schedule.bus_line_id, stopOrder]);
|
||||||
|
|
||||||
|
if (routeStopResult.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'stop_not_found_for_schedule' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeStop = routeStopResult.rows[0];
|
||||||
|
const scheduledPassedAt = new Date(schedule.start_time);
|
||||||
|
scheduledPassedAt.setMinutes(scheduledPassedAt.getMinutes() + (Number(routeStop.estimated_arrival_minutes) || 0));
|
||||||
|
const delayMinutes = Math.round((actualPassedAt.getTime() - scheduledPassedAt.getTime()) / 60000);
|
||||||
|
|
||||||
|
await db.query(`
|
||||||
|
INSERT INTO transit.schedule_stop_progress (
|
||||||
|
schedule_id,
|
||||||
|
bus_line_id,
|
||||||
|
stop_id,
|
||||||
|
stop_order,
|
||||||
|
stop_name,
|
||||||
|
estimated_arrival_minutes,
|
||||||
|
scheduled_passed_at,
|
||||||
|
actual_passed_at,
|
||||||
|
delay_minutes,
|
||||||
|
marked_by_user_id
|
||||||
|
)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
|
||||||
|
ON CONFLICT (schedule_id, stop_order)
|
||||||
|
DO UPDATE SET
|
||||||
|
actual_passed_at = EXCLUDED.actual_passed_at,
|
||||||
|
delay_minutes = EXCLUDED.delay_minutes,
|
||||||
|
marked_by_user_id = EXCLUDED.marked_by_user_id,
|
||||||
|
created_at = NOW()
|
||||||
|
`, [
|
||||||
|
scheduleId,
|
||||||
|
Number(schedule.bus_line_id),
|
||||||
|
Number(routeStop.stop_id),
|
||||||
|
stopOrder,
|
||||||
|
routeStop.stop_name,
|
||||||
|
Number(routeStop.estimated_arrival_minutes) || 0,
|
||||||
|
scheduledPassedAt.toISOString(),
|
||||||
|
actualPassedAt.toISOString(),
|
||||||
|
delayMinutes,
|
||||||
|
Number(req.user?.userId) || null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const totalsResult = await db.query(`
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*)::int FROM transit.route_stops WHERE bus_line_id = $1) AS total_stops,
|
||||||
|
(SELECT COUNT(*)::int FROM transit.schedule_stop_progress WHERE schedule_id = $2) AS passed_stops
|
||||||
|
`, [schedule.bus_line_id, scheduleId]);
|
||||||
|
|
||||||
|
const totalStops = Number(totalsResult.rows[0]?.total_stops || 0);
|
||||||
|
const passedStops = Number(totalsResult.rows[0]?.passed_stops || 0);
|
||||||
|
const isCompleted = totalStops > 0 && passedStops >= totalStops;
|
||||||
|
const nextStatus = isCompleted ? 'completed' : 'active';
|
||||||
|
|
||||||
|
await db.query(`
|
||||||
|
UPDATE transit.schedules
|
||||||
|
SET
|
||||||
|
status = $2,
|
||||||
|
end_time = CASE WHEN $2 = 'completed' THEN $3 ELSE end_time END
|
||||||
|
WHERE id = $1
|
||||||
|
`, [scheduleId, nextStatus, actualPassedAt.toISOString()]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
scheduleId,
|
||||||
|
stopOrder,
|
||||||
|
status: nextStatus,
|
||||||
|
delayMinutes,
|
||||||
|
passedStops,
|
||||||
|
totalStops,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('POST /driver/schedules/:scheduleId/stops/:stopOrder/passed error:', err);
|
||||||
|
res.status(500).json({ error: 'internal_error', details: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// PUBLIC - Live progress for passengers
|
||||||
|
// ============================================
|
||||||
|
app.get('/public/schedules/:scheduleId/progress', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const scheduleId = Number(req.params.scheduleId);
|
||||||
|
if (!Number.isFinite(scheduleId)) {
|
||||||
|
return res.status(400).json({ error: 'invalid_schedule_id' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DEV_MOCK) {
|
||||||
|
const schedule = (_mock.schedules || []).find(s => Number(s.id) === scheduleId);
|
||||||
|
if (!schedule) {
|
||||||
|
return res.status(404).json({ error: 'schedule_not_found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const busLine = (_mock.busLines || []).find(bl => Number(bl.id) === Number(schedule.busLineId));
|
||||||
|
const routeStops = (_mock.routeStops || [])
|
||||||
|
.filter(rs => Number(rs.busLineId) === Number(schedule.busLineId))
|
||||||
|
.sort((a, b) => Number(a.stopOrder) - Number(b.stopOrder));
|
||||||
|
|
||||||
|
const progressEvents = (_mock.scheduleStopProgress || [])
|
||||||
|
.filter(p => Number(p.scheduleId) === scheduleId)
|
||||||
|
.sort((a, b) => Number(a.stopOrder) - Number(b.stopOrder));
|
||||||
|
|
||||||
|
const progressByOrder = new Map(progressEvents.map(p => [Number(p.stopOrder), p]));
|
||||||
|
const latestProgress = progressEvents.length > 0 ? progressEvents[progressEvents.length - 1] : null;
|
||||||
|
const currentDelayMinutes = latestProgress ? Number(latestProgress.delayMinutes) || 0 : 0;
|
||||||
|
|
||||||
|
const stops = routeStops.map(rs => {
|
||||||
|
const stop = (_mock.stops || []).find(s => Number(s.id) === Number(rs.stopId));
|
||||||
|
const scheduledTime = new Date(schedule.startTime);
|
||||||
|
scheduledTime.setMinutes(scheduledTime.getMinutes() + (Number(rs.estimatedArrivalMinutes) || 0));
|
||||||
|
const progress = progressByOrder.get(Number(rs.stopOrder));
|
||||||
|
|
||||||
|
return {
|
||||||
|
stopId: Number(rs.stopId),
|
||||||
|
stopOrder: Number(rs.stopOrder),
|
||||||
|
stopName: stop?.name || rs.name || 'Unknown Stop',
|
||||||
|
estimatedArrivalMinutes: Number(rs.estimatedArrivalMinutes) || 0,
|
||||||
|
scheduledPassedAt: scheduledTime.toISOString(),
|
||||||
|
actualPassedAt: progress?.actualPassedAt || null,
|
||||||
|
delayMinutes: progress ? Number(progress.delayMinutes) || 0 : null,
|
||||||
|
status: progress ? 'passed' : 'pending',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const passedStops = stops.filter(s => s.status === 'passed');
|
||||||
|
const nextStop = stops.find(s => s.status === 'pending') || null;
|
||||||
|
const driverState = passedStops.length === 0
|
||||||
|
? 'not_started'
|
||||||
|
: (passedStops.length >= stops.length ? 'completed' : 'en_route');
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
schedule: {
|
||||||
|
id: scheduleId,
|
||||||
|
lineNumber: Number(busLine?.lineNumber || 0),
|
||||||
|
startStop: busLine?.startStop || null,
|
||||||
|
endStop: busLine?.endStop || null,
|
||||||
|
startTime: schedule.startTime,
|
||||||
|
endTime: schedule.endTime,
|
||||||
|
status: schedule.status,
|
||||||
|
},
|
||||||
|
driverState,
|
||||||
|
currentDelayMinutes,
|
||||||
|
passedStopCount: passedStops.length,
|
||||||
|
totalStopCount: stops.length,
|
||||||
|
nextStop: nextStop
|
||||||
|
? {
|
||||||
|
...nextStop,
|
||||||
|
predictedPassedAt: new Date(new Date(nextStop.scheduledPassedAt).getTime() + currentDelayMinutes * 60000).toISOString(),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
stops,
|
||||||
|
updatedAt: latestProgress?.actualPassedAt || null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureScheduleStopProgressTable();
|
||||||
|
|
||||||
|
const scheduleResult = await db.query(`
|
||||||
|
SELECT
|
||||||
|
s.id,
|
||||||
|
s.bus_line_id,
|
||||||
|
s.start_time,
|
||||||
|
s.end_time,
|
||||||
|
s.status,
|
||||||
|
bl.line_number,
|
||||||
|
bl.start_stop,
|
||||||
|
bl.end_stop
|
||||||
|
FROM transit.schedules s
|
||||||
|
JOIN transit.bus_lines bl ON bl.id = s.bus_line_id
|
||||||
|
WHERE s.id = $1
|
||||||
|
LIMIT 1
|
||||||
|
`, [scheduleId]);
|
||||||
|
|
||||||
|
if (scheduleResult.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'schedule_not_found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const schedule = scheduleResult.rows[0];
|
||||||
|
|
||||||
|
const routeStopsResult = await db.query(`
|
||||||
|
SELECT
|
||||||
|
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 = $1
|
||||||
|
ORDER BY rs.stop_order ASC
|
||||||
|
`, [schedule.bus_line_id]);
|
||||||
|
|
||||||
|
const progressResult = await db.query(`
|
||||||
|
SELECT
|
||||||
|
stop_order,
|
||||||
|
actual_passed_at,
|
||||||
|
delay_minutes
|
||||||
|
FROM transit.schedule_stop_progress
|
||||||
|
WHERE schedule_id = $1
|
||||||
|
ORDER BY stop_order ASC
|
||||||
|
`, [scheduleId]);
|
||||||
|
|
||||||
|
const progressByOrder = new Map(
|
||||||
|
progressResult.rows.map(row => [Number(row.stop_order), row])
|
||||||
|
);
|
||||||
|
const latestProgress = progressResult.rows.length > 0
|
||||||
|
? progressResult.rows[progressResult.rows.length - 1]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const currentDelayMinutes = latestProgress ? Number(latestProgress.delay_minutes) || 0 : 0;
|
||||||
|
|
||||||
|
const stops = routeStopsResult.rows.map(row => {
|
||||||
|
const etaMinutes = Number(row.estimated_arrival_minutes) || 0;
|
||||||
|
const scheduledPassedAt = new Date(schedule.start_time);
|
||||||
|
scheduledPassedAt.setMinutes(scheduledPassedAt.getMinutes() + etaMinutes);
|
||||||
|
|
||||||
|
const progress = progressByOrder.get(Number(row.stop_order));
|
||||||
|
|
||||||
|
return {
|
||||||
|
stopId: Number(row.stop_id),
|
||||||
|
stopOrder: Number(row.stop_order),
|
||||||
|
stopName: row.stop_name,
|
||||||
|
estimatedArrivalMinutes: etaMinutes,
|
||||||
|
scheduledPassedAt: scheduledPassedAt.toISOString(),
|
||||||
|
actualPassedAt: progress?.actual_passed_at || null,
|
||||||
|
delayMinutes: progress ? Number(progress.delay_minutes) || 0 : null,
|
||||||
|
status: progress ? 'passed' : 'pending',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const passedStops = stops.filter(s => s.status === 'passed');
|
||||||
|
const nextStop = stops.find(s => s.status === 'pending') || null;
|
||||||
|
|
||||||
|
const driverState = passedStops.length === 0
|
||||||
|
? 'not_started'
|
||||||
|
: (passedStops.length >= stops.length ? 'completed' : 'en_route');
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
schedule: {
|
||||||
|
id: Number(schedule.id),
|
||||||
|
lineNumber: Number(schedule.line_number),
|
||||||
|
startStop: schedule.start_stop,
|
||||||
|
endStop: schedule.end_stop,
|
||||||
|
startTime: schedule.start_time,
|
||||||
|
endTime: schedule.end_time,
|
||||||
|
status: schedule.status,
|
||||||
|
},
|
||||||
|
driverState,
|
||||||
|
currentDelayMinutes,
|
||||||
|
passedStopCount: passedStops.length,
|
||||||
|
totalStopCount: stops.length,
|
||||||
|
nextStop: nextStop
|
||||||
|
? {
|
||||||
|
...nextStop,
|
||||||
|
predictedPassedAt: new Date(new Date(nextStop.scheduledPassedAt).getTime() + currentDelayMinutes * 60000).toISOString(),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
stops,
|
||||||
|
updatedAt: latestProgress?.actual_passed_at || null,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('GET /public/schedules/:scheduleId/progress error:', err);
|
||||||
|
res.status(500).json({ error: 'internal_error', details: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const PORT = process.env.PORT || 5001;
|
const PORT = process.env.PORT || 5001;
|
||||||
app.listen(PORT, () => console.log(`Transit API listening on ${PORT}`));
|
app.listen(PORT, () => console.log(`Transit API listening on ${PORT}`));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user