feat(transport): add bus route management with capacity-checked student assignments (fixes #262) - #267
Conversation
…nments Adds a transport module covering routes, stops, vehicle and driver details, and per-student assignments. BusRoute embeds an ordered stop list and validates that sequences are unique and contiguous from 1, so the "stop 3 of 7" labelling in the timeline cannot silently mislabel stops after a gap. TransportAssignment carries a partial unique index on an active status, which stops two concurrent requests from both giving a student a live assignment. Seat occupancy is recomputed from the assignment collection on every write rather than incremented in place, and the capacity check re-derives it immediately before saving, so a stale counter cannot let an extra child onto a full bus. Retiring or deleting a route with live assignments is refused, and replacing the stop list is refused when it would strand an assigned student at a stop the bus no longer visits. Students read only their own assignment; the office holds every write path. Closes Sitaram8472#262
Merge note for whoever lands theseThis PR is one of five sibling feature PRs (#267–#271). Each is independent in its own right — separate models, controllers, routers, pages and panels, with no shared logic — but all five register themselves in the same three files:
So the first of the five to merge will go in clean, and the remaining four will then conflict in exactly those three files. Nothing else conflicts. I simulated the full sequential merge locally and verified the combined result. Two things worth passing on:
Verified on the fully merged tree (all five together):
Happy to rebase and push the resolution on this branch as soon as the first sibling lands; just say which order you want them in. |
The route search passed the raw query string to `new RegExp`, so a search term was silently reinterpreted as a pattern. Three consequences. A search of `.*` matched every route rather than routes containing that text. A term like `(a+)+$` triggered catastrophic backtracking — measured at roughly two minutes of pinned CPU for a 33-character query string, which any signed-in student could send. An unbalanced `[` threw out of the handler and surfaced as a 500 instead of a bad-request. Metacharacters are now escaped so the term matches literally, which is what someone typing into a search box expects, and the term is capped at 80 characters.
Related Issue
Closes #262
Description
Adds the transport module the project was missing entirely — routes, stops, vehicle and driver details, and per-student assignments with capacity that is actually enforced.
A student opening
/transportsees their bus as a card (route code, driver name and a tappable phone number, vehicle number, their own pickup and drop stop with times) and the full stop timeline with their stop highlighted. A student with no assignment gets the route catalogue instead, searchable by the stop nearest their home, so they can tell the office which route they need.The transport office gets a Dashboard tab: create and edit routes, reorder the stop list inline, watch a seats-occupied meter per route, assign or remove students, and open a roster grouped by stop in the order the bus actually reaches them.
Things worth a reviewer's attention
Occupancy cannot drift.
seatsOccupiedis recomputed from theTransportAssignmentcollection on every write rather than incremented in place, and the capacity check re-derives it immediately before saving. A stale counter cannot let one extra child onto a full bus. There is also an admin-onlyPOST /recompute-occupancyrepair hatch that rewrites every route's counter from the source data.Stop sequences must be contiguous. The model rejects duplicate sequence numbers and gaps. The gap check matters because the timeline renders "stop 3 of 7" — a gap would silently mislabel every stop after it.
resequenceStops()renumbers from array order so the UI never hand-maintains sequences.Editing stops cannot strand a rider.
PUT /routes/:id/stopsreplaces the whole list, but first checks every live assignment: if the new list drops a stop somebody boards at, it returns409naming the affected students rather than leaving them assigned to a stop the bus no longer visits.One live assignment per student, enforced by a partial unique index on the database rather than an application check, so two concurrent requests cannot both succeed. Retiring or deleting a route with active assignments is refused.
Times are
"HH:MM"strings, not Dates. A stop's pickup time is a recurring wall-clock time, not an instant — a Date would force an arbitrary date part and break the moment the server and the school sit in different timezones.A note on Mongoose 9
While testing this I found that Mongoose 9 has dropped callback-style middleware. A hook written as
pre('validate', function (next) { ... })is silently skipped — the guards inside it never run, and any call tonext(err)throwsnext is not a function. All hooks here are written asasyncfunctions that throw, which is the supported form. Worth knowing before anyone adds a hook elsewhere inbackend/models/, since the failure is silent.Pages / Components Added or Modified
backend/models/BusRoute.js— new; embedded stop array, sequence validation, occupancy virtualsbackend/models/TransportAssignment.js— new; partial unique index on the active assignmentbackend/controllers/transportController.js— new; 12 handlersbackend/routes/transportRoutes.js— new; mounted at/api/transportbackend/server.js— two lines to register the routerfrontend/src/pages/Transport.jsx— new; student-facing page at/transportfrontend/src/components/teacher/TransportPanel.jsx— new; Dashboard tabfrontend/src/App.jsx— lazy import plus the/transportroutefrontend/src/pages/TeacherDashboard.jsx— one tab entryVerification
node --checkpasses on every new backend filenpx eslintis clean on all new frontend files (the one pre-existingrole is assigned but never usedwarning inTeacherDashboard.jsxis onmainalready and is untouched here)HH:MM, out-of-range latitude, single-stop route, identical pickup/drop, and end-date-before-start all reject with the intended message; the happy path andresequenceStops()behave as expectedChecklist
max-h-[90vh])Notes
Purely additive. No existing model, controller, route or component is modified beyond registering the new router and adding the page route. Capacity is enforced in one place, so a future waiting-list feature has one obvious hook.