feat: add wagon usage computation and maintenance logging features

- Implemented  utility to calculate wagon usage metrics for train schedules.
- Created  for sending wagons to maintenance with optional notes.
- Added unit tests for train builder maintenance functionalities, including formatting train run labels and building maintenance notes.
- Developed  component for merging train schedules with detailed previews and reasons for merging.
- Introduced  component for selecting wagons with search functionality and selection limits.
- Created  for displaying and filtering audit logs, including detailed views of individual log entries.
- Added  for handling API interactions related to audit logs, including fetching logs and entity types.
This commit is contained in:
marshalyordanos
2026-08-12 09:36:50 +03:00
parent 35e5404b41
commit 5da36eb128
77 changed files with 6275 additions and 296 deletions

View File

@@ -1267,18 +1267,18 @@ export class BookingsController {
@Post(':id/clearance/delivery-order')
@BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
@UseInterceptors(FileInterceptor('file'))
@UseInterceptors(AnyFilesInterceptor())
@ApiConsumes('multipart/form-data')
async uploadBookingDeliveryOrder(
@Param('id', ParseUUIDPipe) id: string,
@UploadedFile() file: Express.Multer.File,
@UploadedFiles() files: Express.Multer.File[],
@Body('vesselArrivalDate') vesselArrivalDate: string | undefined,
@Body('doCollectedDate') doCollectedDate: string | undefined,
@CurrentUser() user: TCurrentUser,
) {
const booking = await this.bookingClearanceService.uploadDeliveryOrder(
id,
file,
files ?? [],
resolveAuthUserId(user),
{ vesselArrivalDate, doCollectedDate },
);
@@ -1287,17 +1287,17 @@ export class BookingsController {
@Post(':id/clearance/release-order')
@BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
@UseInterceptors(FileInterceptor('file'))
@UseInterceptors(AnyFilesInterceptor())
@ApiConsumes('multipart/form-data')
async uploadBookingReleaseOrder(
@Param('id', ParseUUIDPipe) id: string,
@UploadedFile() file: Express.Multer.File,
@UploadedFiles() files: Express.Multer.File[],
@Body('vesselDepartureDate') vesselDepartureDate: string,
@CurrentUser() user: TCurrentUser,
) {
const result = await this.bookingClearanceService.uploadReleaseOrder(
id,
file,
files ?? [],
vesselDepartureDate,
resolveAuthUserId(user),
);

View File

@@ -59,9 +59,11 @@ import { PdfRenderService } from '../billing/documents/pdf-render.service';
import { buildTabularFallbackPdf } from '../billing/documents/styled-pdf.util';
/**
* The allocated train as the backoffice booking detail page needs it: which
* train, its window phase, and both the planned and actual clock. Attached by
* `findById` only when the booking is on a schedule.
* The train as the backoffice booking detail page needs it: which train, its
* window phase, and both the planned and actual clock. Attached by `findById`
* for the allocated train (`train_schedule_id`) or, before the batch engine has
* allocated one, the train the customer picked at day-commit
* (`requested_train_schedule_id`) — see `isRequested`.
*/
export interface TrainScheduleSummary {
id: string;
@@ -74,6 +76,12 @@ export interface TrainScheduleSummary {
actualArrivalAt: string | null;
windowPhase: string | null;
paymentPhaseEndsAt: string | null;
/**
* True when this is the customer's requested train rather than a confirmed
* allocation — the state staff review at OPERATION_REQUEST_PENDING, before
* accepting the operation puts the booking into the batch pool.
*/
isRequested: boolean;
}
/** Paginated booking list: flat `total` (backoffice) + `meta` block (portal). */
@@ -2132,15 +2140,23 @@ export class BookingsService {
// Surface the assigned train's operational status so the portal stepper
// can show the Arrival stage: the booking status stays IN_TRANSIT from
// dispatch until delivery, so arrival is only knowable from the schedule.
if (booking.trainScheduleId) {
// The allocated train, or — before the batch engine has allocated one — the
// train the customer picked at day-commit. Staff reviewing an operation
// request (OPERATION_REQUEST_PENDING) must see which train they are
// accepting onto before they approve, and at that point only the requested
// id is set.
const summarySourceId = booking.trainScheduleId ?? booking.requestedTrainScheduleId;
if (summarySourceId) {
const schedule = await this.dataSource
.getRepository(TrainSchedule)
.findOne({ where: { id: booking.trainScheduleId } });
.findOne({ where: { id: summarySourceId } });
// trainScheduleStatus drives the portal's Arrival stage, so it stays tied
// to a real allocation — a merely requested train has not departed.
(booking as Booking & { trainScheduleStatus?: string | null }).trainScheduleStatus =
schedule?.status ?? null;
// Backoffice staff view: the allocated train's identity and clock, so the
// detail page can state which train the booking rides and when it runs
// without a second round-trip to the schedules API.
booking.trainScheduleId ? (schedule?.status ?? null) : null;
// Backoffice staff view: the train's identity and clock, so the detail
// page can state which train the booking rides and when it runs without a
// second round-trip to the schedules API.
(
booking as Booking & { trainScheduleSummary?: TrainScheduleSummary | null }
).trainScheduleSummary = schedule
@@ -2155,6 +2171,7 @@ export class BookingsService {
actualArrivalAt: schedule.actualArrivalAt?.toISOString() ?? null,
windowPhase: schedule.windowPhase ?? null,
paymentPhaseEndsAt: schedule.paymentPhaseEndsAt?.toISOString() ?? null,
isRequested: !booking.trainScheduleId,
}
: null;
}