Files
Sven laptop 7d55580d87 Refactor transit_api into a repo/routes module split
Split the 5,441-line index.js into an 86-line bootstrap, thin domain
route modules, shared lib/ helpers, and a repo/ layer that isolates the
DEV_MOCK branches (repo/*.mock.js). Repo methods return {statusCode, body};
routes are one-line wrappers. Pin search_path to "transit.bus" and
document the /bus/ prefix with legacy root aliases (Deprecation header).

Parity verified: 57/57 byte-identical on the stub differential harness
plus 24/24 live read-only responses vs the original against real
PostgreSQL. The only intentional behavior change: mock reorder/delete
bus-line endpoints no longer require a live DB (previously 500 in
DEV_MOCK=1 without PG). DEV_MOCK gate and node --check clean.
2026-09-24 20:18:32 +02:00

390 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { store: _mock } = require("../mock/store");
const { calculateDepartureTimes, expandRecurringScheduleRows, scheduleMatchesFilters, parseScheduleFilters } = require("../lib/schedule");
const { reportError } = require("../lib/report-error");
// Admin schedules repository methods (DEV_MOCK mode). These are exact copies of
// the original handlers’ mock branches, returning { statusCode, body }.
async function listAdminSchedules(req) {
try {
const templatesOnly =
String(
req.query.templates || req.query.includeTemplates || "",
).toLowerCase() === "true" ||
String(req.query.templates || req.query.includeTemplates || "") === "1";
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 rawSchedules = (_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 {
...s,
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: s.driverId || null,
estimatedDurationMinutes: busLine?.estimatedDurationMinutes || 60,
};
});
const schedules = (
templatesOnly
? rawSchedules
: expandRecurringScheduleRows(rawSchedules)
)
.filter((row) =>
scheduleMatchesFilters(
row,
parseScheduleFilters(req.query),
routeStopsByLine,
),
)
.map((s) => ({
...s,
departureTimes: calculateDepartureTimes(
s.startTime,
s.busLineId,
_mock,
),
}))
.sort((a, b) => new Date(a.startTime) - new Date(b.startTime));
return { statusCode: 200, body: { schedules } };
} catch (err) {
console.error("GET /admin/schedules error:", err);
reportError({ partofservice: "GET /admin/schedules", error: err.stack || err.message });
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
}
}
async function createAdminSchedule(req) {
try {
const { busLineId, busId, driverId, startTime, weekdays } = req.body;
if (!busLineId || !busId || !startTime) {
return {
statusCode: 400,
body: {
error:
"missing_required_fields: busLineId, busId, startTime required",
},
};
}
const scheduleWeekdays =
Array.isArray(weekdays) && weekdays.length > 0
? weekdays.map((w) => Number(w)).filter((w) => w >= 0 && w <= 6)
: [];
if (!_mock.schedules) _mock.schedules = [];
const busLine = _mock.busLines.find((bl) => bl.id == busLineId);
if (!busLine) {
return { statusCode: 404, body: { error: "bus_line_not_found" } };
}
const startDate = new Date(startTime);
const endDate = new Date(startDate);
endDate.setMinutes(
endDate.getMinutes() + (busLine.estimatedDurationMinutes || 60),
);
const departureTimes = calculateDepartureTimes(
startTime,
Number(busLineId),
_mock,
);
const schedule = {
id: _mock.nextScheduleId || 1,
busLineId,
busId,
driverId: driverId ? Number(driverId) : null,
startTime,
endTime: endDate.toISOString(),
weekdays: scheduleWeekdays,
departureTimes,
status: "planned",
createdAt: new Date().toISOString(),
};
_mock.nextScheduleId = (_mock.nextScheduleId || 1) + 1;
_mock.schedules.push(schedule);
return { statusCode: 201, body: { schedule } };
} 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 updateAdminSchedule(req) {
try {
const { scheduleId } = req.params;
const { busLineId, busId, driverId, startTime, weekdays } = req.body;
if (!scheduleId || !Number.isFinite(Number(scheduleId))) {
return { statusCode: 400, body: { error: "invalid_schedule_id" } };
}
if (!busLineId || !busId || !startTime) {
return {
statusCode: 400,
body: {
error:
"missing_required_fields: busLineId, busId, startTime required",
},
};
}
const scheduleWeekdays =
Array.isArray(weekdays) && weekdays.length > 0
? weekdays.map((w) => Number(w)).filter((w) => w >= 0 && w <= 6)
: [];
const index = (_mock.schedules || []).findIndex(
(s) => Number(s.id) === Number(scheduleId),
);
if (index === -1) {
return { statusCode: 404, body: { error: "schedule_not_found" } };
}
const busLine = _mock.busLines.find((bl) => bl.id == busLineId);
if (!busLine) {
return { statusCode: 404, body: { error: "bus_line_not_found" } };
}
const startDate = new Date(startTime);
const endDate = new Date(startDate);
endDate.setMinutes(
endDate.getMinutes() + (busLine.estimatedDurationMinutes || 60),
);
_mock.schedules[index] = {
..._mock.schedules[index],
busLineId: Number(busLineId),
busId: Number(busId),
driverId: driverId ? Number(driverId) : null,
startTime,
endTime: endDate.toISOString(),
weekdays: scheduleWeekdays,
};
return { statusCode: 200, body: { schedule: _mock.schedules[index] } };
} 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 bulkCreateAdminSchedules(req) {
const requestedSchedules = Array.isArray(req.body?.schedules)
? req.body.schedules
: [];
if (requestedSchedules.length === 0) {
return {
statusCode: 400,
body: { error: "missing_required_fields: schedules array required" },
};
}
const normalizedSchedules = requestedSchedules.map((schedule, index) => {
const busLineId = Number(schedule?.busLineId);
const busId = Number(schedule?.busId);
const driverId =
schedule?.driverId !== null &&
schedule?.driverId !== undefined &&
schedule?.driverId !== ""
? Number(schedule.driverId)
: null;
const startTime = schedule?.startTime ? String(schedule.startTime) : "";
const weekdays =
Array.isArray(schedule?.weekdays) && schedule.weekdays.length > 0
? schedule.weekdays
.map((day) => Number(day))
.filter((day) => day >= 0 && day <= 6)
: [];
if (
!Number.isFinite(busLineId) ||
!Number.isFinite(busId) ||
!startTime
) {
throw new Error(`invalid_schedule_entry:${index}`);
}
return { busLineId, busId, driverId, startTime, weekdays };
});
try {
if (!_mock.schedules) _mock.schedules = [];
const createdSchedules = [];
for (const entry of normalizedSchedules) {
const busLine = _mock.busLines.find((bl) => bl.id == entry.busLineId);
if (!busLine) {
return { statusCode: 404, body: { error: "bus_line_not_found" } };
}
const startDate = new Date(entry.startTime);
const endDate = new Date(startDate);
endDate.setMinutes(
endDate.getMinutes() + (busLine.estimatedDurationMinutes || 60),
);
const departureTimes = calculateDepartureTimes(
entry.startTime,
Number(entry.busLineId),
_mock,
);
const schedule = {
id: _mock.nextScheduleId || 1,
busLineId: entry.busLineId,
busId: entry.busId,
driverId: entry.driverId,
startTime: entry.startTime,
endTime: endDate.toISOString(),
weekdays: entry.weekdays,
departureTimes,
status: "planned",
createdAt: new Date().toISOString(),
};
_mock.nextScheduleId = (_mock.nextScheduleId || 1) + 1;
_mock.schedules.push(schedule);
createdSchedules.push(schedule);
}
return {
statusCode: 201,
body: {
createdCount: createdSchedules.length,
schedules: createdSchedules,
},
};
} catch (err) {
console.error(err);
reportError({ partofservice: "POST /admin/schedules/bulk", error: err.stack || err.message });
const errorMessage =
err.message === "bus_line_not_found"
? "bus_line_not_found"
: err.message.startsWith("invalid_schedule_entry:")
? err.message
: "internal_error";
const statusCode =
errorMessage === "bus_line_not_found"
? 404
: errorMessage.startsWith("invalid_schedule_entry:")
? 400
: 500;
return { statusCode, body: { error: errorMessage, details: err.message } };
}
}
async function deleteAdminSchedule(req) {
try {
const { scheduleId } = req.params;
if (!scheduleId || !Number.isFinite(Number(scheduleId))) {
return { statusCode: 400, body: { error: "invalid_schedule_id" } };
}
if (!_mock.schedules) _mock.schedules = [];
const index = _mock.schedules.findIndex(
(s) => Number(s.id) === Number(scheduleId),
);
if (index === -1) {
return { statusCode: 404, body: { error: "schedule_not_found" } };
}
const deleted = _mock.schedules.splice(index, 1)[0];
return { statusCode: 200, body: { success: true, schedule: deleted } };
} 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 assignSchedules(req) {
try {
const { assignments, busLineId, busId, weekdays = [] } = req.body;
if (!Array.isArray(assignments)) {
return { statusCode: 400, body: { error: "missing_assignments" } };
}
const schedulesToCreate = [];
let driverIndex = 0;
for (const assignment of assignments) {
if (!assignment.time || !assignment.assignedDriverId) continue;
const startTime = assignment.time;
const endTime = assignment.endTime;
schedulesToCreate.push({
busLineId,
busId,
driverId: assignment.assignedDriverId,
startTime,
endTime,
weekdays,
});
}
if (!_mock.schedules) _mock.schedules = [];
const createdSchedules = [];
for (const entry of schedulesToCreate) {
const schedule = {
id: _mock.nextScheduleId || 1,
...entry,
status: "planned",
createdAt: new Date().toISOString(),
};
_mock.nextScheduleId = (_mock.nextScheduleId || 1) + 1;
_mock.schedules.push(schedule);
createdSchedules.push(schedule);
}
return {
statusCode: 201,
body: {
createdCount: createdSchedules.length,
schedules: createdSchedules,
},
};
} catch (err) {
console.error("/admin/assign-schedules error:", err);
reportError({ partofservice: "/admin/assign-schedules", error: err.stack || err.message });
return { statusCode: 500, body: { error: "internal_error", details: err.message } };
}
}
module.exports = {
listAdminSchedules,
createAdminSchedule,
updateAdminSchedule,
bulkCreateAdminSchedules,
deleteAdminSchedule,
assignSchedules,
};