7d55580d87
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.
390 lines
12 KiB
JavaScript
390 lines
12 KiB
JavaScript
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,
|
||
}; |