feat(import-operations): bulk Excel upload for yard-resident empty containers

Empties already sitting in an EDR yard but never entered in the system had
to be typed one at a time. Adds a bulk path: parse the sheet in the browser
(all-or-nothing, row-numbered errors), preview it, then POST one batch.

The server rejects the batch if any container already has a non-COMPLETED
return, so re-uploading the same sheet cannot duplicate boxes. No interchange
notification fires — these are historical rows, not a live handover.

Company is an Autocomplete over registered customers that also accepts a
typed name, since a backfilled box may belong to a company that is not a
customer yet. Exact name match sets customer_id; the name always lands in the
new empty_container_returns.company_name.

Also fixes the single Record Return modal, which collected Yard and Zone and
then dropped them before the API call, and did not invalidate the returns
list after a standalone return.
This commit is contained in:
Hagernesh
2026-08-28 11:31:50 +00:00
parent 132374ed03
commit 3f4fd8648a
16 changed files with 1075 additions and 44 deletions

View File

@@ -895,6 +895,28 @@ export class TrainSchedulingController {
return res.send(buffer);
}
@Get("schedules/:id/marshalling/stops")
@TrainSchedulingView()
@ApiOperation({ summary: "Corridor stops with a logged consist change, in order (Marshalling 2, 3, 4…)" })
marshallingStops(@Param("id", ParseUUIDPipe) id: string) {
return this.trainSchedulingService.marshallingStops(id);
}
@Get("schedules/:id/marshalling/document/:stopIndex")
@TrainSchedulingView()
@ApiOperation({ summary: "Download the numbered marshalling PDF for one corridor stop" })
async marshallingDocumentAt(
@Param("id", ParseUUIDPipe) id: string,
@Param("stopIndex", ParseIntPipe) stopIndex: number,
@Res() res: Response,
) {
const { filename, buffer } = await this.trainSchedulingService.marshallingDocumentAt(id, stopIndex);
res.setHeader("Content-Type", "application/pdf");
res.setHeader("Content-Disposition", `inline; filename="${filename}"`);
res.setHeader("Content-Length", buffer.length);
return res.send(buffer);
}
// ---- batch / booking-window staff actions ----
@Post("schedules/:id/run-batch")