Merge pull request #1046 from Tria-plc/freight_feature/usermanagement

Freight feature/usermanagement
This commit is contained in:
marshal
2026-07-31 15:30:39 +03:00
committed by GitHub
7 changed files with 380 additions and 469 deletions

View File

@@ -83,6 +83,11 @@ export class BookingJourneyService {
loadedByUserId: userId ?? null,
} as never);
await this.setAllocationStatuses(manager, scheduleId, bookingId, 'LOADED');
// Keep the schedule↔booking link's tracking flag in sync — the dispatch
// readiness warnings and workspace badges read loading_status, not loadedAt.
await manager
.getRepository(TrainScheduleBooking)
.update({ trainScheduleId: scheduleId, bookingId }, { loadingStatus: 'LOADED' });
// The facility handed the cargo over — raise its GRN. No-ops for yards
// without a facility (import/export terminals), which keep their own flow.
await this.facilityHandling.recordHandling(manager, {
@@ -236,7 +241,10 @@ export class BookingJourneyService {
return {
scheduleId,
scheduleStatus: schedule.status,
trainAtYardId: latest?.yardId ?? (schedule.status === 'DISPATCHED' ? null : schedule.originStationId),
// No checkpoint yet ⇒ the train is still at its origin, even just after
// dispatch — assertTrainAtYard allows origin loading in that state, so
// the UI position must agree or origin Load buttons grey out wrongly.
trainAtYardId: latest?.yardId ?? schedule.originStationId,
yards: [...byYard.values()],
};
}

View File

@@ -1977,6 +1977,16 @@ export class TrainSchedulingService {
manager,
);
// Unassign only runs pre-dispatch, so an IN_TRANSIT status here is stale
// (e.g. auto-loaded by an earlier dispatch that was rolled back). Left as
// is, the booking becomes invisible: the eligible pool only admits PAID,
// so it can never be re-added to any train. Revert it to PAID.
if (booking?.status === 'IN_TRANSIT' && !booking.arrivedAt) {
await manager
.getRepository(Booking)
.update(bookingId, { status: 'PAID', loadedAt: null } as never);
}
// Recompute the train-set composition from whatever survives this removal.
// The removed booking's allocations were already deleted above, so any slot
// left with zero allocations was ridden only by this booking — release it
@@ -6407,7 +6417,60 @@ export class TrainSchedulingService {
action: 'BOOKING_REMOVED',
yardLabel: null,
}));
return [...wagonRows, ...bookingRows].sort(
// Per-booking journey events (load at boarding yard / unload at alighting
// yard) — sourced from the booking's own loaded_at/arrived_at stamps, so a
// multi-stop train's disjoint legs (a→b loads then unloads at b while a→c
// rides through) each show as their own row. Append-only: these columns are
// only ever set once per booking, never cleared, so rows never disappear.
const journeyRows: HistoryRow[] = (
await this.dataSource.query(
`SELECT b.id,
b.reference AS "subject",
COALESCE(oy.label, oy.code) AS "yardLabel",
COALESCE(u.username, u.email) AS "actor",
b.loaded_at AS "occurredAt"
FROM freight.bookings b
JOIN freight.train_schedule_bookings tsb
ON tsb.booking_id = b.id AND tsb.train_schedule_id = $1 AND tsb.deleted_at IS NULL
LEFT JOIN freight.yards oy ON oy.id = b.origin_yard_id
LEFT JOIN iam.users u ON u.id = b.loaded_by_user_id
WHERE b.loaded_at IS NOT NULL
AND b.deleted_at IS NULL
ORDER BY b.loaded_at DESC
LIMIT 200`,
[scheduleId],
)
).map((r: Omit<HistoryRow, 'kind' | 'action' | 'note'>) => ({
...r,
kind: 'BOOKING' as const,
action: 'BOOKING_LOADED',
note: null,
}));
const unloadRows: HistoryRow[] = (
await this.dataSource.query(
`SELECT b.id,
b.reference AS "subject",
COALESCE(dy.label, dy.code) AS "yardLabel",
COALESCE(u.username, u.email) AS "actor",
b.arrived_at AS "occurredAt"
FROM freight.bookings b
JOIN freight.train_schedule_bookings tsb
ON tsb.booking_id = b.id AND tsb.train_schedule_id = $1 AND tsb.deleted_at IS NULL
LEFT JOIN freight.yards dy ON dy.id = b.destination_yard_id
LEFT JOIN iam.users u ON u.id = b.arrived_by_user_id
WHERE b.arrived_at IS NOT NULL
AND b.deleted_at IS NULL
ORDER BY b.arrived_at DESC
LIMIT 200`,
[scheduleId],
)
).map((r: Omit<HistoryRow, 'kind' | 'action' | 'note'>) => ({
...r,
kind: 'BOOKING' as const,
action: 'BOOKING_UNLOADED',
note: null,
}));
return [...wagonRows, ...bookingRows, ...journeyRows, ...unloadRows].sort(
(a, b) => new Date(b.occurredAt).getTime() - new Date(a.occurredAt).getTime(),
);
}