const { store: _mock } = require("../mock/store"); const { toDateKey, expandRecurringScheduleRows, scheduleMatchesFilters, parseScheduleFilters, mapRecurringOccurrenceRows, materializeScheduleOverviewRow } = require("../lib/schedule"); const { reportError } = require("../lib/report-error"); // Schedules repository methods (DEV_MOCK mode). Exact copies of the original // handlers’ mock branches, returning { statusCode, body }. Note: the markStopPassed // mock branch reproduces the original ReferenceError (it reads an undeclared // `delayMinutes`), so it always returns 500 in mock mode, exactly as before. async function listSchedules(req) { try { const filters = parseScheduleFilters(req.query, { statuses: ["planned", "active"], }); 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({ stop_id: routeStop.stopId, stop_order: routeStop.stopOrder, }); } const schedules = expandRecurringScheduleRows( (_mock.schedules || []).map((s) => { const busLine = _mock.busLines.find((bl) => bl.id === s.busLineId); const bus = _mock.fleetBuses.find((b) => b.id === s.busId); return { id: s.id, busLineId: s.busLineId, busId: s.busId, startTime: s.startTime, endTime: s.endTime, weekdays: s.weekdays || [], status: s.status, lineNumber: busLine?.lineNumber || 0, busName: bus?.name || "Unknown Bus", driverId: null, estimatedDurationMinutes: busLine?.estimatedDurationMinutes || 60, }; }), ).filter((row) => scheduleMatchesFilters(row, filters, routeStopsByLine)); return { statusCode: 200, body: { schedules } }; } catch (err) { console.error(err); reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message }); return { statusCode: 500, body: { error: "internal_error", details: err.message } }; } } async function getSchedulesOverview(req) { try { const filters = parseScheduleFilters(req.query, { statuses: ["planned", "active"], }); const overviewDateKey = filters.date || null; 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 || []).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 || [], status: schedule.status, lineNumber: busLine?.lineNumber || 0, busName: bus?.name || "Unknown Bus", licensePlate: bus?.licensePlate || "", createdAt: schedule.createdAt, }; }); const schedules = mapRecurringOccurrenceRows( baseRows, overviewDateKey, routeStopsByLine, ).filter((row) => scheduleMatchesFilters(row, filters, routeStopsByLine)); return { statusCode: 200, body: { date: overviewDateKey, summary: { total: schedules.length, recurring: schedules.filter( (schedule) => schedule.scheduleType === "recurring", ).length, oneTime: schedules.filter( (schedule) => schedule.scheduleType === "one_time", ).length, }, schedules, }, }; } catch (err) { console.error(err); reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message }); return { statusCode: 500, body: { error: "internal_error", details: err.message } }; } } async function listRecurringSchedules(req) { try { const filters = parseScheduleFilters(req.query); 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 { statusCode: 200, body: { schedules } }; } catch (err) { console.error(err); reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message }); return { statusCode: 500, body: { error: "internal_error", details: err.message } }; } } async function getScheduleById(req) { try { const { scheduleId } = req.params; const schedule = (_mock.schedules || []).find( (s) => Number(s.id) === Number(scheduleId), ); if (!schedule) { return { statusCode: 404, body: { error: "schedule_not_found" } }; } const busLine = _mock.busLines.find((bl) => bl.id === schedule.busLineId); const bus = _mock.fleetBuses.find((b) => b.id === schedule.busId); return { statusCode: 200, body: { schedule: { id: schedule.id, busLineId: schedule.busLineId, busId: schedule.busId, startTime: schedule.startTime, endTime: schedule.endTime, weekdays: schedule.weekdays || [], status: schedule.status, lineNumber: busLine?.lineNumber || 0, busName: bus?.name || "Unknown Bus", }, }, }; } catch (err) { console.error(err); reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message }); return { statusCode: 500, body: { error: "internal_error", details: err.message } }; } } async function getScheduleStops(req) { try { const { scheduleId } = req.params; const schedule = (_mock.schedules || []).find( (s) => Number(s.id) === Number(scheduleId), ); if (!schedule) { return { statusCode: 404, body: { error: "schedule_not_found" } }; } const routeStops = (_mock.routeStops || []) .filter((rs) => Number(rs.busLineId) === Number(schedule.busLineId)) .sort((a, b) => a.stopOrder - b.stopOrder); const formattedStops = routeStops.map((rs) => { const stop = (_mock.stops || []).find( (s) => Number(s.id) === Number(rs.stopId), ); return { id: rs.id, order: rs.stopOrder, name: stop?.name || rs.name || "Unknown Stop", latitude: stop?.latitude ?? rs.latitude ?? 0, longitude: stop?.longitude ?? rs.longitude ?? 0, estimatedArrivalMinutes: rs.estimatedArrivalMinutes || 0, }; }); return { statusCode: 200, body: { stops: formattedStops } }; } catch (err) { console.error(err); reportError({ partofservice: `${req.method} ${req.originalUrl}`, error: err.stack || err.message }); return { statusCode: 500, body: { error: "internal_error", details: err.message } }; } } async function markStopPassed(req) { try { const scheduleId = Number(req.params.scheduleId); const stopOrder = Number(req.params.stopOrder); if ( !Number.isFinite(scheduleId) || !Number.isFinite(stopOrder) || stopOrder < 1 ) { return { statusCode: 400, body: { error: "invalid_schedule_or_stop_order" }, }; } const actualPassedAt = req.body?.actualPassedAt ? new Date(req.body.actualPassedAt) : new Date(); if (Number.isNaN(actualPassedAt.getTime())) { return { statusCode: 400, body: { error: "invalid_actual_passed_at" } }; } const schedule = (_mock.schedules || []).find( (s) => Number(s.id) === scheduleId, ); if (!schedule) { return { statusCode: 404, body: { error: "schedule_not_found" } }; } const routeStop = (_mock.routeStops || []).find( (rs) => Number(rs.busLineId) === Number(schedule.busLineId) && Number(rs.stopOrder) === stopOrder, ); if (!routeStop) { return { statusCode: 404, body: { error: "stop_not_found_for_schedule" } }; } const stop = (_mock.stops || []).find( (s) => Number(s.id) === Number(routeStop.stopId), ); const occurrenceDateKey = req.query.date || req.body.date || toDateKey(schedule.startTime); const templateStart = new Date(schedule.startTime); const scheduledPassedAt = new Date(`${occurrenceDateKey}T00:00:00`); scheduledPassedAt.setHours( templateStart.getHours(), templateStart.getMinutes(), templateStart.getSeconds(), templateStart.getMilliseconds(), ); scheduledPassedAt.setMinutes( scheduledPassedAt.getMinutes() + (Number(routeStop.estimatedArrivalMinutes) || 0), ); 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(), scheduledPassedDate: occurrenceDateKey, 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 && p.scheduledPassedDate === occurrenceDateKey, ).length; if (passedCount >= totalStops) { schedule.status = "completed"; schedule.endTime = actualPassedAt.toISOString(); } else if (schedule.status === "planned") { schedule.status = "in_progress"; } return { statusCode: 200, body: { scheduleId, stopOrder, status: schedule.status, delayMinutes, passedStops: passedCount, totalStops, occurrenceDate: occurrenceDateKey, }, }; } catch (err) { console.error( "POST /driver/schedules/:scheduleId/stops/:stopOrder/passed error:", err, ); reportError({ partofservice: "POST /driver/schedules/:scheduleId/stops/:stopOrder/passed", error: err.stack || err.message }); return { statusCode: 500, body: { error: "internal_error", details: err.message } }; } } async function getPublicScheduleProgress(req) { try { const scheduleId = Number(req.params.scheduleId); if (!Number.isFinite(scheduleId)) { return { statusCode: 400, body: { error: "invalid_schedule_id" } }; } const schedule = (_mock.schedules || []).find( (s) => Number(s.id) === scheduleId, ); if (!schedule) { return { statusCode: 404, body: { 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 mockProgressDate = req.query.date || req.query.occurrenceDate || null; let progressEvents = (_mock.scheduleStopProgress || []).filter( (p) => Number(p.scheduleId) === scheduleId, ); if (mockProgressDate) { progressEvents = progressEvents.filter( (p) => p.scheduledPassedDate === mockProgressDate, ); } progressEvents = progressEvents.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 occurrenceDateKey = req.query.date || req.query.occurrenceDate || toDateKey(schedule.startTime); const stops = routeStops.map((rs) => { const stop = (_mock.stops || []).find( (s) => Number(s.id) === Number(rs.stopId), ); const templateStart = new Date(schedule.startTime); const scheduledTime = new Date(`${occurrenceDateKey}T00:00:00`); scheduledTime.setHours( templateStart.getHours(), templateStart.getMinutes(), templateStart.getSeconds(), templateStart.getMilliseconds(), ); 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 { statusCode: 200, body: { 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, }, }; } catch (err) { console.error("GET /public/schedules/:scheduleId/progress error:", err); reportError({ partofservice: "GET /public/schedules/:scheduleId/progress", error: err.stack || err.message }); return { statusCode: 500, body: { error: "internal_error", details: err.message } }; } } module.exports = { listSchedules, getSchedulesOverview, listRecurringSchedules, getScheduleById, getScheduleStops, markStopPassed, getPublicScheduleProgress, };