mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 02:58:11 +00:00
feat(loading): notify loaded and left-behind containers, surface the CAS
A booking is routinely loaded in parts, and nothing told the customer which containers boarded and which stayed behind. The carriage acceptance sheet was the only record, and it both totalled up cargo still sitting in the yard and lived inside a Warehouse documents bundle that direct truck-to-train cargo has no business in. The sheet now marks each wagon Loaded or Not loaded and totals only the loaded ones. On load, the customer gets an in-app, SMS and email notice carrying the train number, route, departure time and both container lists — capped to a summary on SMS and email, complete in the inbox. Anything left behind also raises a warehouse-desk notice so somebody owns finding it space. That desk is addressed by a new warehouse_inventory:get_notification permission: a recipient selector, not a route guard, so ops can assign who gets pinged without granting access to anything. The GRN notice went out over SMS alone, to whatever phone number the gate clerk typed. Where the receive carries a booking it now resolves the company and delivers in-app, SMS and email, skipping the typed phone so the customer is not texted twice; manual and backlog receives keep the old path.
This commit is contained in:
@@ -40,6 +40,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
|
||||
import { PageContainer, PageHeader, KpiStrip } from "@/components/page";
|
||||
import { extractDownloadErrorMessage } from "@/components/warehouses/options";
|
||||
import type { KpiItem } from "@/components/page";
|
||||
import { EntityLink } from "@/components/detail";
|
||||
import { BookingActionsToolbar } from "@/components/bookings/BookingActionsToolbar";
|
||||
@@ -374,11 +375,9 @@ export default function BookingRequestDetailPage() {
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Carriage acceptance sheet is not available yet",
|
||||
);
|
||||
// Blob response: the JSON reason is inside the Blob, so
|
||||
// the sync path would show only "status code 400".
|
||||
toast.error(await extractDownloadErrorMessage(error));
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -27,6 +27,25 @@ import { ClearanceUploadedDocumentsPanel } from "@/components/contracts/Clearanc
|
||||
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
/**
|
||||
* A `responseType: "blob"` request delivers its JSON error body as a Blob, so
|
||||
* reading `error.message` gives "Request failed with status code 400" instead
|
||||
* of the reason. Read the blob back before falling back.
|
||||
*/
|
||||
async function downloadErrorMessage(error: unknown, fallback: string): Promise<string> {
|
||||
const data = (error as { response?: { data?: unknown } })?.response?.data;
|
||||
if (data instanceof Blob) {
|
||||
try {
|
||||
const parsed = JSON.parse(await data.text()) as { message?: unknown };
|
||||
if (parsed?.message) return String(parsed.message);
|
||||
} catch {
|
||||
/* not JSON — fall through */
|
||||
}
|
||||
}
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
}
|
||||
|
||||
|
||||
import { bookingsService } from "@/services/bookings.service";
|
||||
import type { EmptyContainerReturn } from "@/services/bookings.service";
|
||||
import { saveBlob } from "@/utils/download";
|
||||
@@ -308,6 +327,25 @@ export function DocumentsTab({ booking }: { booking: Freight.IBooking }) {
|
||||
|
||||
// One-click warehouse-document bundle: GRN + gate clearance + handover.
|
||||
const [bundleBusy, setBundleBusy] = useState(false);
|
||||
|
||||
// The carriage acceptance sheet is its own document, not warehouse paperwork:
|
||||
// direct truck-to-train cargo never sees a warehouse, and this sheet IS its
|
||||
// handover record. Hiding it inside the warehouse bundle made it unfindable.
|
||||
const [casBusy, setCasBusy] = useState(false);
|
||||
const downloadCarriageAcceptance = async () => {
|
||||
setCasBusy(true);
|
||||
const ref = booking.reference ?? booking.id;
|
||||
try {
|
||||
const blob = await bookingsService.downloadCarriageAcceptanceSheet(booking.id);
|
||||
saveBlob(blob, `carriage-acceptance-${ref}.pdf`);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
await downloadErrorMessage(error, "Carriage acceptance sheet is not available yet."),
|
||||
);
|
||||
} finally {
|
||||
setCasBusy(false);
|
||||
}
|
||||
};
|
||||
const downloadWarehouseDocuments = async () => {
|
||||
setBundleBusy(true);
|
||||
const ref = booking.reference ?? booking.id;
|
||||
@@ -654,6 +692,24 @@ export function DocumentsTab({ booking }: { booking: Freight.IBooking }) {
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{/* ── Carriage acceptance sheet (its own document) ────────────────── */}
|
||||
<SectionCard>
|
||||
<CardTitle>Carriage acceptance sheet</CardTitle>
|
||||
<Text fz="12.5px" c="dimmed" mt={4} mb="sm">
|
||||
The record of the cargo EDR has accepted for carriage, listing each wagon and the
|
||||
containers on it, and marking which have been loaded.
|
||||
</Text>
|
||||
<Button
|
||||
leftSection={<Download size={16} />}
|
||||
color="edr-green"
|
||||
variant="light"
|
||||
loading={casBusy}
|
||||
onClick={downloadCarriageAcceptance}
|
||||
>
|
||||
Download sheet
|
||||
</Button>
|
||||
</SectionCard>
|
||||
|
||||
{/* ── Warehouse documents (one-click bundle) ──────────────────────── */}
|
||||
<SectionCard>
|
||||
<CardTitle>Warehouse documents</CardTitle>
|
||||
|
||||
Reference in New Issue
Block a user