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.
This commit is contained in:
@@ -0,0 +1,606 @@
|
||||
// Pure schedule/conflict helpers (moved from index.js — do not add db or http deps).
|
||||
|
||||
|
||||
// ============================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Calculate departure times for each stop in a route
|
||||
* Returns array of { order, stopName, departureTime }
|
||||
*/
|
||||
function calculateDepartureTimes(startTime, busLineId, mock) {
|
||||
const startDate = new Date(startTime);
|
||||
|
||||
// Get route stops for this bus line
|
||||
const routeStops = mock.routeStops
|
||||
.filter((rs) => rs.busLineId === busLineId)
|
||||
.sort((a, b) => a.stopOrder - b.stopOrder);
|
||||
|
||||
return routeStops.map((rs) => {
|
||||
const departureDate = new Date(startDate);
|
||||
// Add estimated arrival minutes to get departure time
|
||||
departureDate.setMinutes(
|
||||
departureDate.getMinutes() + (rs.estimatedArrivalMinutes || 0),
|
||||
);
|
||||
|
||||
const stop = mock.stops.find((s) => s.id === rs.stopId);
|
||||
return {
|
||||
order: rs.stopOrder,
|
||||
stopId: rs.stopId,
|
||||
stopName: stop?.name || "Unknown Stop",
|
||||
estimatedArrivalMinutes: rs.estimatedArrivalMinutes || 0,
|
||||
departureTime: departureDate.toISOString(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function parseWeekdaysValue(value) {
|
||||
if (!value) return [];
|
||||
if (Array.isArray(value))
|
||||
return value.map((v) => Number(v)).filter((v) => Number.isFinite(v));
|
||||
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return [];
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed.map((v) => Number(v)).filter((v) => Number.isFinite(v));
|
||||
}
|
||||
} catch (_) {
|
||||
// Fall through to CSV/PG-array parsing.
|
||||
}
|
||||
|
||||
// Handles values like "1,2,3" or "{1,2,3}".
|
||||
const normalized = trimmed.replace("{", "").replace("}", "");
|
||||
return normalized
|
||||
.split(",")
|
||||
.map((v) => Number(v.trim()))
|
||||
.filter((v) => Number.isFinite(v));
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
function toNumberOrNull(value) {
|
||||
if (value === undefined || value === null || value === "") return null;
|
||||
const numberValue = Number(value);
|
||||
return Number.isFinite(numberValue) ? numberValue : null;
|
||||
}
|
||||
|
||||
|
||||
function parseCommaSeparatedValues(value) {
|
||||
if (!value && value !== 0) return [];
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => String(item).trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
return String(value)
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
|
||||
function toDateKey(value) {
|
||||
if (!value) return null;
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return null;
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
|
||||
function expandRecurringScheduleRows(rows) {
|
||||
const expandedRows = [];
|
||||
|
||||
for (const row of rows) {
|
||||
const weekdays = parseWeekdaysValue(row.weekdays);
|
||||
if (!weekdays || weekdays.length === 0) {
|
||||
expandedRows.push(row);
|
||||
continue;
|
||||
}
|
||||
|
||||
const templateDate = new Date(row.start_time);
|
||||
if (Number.isNaN(templateDate.getTime())) {
|
||||
expandedRows.push(row);
|
||||
continue;
|
||||
}
|
||||
|
||||
const templateDayOfWeek = templateDate.getDay();
|
||||
const sortedWeekdays = [...new Set(weekdays)].sort((a, b) => a - b);
|
||||
const durationMinutes = Number(
|
||||
row.estimated_duration_minutes ?? row.estimatedDurationMinutes ?? 60,
|
||||
);
|
||||
|
||||
for (let week = 0; week < 52; week++) {
|
||||
for (const targetWeekday of sortedWeekdays) {
|
||||
const daysOffset = (targetWeekday - templateDayOfWeek + 7) % 7;
|
||||
const totalDays = week * 7 + daysOffset;
|
||||
|
||||
const scheduleDate = new Date(templateDate);
|
||||
scheduleDate.setDate(scheduleDate.getDate() + totalDays);
|
||||
|
||||
const endDate = new Date(scheduleDate);
|
||||
endDate.setMinutes(endDate.getMinutes() + durationMinutes);
|
||||
|
||||
expandedRows.push({
|
||||
...row,
|
||||
start_time: scheduleDate.toISOString(),
|
||||
end_time: endDate.toISOString(),
|
||||
is_recurring_instance: true,
|
||||
template_id: row.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return expandedRows;
|
||||
}
|
||||
|
||||
|
||||
function mapAdminScheduleRow(row, routeStopsByLine = new Map()) {
|
||||
const weekdays = parseWeekdaysValue(row.weekdays);
|
||||
const busLineId = Number(row.busLineId ?? row.bus_line_id);
|
||||
const startTime = row.startTime ?? row.start_time;
|
||||
const routeStops = routeStopsByLine.get(busLineId) || [];
|
||||
const departureTimes = buildDepartureTimes(startTime, routeStops);
|
||||
|
||||
return {
|
||||
id: Number(row.id),
|
||||
busLineId,
|
||||
busId: Number(row.busId ?? row.bus_id),
|
||||
driverId:
|
||||
row.driverId !== null && row.driverId !== undefined
|
||||
? Number(row.driverId)
|
||||
: null,
|
||||
driverName: row.driverName ?? row.driver_name ?? null,
|
||||
startTime,
|
||||
endTime: row.endTime ?? row.end_time,
|
||||
weekdays,
|
||||
departureTimes,
|
||||
status: row.status,
|
||||
lineNumber: Number(row.lineNumber ?? row.line_number) || null,
|
||||
busName: row.busName ?? row.bus_name ?? "Unknown Bus",
|
||||
licensePlate: row.licensePlate ?? row.license_plate ?? null,
|
||||
createdAt: row.createdAt ?? row.created_at ?? null,
|
||||
isRecurringInstance: row.is_recurring_instance || false,
|
||||
templateId: row.template_id || row.templateId || null,
|
||||
recurrenceLabel: formatWeekdayLabel(weekdays),
|
||||
isRecurring: weekdays.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function scheduleMatchesFilters(
|
||||
schedule,
|
||||
filters,
|
||||
routeStopsByLine = new Map(),
|
||||
) {
|
||||
if (!schedule || !filters) return true;
|
||||
|
||||
const scheduleBusLineId = Number(schedule.bus_line_id ?? schedule.busLineId);
|
||||
const scheduleBusId = Number(schedule.bus_id ?? schedule.busId);
|
||||
const scheduleDriverId = Number(schedule.driver_id ?? schedule.driverId);
|
||||
const scheduleStatus = String(schedule.status || "");
|
||||
const scheduleDateKey = toDateKey(schedule.start_time ?? schedule.startTime);
|
||||
|
||||
if (filters.busLineId !== null && scheduleBusLineId !== filters.busLineId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filters.busId !== null && scheduleBusId !== filters.busId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filters.driverId !== null && scheduleDriverId !== filters.driverId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
filters.statuses.length > 0 &&
|
||||
!filters.statuses.includes(scheduleStatus)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filters.date && scheduleDateKey !== filters.date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
filters.fromDate &&
|
||||
scheduleDateKey &&
|
||||
scheduleDateKey < filters.fromDate
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filters.toDate && scheduleDateKey && scheduleDateKey > filters.toDate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filters.stopId !== null) {
|
||||
const routeStops = routeStopsByLine.get(scheduleBusLineId) || [];
|
||||
const matchesStop = routeStops.some(
|
||||
(routeStop) =>
|
||||
Number(routeStop.stop_id ?? routeStop.stopId) === filters.stopId,
|
||||
);
|
||||
if (!matchesStop) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function parseScheduleFilters(query, options = {}) {
|
||||
const statuses = parseCommaSeparatedValues(
|
||||
query.status || query.statuses || options.statuses || [],
|
||||
);
|
||||
|
||||
return {
|
||||
date: query.date ? String(query.date).trim() : null,
|
||||
fromDate: query.fromDate ? String(query.fromDate).trim() : null,
|
||||
toDate: query.toDate ? String(query.toDate).trim() : null,
|
||||
driverId: toNumberOrNull(
|
||||
query.driverId ??
|
||||
query.driver_id ??
|
||||
query.chauffeurId ??
|
||||
query.chauffeur_id,
|
||||
),
|
||||
busId: toNumberOrNull(query.busId ?? query.bus_id),
|
||||
busLineId: toNumberOrNull(
|
||||
query.busLineId ?? query.lineId ?? query.bus_line_id,
|
||||
),
|
||||
stopId: toNumberOrNull(
|
||||
query.stopId ?? query.stop_id ?? query.busStopId ?? query.bus_stop_id,
|
||||
),
|
||||
statuses,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function formatWeekdayLabel(weekdays) {
|
||||
const labels = ["Zo", "Ma", "Di", "Wo", "Do", "Vr", "Za"];
|
||||
|
||||
if (!Array.isArray(weekdays) || weekdays.length === 0) {
|
||||
return "Eenmalig";
|
||||
}
|
||||
|
||||
if (weekdays.length === 7) {
|
||||
return "Dagelijks";
|
||||
}
|
||||
|
||||
return [...new Set(weekdays)]
|
||||
.sort((a, b) => a - b)
|
||||
.filter((day) => day >= 0 && day < labels.length)
|
||||
.map((day) => labels[day])
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
|
||||
function buildDepartureTimes(startTime, routeStops = []) {
|
||||
const startDate = new Date(startTime);
|
||||
|
||||
return routeStops.map((routeStop) => {
|
||||
const departureDate = new Date(startDate);
|
||||
const etaMinutes =
|
||||
Number(
|
||||
routeStop.estimated_arrival_minutes ??
|
||||
routeStop.estimatedArrivalMinutes,
|
||||
) || 0;
|
||||
departureDate.setMinutes(departureDate.getMinutes() + etaMinutes);
|
||||
|
||||
return {
|
||||
order: Number(routeStop.stop_order ?? routeStop.stopOrder),
|
||||
stopId: Number(routeStop.stop_id ?? routeStop.stopId),
|
||||
stopName: routeStop.stop_name || routeStop.name || "Unknown Stop",
|
||||
estimatedArrivalMinutes: etaMinutes,
|
||||
departureTime: departureDate.toISOString(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function isRecurringSchedule(schedule) {
|
||||
return parseWeekdaysValue(schedule.weekdays).length > 0;
|
||||
}
|
||||
|
||||
|
||||
function scheduleOccursOnDate(schedule, dateKey) {
|
||||
if (!dateKey) return true;
|
||||
|
||||
const weekdays = parseWeekdaysValue(schedule.weekdays);
|
||||
const scheduleDateKey = toDateKey(schedule.start_time ?? schedule.startTime);
|
||||
|
||||
if (!scheduleDateKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (weekdays.length === 0) {
|
||||
return scheduleDateKey === dateKey;
|
||||
}
|
||||
|
||||
if (scheduleDateKey > dateKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetDate = new Date(`${dateKey}T00:00:00`);
|
||||
return weekdays.includes(targetDate.getDay());
|
||||
}
|
||||
|
||||
|
||||
function materializeScheduleOverviewRow(
|
||||
row,
|
||||
dateKey,
|
||||
routeStopsByLine = new Map(),
|
||||
) {
|
||||
const weekdays = parseWeekdaysValue(row.weekdays);
|
||||
const recurring = weekdays.length > 0;
|
||||
const templateStart = new Date(row.startTime ?? row.start_time);
|
||||
const templateEnd = new Date(row.endTime ?? row.end_time);
|
||||
let startTime = row.startTime ?? row.start_time;
|
||||
let endTime = row.endTime ?? row.end_time;
|
||||
|
||||
if (recurring && dateKey) {
|
||||
const occurrenceStart = new Date(`${dateKey}T00:00:00`);
|
||||
occurrenceStart.setHours(
|
||||
templateStart.getHours(),
|
||||
templateStart.getMinutes(),
|
||||
templateStart.getSeconds(),
|
||||
templateStart.getMilliseconds(),
|
||||
);
|
||||
|
||||
startTime = occurrenceStart.toISOString();
|
||||
|
||||
if (
|
||||
Number.isFinite(templateEnd.getTime()) &&
|
||||
Number.isFinite(templateStart.getTime())
|
||||
) {
|
||||
const durationMillis = templateEnd.getTime() - templateStart.getTime();
|
||||
const occurrenceEnd = new Date(occurrenceStart);
|
||||
occurrenceEnd.setMilliseconds(
|
||||
occurrenceEnd.getMilliseconds() + durationMillis,
|
||||
);
|
||||
endTime = occurrenceEnd.toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
const busLineId = Number(row.busLineId ?? row.bus_line_id);
|
||||
const routeStops = routeStopsByLine.get(busLineId) || [];
|
||||
const departureTimes = buildDepartureTimes(startTime, routeStops);
|
||||
|
||||
return {
|
||||
id: Number(row.id),
|
||||
busLineId,
|
||||
busId: Number(row.busId ?? row.bus_id),
|
||||
// Hide driver information in public schedule materialization for privacy
|
||||
driverId: null,
|
||||
driverName: null,
|
||||
startTime,
|
||||
endTime,
|
||||
weekdays,
|
||||
status: row.status,
|
||||
lineNumber: Number(row.lineNumber ?? row.line_number) || null,
|
||||
busName: row.busName ?? row.bus_name ?? "Unknown Bus",
|
||||
licensePlate: row.licensePlate ?? row.license_plate ?? null,
|
||||
createdAt: row.createdAt ?? row.created_at ?? null,
|
||||
scheduleType: recurring ? "recurring" : "one_time",
|
||||
recurrenceLabel: formatWeekdayLabel(weekdays),
|
||||
isRecurringInstance: recurring && Boolean(dateKey),
|
||||
templateId: row.templateId ?? row.template_id ?? null,
|
||||
departureTimes,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function mapRecurringOccurrenceRows(rows, dateKey, routeStopsByLine) {
|
||||
return rows
|
||||
.filter((row) => scheduleOccursOnDate(row, dateKey))
|
||||
.map((row) =>
|
||||
materializeScheduleOverviewRow(row, dateKey, routeStopsByLine),
|
||||
)
|
||||
.sort((a, b) => new Date(a.startTime) - new Date(b.startTime));
|
||||
}
|
||||
|
||||
|
||||
function materializeScheduleRowsInRange(
|
||||
rows,
|
||||
fromDateKey,
|
||||
toDateKey,
|
||||
routeStopsByLine,
|
||||
) {
|
||||
const occurrences = [];
|
||||
if (!fromDateKey || !toDateKey) return occurrences;
|
||||
|
||||
const fromDate = new Date(`${fromDateKey}T00:00:00`);
|
||||
const toDate = new Date(`${toDateKey}T23:59:59`);
|
||||
|
||||
// Safety cap: don't generate more than 366 days
|
||||
const maxDays = 366;
|
||||
const spanDays = Math.ceil((toDate - fromDate) / (24 * 60 * 60 * 1000));
|
||||
if (spanDays > maxDays) return occurrences;
|
||||
|
||||
for (const row of rows) {
|
||||
const weekdays = parseWeekdaysValue(row.weekdays);
|
||||
|
||||
if (!weekdays || weekdays.length === 0) {
|
||||
const rowDateKey = toDateKey(row.start_time ?? row.startTime);
|
||||
if (rowDateKey && rowDateKey >= fromDateKey && rowDateKey <= toDateKey) {
|
||||
occurrences.push(
|
||||
materializeScheduleOverviewRow(row, rowDateKey, routeStopsByLine),
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// recurring template: generate occurrences per weekday within range
|
||||
const templateStart = new Date(row.start_time ?? row.startTime);
|
||||
for (const wd of [...new Set(weekdays)]) {
|
||||
// find first date >= fromDate that matches weekday wd and is >= templateStart
|
||||
let cursor = new Date(fromDate);
|
||||
const fromWeekday = cursor.getDay();
|
||||
const daysOffset = (wd - fromWeekday + 7) % 7;
|
||||
cursor.setDate(cursor.getDate() + daysOffset);
|
||||
|
||||
// ensure cursor >= templateStart (so recurrence doesn't start before template)
|
||||
if (cursor < templateStart) {
|
||||
// advance by weeks until >= templateStart
|
||||
const diffDays = Math.ceil(
|
||||
(templateStart - cursor) / (24 * 60 * 60 * 1000),
|
||||
);
|
||||
const weeksToAdvance = Math.ceil(diffDays / 7);
|
||||
cursor.setDate(cursor.getDate() + weeksToAdvance * 7);
|
||||
}
|
||||
|
||||
while (cursor <= toDate) {
|
||||
const occurrenceKey = cursor.toISOString().slice(0, 10);
|
||||
occurrences.push(
|
||||
materializeScheduleOverviewRow(row, occurrenceKey, routeStopsByLine),
|
||||
);
|
||||
cursor.setDate(cursor.getDate() + 7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort and return
|
||||
return occurrences.sort(
|
||||
(a, b) => new Date(a.startTime) - new Date(b.startTime),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if two schedules have date/weekday overlap
|
||||
*/
|
||||
function doSchedulesDateOverlap(
|
||||
schedule1Start,
|
||||
schedule1End,
|
||||
schedule1Weekdays,
|
||||
schedule2Start,
|
||||
schedule2End,
|
||||
schedule2Weekdays,
|
||||
) {
|
||||
const s1Start = new Date(schedule1Start);
|
||||
const s1End = new Date(schedule1End);
|
||||
const s2Start = new Date(schedule2Start);
|
||||
const s2End = new Date(schedule2End);
|
||||
|
||||
// Check time overlap
|
||||
if (s1End <= s2Start || s2End <= s1Start) {
|
||||
return false; // No time overlap
|
||||
}
|
||||
|
||||
// If both have no weekdays (one-time), check if same day
|
||||
if (
|
||||
(!schedule1Weekdays || schedule1Weekdays.length === 0) &&
|
||||
(!schedule2Weekdays || schedule2Weekdays.length === 0)
|
||||
) {
|
||||
return s1Start.toDateString() === s2Start.toDateString();
|
||||
}
|
||||
|
||||
// If one has weekdays and other doesn't, check if one-time falls on weekday
|
||||
if (
|
||||
schedule1Weekdays &&
|
||||
schedule1Weekdays.length > 0 &&
|
||||
(!schedule2Weekdays || schedule2Weekdays.length === 0)
|
||||
) {
|
||||
const s2Day = s2Start.getDay(); // 0=Sun, 1=Mon, etc.
|
||||
const s2Adjusted = s2Day === 0 ? 0 : s2Day; // Normalize to 0-6
|
||||
return schedule1Weekdays.includes(s2Adjusted);
|
||||
}
|
||||
|
||||
if (
|
||||
(!schedule1Weekdays || schedule1Weekdays.length === 0) &&
|
||||
schedule2Weekdays &&
|
||||
schedule2Weekdays.length > 0
|
||||
) {
|
||||
const s1Day = s1Start.getDay();
|
||||
const s1Adjusted = s1Day === 0 ? 0 : s1Day;
|
||||
return schedule2Weekdays.includes(s1Adjusted);
|
||||
}
|
||||
|
||||
// Both have weekdays: check if any day overlaps
|
||||
if (schedule1Weekdays && schedule2Weekdays) {
|
||||
return schedule1Weekdays.some((d) => schedule2Weekdays.includes(d));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if a driver has conflicts with existing schedules
|
||||
*/
|
||||
function checkDriverConflict(
|
||||
driverId,
|
||||
startTime,
|
||||
endTime,
|
||||
weekdays,
|
||||
allSchedules,
|
||||
excludeScheduleId = null,
|
||||
) {
|
||||
if (!driverId) return false; // No conflict if no driver specified
|
||||
|
||||
return allSchedules.some((s) => {
|
||||
if (excludeScheduleId && s.id === excludeScheduleId) return false;
|
||||
const sDriverId = s.driverId ?? s.driver_id;
|
||||
if (Number(sDriverId) !== Number(driverId)) return false;
|
||||
|
||||
const sWeekdays = parseWeekdaysValue(s.weekdays);
|
||||
const wkdays = weekdays || [];
|
||||
const sStart = s.startTime ?? s.start_time;
|
||||
const sEnd = s.endTime ?? s.end_time;
|
||||
|
||||
return doSchedulesDateOverlap(
|
||||
startTime,
|
||||
endTime,
|
||||
wkdays,
|
||||
sStart,
|
||||
sEnd,
|
||||
sWeekdays,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if a bus has conflicts with existing schedules
|
||||
*/
|
||||
function checkBusConflict(
|
||||
busId,
|
||||
startTime,
|
||||
endTime,
|
||||
weekdays,
|
||||
allSchedules,
|
||||
excludeScheduleId = null,
|
||||
) {
|
||||
if (!busId) return false;
|
||||
|
||||
return allSchedules.some((s) => {
|
||||
if (excludeScheduleId && s.id === excludeScheduleId) return false;
|
||||
const sBusId = s.busId ?? s.bus_id;
|
||||
if (Number(sBusId) !== Number(busId)) return false;
|
||||
|
||||
const sWeekdays = parseWeekdaysValue(s.weekdays);
|
||||
const wkdays = weekdays || [];
|
||||
const sStart = s.startTime ?? s.start_time;
|
||||
const sEnd = s.endTime ?? s.end_time;
|
||||
|
||||
return doSchedulesDateOverlap(
|
||||
startTime,
|
||||
endTime,
|
||||
wkdays,
|
||||
sStart,
|
||||
sEnd,
|
||||
sWeekdays,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { calculateDepartureTimes, parseWeekdaysValue, toNumberOrNull, parseCommaSeparatedValues, toDateKey, expandRecurringScheduleRows, mapAdminScheduleRow, scheduleMatchesFilters, parseScheduleFilters, formatWeekdayLabel, buildDepartureTimes, isRecurringSchedule, scheduleOccursOnDate, materializeScheduleOverviewRow, mapRecurringOccurrenceRows, materializeScheduleRowsInRange, doSchedulesDateOverlap, checkDriverConflict, checkBusConflict };
|
||||
Reference in New Issue
Block a user