feat: enhance booking and audit log functionalities

- Implemented read-only locking for customer-requested container sizes and billing currency in the GlCreateBookingForm component.
- Added functionality to lock partner quantities based on shipment requests in the ConsolidationPartnerPanel.
- Introduced a new Leave action in the LogPassYardWorkModal to unassign bookings from trains.
- Enhanced the AuditLogsPage to support filtering by action and added a Go button for direct navigation to entity detail pages.
- Updated WagonCancellationsPage to handle odd-20ft credits requiring partner selection during rebooking.
- Improved TrainScheduleV2DetailPage to allow manual loading of cargo and display warnings for unassigned bookings.
- Added a new reference field to the audit logs for better searchability and tracking of actions.
- Created a migration to add the reference column to the audit logs table and established an index for efficient querying.
- Defined a registry for audit reference sources to streamline the retrieval of human identifiers for various entities.
This commit is contained in:
Marshal
2026-08-24 23:49:20 +00:00
parent 2a107e8ba3
commit d5a5085d6d
28 changed files with 1356 additions and 86 deletions

View File

@@ -133,8 +133,14 @@ export default function TrainScheduleV2DetailPage() {
// Actual departure — staff often dispatch on paper first and record it later,
// so the time is picked (defaults to now when the dialog opens).
const [dispatchAt, setDispatchAt] = useState<Date | null>(null);
// Loading is manual: dispatch decides the fate of every unloaded origin
// boarder — checked = loaded and departs, unchecked = left behind (wagon
// freed, booking back to the pool). Default unchecked; government bookings
// cannot be removed from a train so they are forced on.
const [dispatchLoadedIds, setDispatchLoadedIds] = useState<Set<string>>(new Set());
const openDispatchConfirm = () => {
setDispatchAt(new Date());
setDispatchLoadedIds(new Set());
setDispatchConfirmOpen(true);
};
const [switchTarget, setSwitchTarget] = useState<EligibleContainerBooking | null>(null);
@@ -465,6 +471,25 @@ export default function TrainScheduleV2DetailPage() {
// per yard from the track page's log-pass flow. Everything below is advisory.
const hasDispatchWarnings =
unassignedCount > 0 || unloadedCount > 0 || intercityNotLoadedCount > 0;
// Unloaded boarders at the TRAIN's origin — the dispatch dialog's manual
// load/leave list. Mirrors the API's unloadedOriginBoarderIds predicate
// (plus government, which is shown but forced-loaded).
const originYardId = schedule.originStation?.id;
const pendingOriginBoarders = dispatchBookings.filter(
(b) =>
Boolean(b.originYardId) &&
b.originYardId === originYardId &&
!b.loadedAt &&
(b.loadingStatus ?? "UNLOADED") !== "LOADED" &&
(b.isGovernment
? b.status === "APPROVED" || b.status === "PAID"
: b.status === "PAID" ||
// Shipping-line bookings ride from accept on the credit ledger.
(Boolean(b.shippingLineCompanyId) && b.status === "FULLY_EXECUTED")),
);
const dispatchLeftCount = pendingOriginBoarders.filter(
(b) => !b.isGovernment && !dispatchLoadedIds.has(b.id),
).length;
const finalizeStep = hasContainerStep ? 3 : 2;
const canModifyBookings = canEditBookings && !["DISPATCHED", "ARRIVED"].includes(schedule.status);
@@ -516,7 +541,12 @@ export default function TrainScheduleV2DetailPage() {
try {
await dispatch.mutateAsync({
id: scheduleId,
payload: dispatchAt ? { actualDepartureAt: dispatchAt.toISOString() } : {},
payload: {
...(dispatchAt ? { actualDepartureAt: dispatchAt.toISOString() } : {}),
loadedBookingIds: pendingOriginBoarders
.filter((b) => b.isGovernment || dispatchLoadedIds.has(b.id))
.map((b) => b.id),
},
});
await openMarshallingDocument({
title: "Train dispatched",
@@ -1524,6 +1554,48 @@ export default function TrainScheduleV2DetailPage() {
radius="md"
/>
{pendingOriginBoarders.length > 0 ? (
<Stack gap={6}>
<Text size="sm" fw={700}>
Cargo boarding at {schedule.originStation?.label ?? "the origin yard"}
tick what was loaded
</Text>
<Text size="xs" c="dimmed">
Unticked bookings are left behind: removed from this train, their
wagons freed, and the booking returned to the pool for a later
schedule. The customer is notified.
</Text>
<Stack gap={6} mah={220} style={{ overflowY: "auto" }}>
{pendingOriginBoarders.map((b) => (
<Checkbox
key={b.id}
size="sm"
checked={b.isGovernment || dispatchLoadedIds.has(b.id)}
disabled={b.isGovernment}
onChange={(e) => {
const next = new Set(dispatchLoadedIds);
if (e.currentTarget.checked) next.add(b.id);
else next.delete(b.id);
setDispatchLoadedIds(next);
}}
label={
<Text size="sm" span>
{b.reference ?? b.id.slice(0, 8)} {b.customer ?? "Unknown customer"}
{b.isGovernment ? " (government — always rides)" : ""}
</Text>
}
/>
))}
</Stack>
{dispatchLeftCount > 0 ? (
<Text size="xs" c="orange.7" fw={600}>
{dispatchLeftCount} booking{dispatchLeftCount === 1 ? "" : "s"} will
be left behind and returned to the booking pool.
</Text>
) : null}
</Stack>
) : null}
{hasDispatchWarnings ? (
<Alert
color="orange"