mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
@@ -464,6 +464,26 @@ export class BookingsController {
|
||||
res.send(buffer);
|
||||
}
|
||||
|
||||
@Get(':id/carriage-acceptance-sheet')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Download the carriage acceptance sheet (one per booking, lists every allocated wagon)',
|
||||
})
|
||||
async carriageAcceptanceSheet(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@CurrentUser() user: TCurrentUser,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
const booking = await this.bookingsService.findById(id);
|
||||
if (!hasFreightPermission(user, FREIGHT_PERMS.bookings.view)) {
|
||||
await this.bookingsService.assertCustomerCanAccessBooking(user?.id, booking);
|
||||
}
|
||||
const { filename, buffer } = await this.bookingsService.carriageAcceptanceSheet(id);
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
res.send(buffer);
|
||||
}
|
||||
|
||||
@Get(':id/customer-trucks')
|
||||
@ApiOperation({ summary: 'List customer self-haul trucks (multi-truck) for a booking' })
|
||||
async listCustomerTrucks(
|
||||
|
||||
@@ -70,6 +70,23 @@ export interface PaginatedBookings {
|
||||
};
|
||||
}
|
||||
|
||||
/** One wagon line on the carriage acceptance sheet (raw SQL projection). */
|
||||
interface CarriageAcceptanceWagonRow {
|
||||
sequenceNo: number;
|
||||
wagonType: string | null;
|
||||
wagonNumber: string | null;
|
||||
tareWeightTons: string | null;
|
||||
equatedLength: string | null;
|
||||
loadCapacityTons: string | null;
|
||||
allocatedWeightTons: string | null;
|
||||
trainNumber: string | null;
|
||||
departureAt: Date | null;
|
||||
marshalledAt: string | null;
|
||||
arrivalAt: string | null;
|
||||
containerNumbers: string | null;
|
||||
sealNumbers: string | null;
|
||||
}
|
||||
|
||||
const URGENT_PRIORITY_THRESHOLD = 1000;
|
||||
const NEEDS_ACTION_STATUSES = [
|
||||
'SUBMITTED',
|
||||
@@ -208,6 +225,230 @@ export class BookingsService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Carriage acceptance sheet — one per booking, listing every wagon the booking
|
||||
* occupies. Handed to the customer when EDR accepts the cargo (export) and when
|
||||
* the wagons are allocated before marshalling (import), so it is only available
|
||||
* once the booking has wagon allocations.
|
||||
*/
|
||||
async carriageAcceptanceSheet(bookingId: string): Promise<{ filename: string; buffer: Buffer }> {
|
||||
const booking = await this.findById(bookingId);
|
||||
const wagons: CarriageAcceptanceWagonRow[] = await this.dataSource.query(
|
||||
`SELECT tsw.sequence_no AS "sequenceNo",
|
||||
COALESCE(wt.code, wt.name) AS "wagonType",
|
||||
w.wagon_number AS "wagonNumber",
|
||||
wt.tare_weight_tons AS "tareWeightTons",
|
||||
tsw.length_meters AS "equatedLength",
|
||||
tsw.capacity_tons AS "loadCapacityTons",
|
||||
a.allocated_weight_tons AS "allocatedWeightTons",
|
||||
s.train_number AS "trainNumber",
|
||||
s.scheduled_departure_date AS "departureAt",
|
||||
so.label AS "marshalledAt",
|
||||
sd.label AS "arrivalAt",
|
||||
string_agg(DISTINCT ci.container_number, ', ') AS "containerNumbers",
|
||||
string_agg(DISTINCT ci.seal_number, ', ') AS "sealNumbers"
|
||||
FROM freight.wagon_booking_allocations a
|
||||
JOIN freight.train_set_wagons tsw
|
||||
ON tsw.id = a.train_set_wagon_id AND tsw.deleted_at IS NULL
|
||||
LEFT JOIN freight.wagon_types wt ON wt.id = tsw.wagon_type_id
|
||||
LEFT JOIN freight.wagons w ON w.id = tsw.physical_wagon_id
|
||||
LEFT JOIN freight.train_schedules s
|
||||
ON s.train_set_id = tsw.train_set_id AND s.deleted_at IS NULL
|
||||
LEFT JOIN freight.yards so ON so.id = s.origin_station_id
|
||||
LEFT JOIN freight.yards sd ON sd.id = s.destination_station_id
|
||||
LEFT JOIN freight.wagon_allocation_container_items ci
|
||||
ON ci.wagon_booking_allocation_id = a.id AND ci.deleted_at IS NULL
|
||||
WHERE a.booking_id = $1 AND a.deleted_at IS NULL
|
||||
GROUP BY tsw.id, a.id, wt.code, wt.name, w.wagon_number, wt.tare_weight_tons,
|
||||
s.train_number, s.scheduled_departure_date, so.label, sd.label
|
||||
ORDER BY tsw.sequence_no`,
|
||||
[bookingId],
|
||||
);
|
||||
if (wagons.length === 0) {
|
||||
throw new BadRequestException(
|
||||
'No wagons are allocated to this booking yet — the carriage acceptance sheet is issued after wagon allocation',
|
||||
);
|
||||
}
|
||||
|
||||
const html = this.buildCarriageAcceptanceSheetHtml(booking, wagons);
|
||||
const buffer = await this.pdfRender.htmlToPdfBuffer(html, {
|
||||
label: 'carriage acceptance sheet',
|
||||
fallback: (prepared) => buildTabularFallbackPdf(prepared),
|
||||
});
|
||||
return {
|
||||
filename: `carriage-acceptance-${booking.reference.replace(/[^a-zA-Z0-9_-]+/g, '-')}.pdf`,
|
||||
buffer,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the booking amount across its wagons, proportional to allocated weight
|
||||
* (equal shares when no weights are recorded). The last row absorbs the rounding
|
||||
* remainder so the Price column always sums to the Total Amount on the sheet.
|
||||
*/
|
||||
private splitAmountAcrossWagons(total: number, weights: number[]): number[] {
|
||||
const sum = weights.reduce((acc, w) => acc + w, 0);
|
||||
const shares = weights.map((w) =>
|
||||
Math.round((sum > 0 ? (total * w) / sum : total / weights.length) * 100) / 100,
|
||||
);
|
||||
const drift = Math.round((total - shares.reduce((a, b) => a + b, 0)) * 100) / 100;
|
||||
shares[shares.length - 1] = Math.round((shares[shares.length - 1] + drift) * 100) / 100;
|
||||
return shares;
|
||||
}
|
||||
|
||||
private buildCarriageAcceptanceSheetHtml(
|
||||
booking: Booking,
|
||||
wagons: CarriageAcceptanceWagonRow[],
|
||||
): string {
|
||||
const esc = (v: unknown) => this.escapeHtml(String(v ?? '-'));
|
||||
const num = (v: unknown, digits = 3) => (Number(v) || 0).toFixed(digits);
|
||||
const money = (v: number) =>
|
||||
v.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
|
||||
const departureStation = booking.originYard?.label ?? booking.originYard?.code ?? '-';
|
||||
const arrivalStation = booking.destinationYard?.label ?? booking.destinationYard?.code ?? '-';
|
||||
const cargoName = booking.cargoType?.cargoTypeName ?? booking.cargoFreeText ?? '-';
|
||||
const currency = booking.paymentCurrency ?? 'ETB';
|
||||
const totalAmount = Number(booking.adjustedTotalAmount ?? booking.totalAmount) || 0;
|
||||
const prices = this.splitAmountAcrossWagons(
|
||||
totalAmount,
|
||||
wagons.map((w) => Number(w.allocatedWeightTons) || 0),
|
||||
);
|
||||
const header = wagons[0];
|
||||
const sheetDate = header.departureAt ? new Date(header.departureAt) : new Date();
|
||||
|
||||
const totals = wagons.reduce(
|
||||
(acc, w) => ({
|
||||
tare: acc.tare + (Number(w.tareWeightTons) || 0),
|
||||
capacity: acc.capacity + (Number(w.loadCapacityTons) || 0),
|
||||
load: acc.load + (Number(w.allocatedWeightTons) || 0),
|
||||
length: acc.length + (Number(w.equatedLength) || 0),
|
||||
}),
|
||||
{ tare: 0, capacity: 0, load: 0, length: 0 },
|
||||
);
|
||||
// A wagon carrying no weight and no container is running empty under this booking.
|
||||
const fullWagons = wagons.filter(
|
||||
(w) => (Number(w.allocatedWeightTons) || 0) > 0 || Boolean(w.containerNumbers),
|
||||
).length;
|
||||
|
||||
const rows = wagons
|
||||
.map(
|
||||
(w, i) => `<tr>
|
||||
<td class="num">${i + 1}</td>
|
||||
<td>${esc(w.wagonType)}</td>
|
||||
<td>${esc(w.wagonNumber)}</td>
|
||||
<td class="num">${num(w.tareWeightTons, 2)}</td>
|
||||
<td class="num">${num(w.equatedLength)}</td>
|
||||
<td class="num">${num(w.loadCapacityTons)}</td>
|
||||
<td>${esc(arrivalStation)}</td>
|
||||
<td>${esc(cargoName)}</td>
|
||||
<td>${esc(departureStation)}</td>
|
||||
<td>${esc(w.containerNumbers)}</td>
|
||||
<td>${esc(w.sealNumbers)}</td>
|
||||
<td class="num">${money(prices[i])}</td>
|
||||
</tr>`,
|
||||
)
|
||||
.join('');
|
||||
|
||||
return `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Carriage Acceptance Sheet</title>
|
||||
<style>
|
||||
@page { size: A4 landscape; margin: 10mm; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; color: #0f172a; font-family: Arial, sans-serif; }
|
||||
.top { display: flex; justify-content: space-between; border-bottom: 3px solid #0f766e; padding-bottom: 10px; gap: 24px; }
|
||||
.brand { font-size: 11px; color: #475569; text-transform: uppercase; letter-spacing: .08em; font-weight: 700; }
|
||||
h1 { margin: 6px 0 0; font-size: 25px; line-height: 1.05; }
|
||||
.subtitle { font-size: 11px; color: #475569; margin-top: 4px; }
|
||||
.meta { text-align: right; font-size: 11px; color: #475569; min-width: 210px; }
|
||||
.meta strong { display: block; margin-top: 4px; color: #0f172a; font-size: 15px; }
|
||||
.summary { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; margin: 14px 0; }
|
||||
.tile { border: 1px solid #cbd5e1; padding: 8px; min-height: 50px; }
|
||||
.tile span { display: block; color: #64748b; font-size: 9px; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 4px; }
|
||||
.tile strong { font-size: 11px; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th { background: #f8fafc; color: #475569; text-align: left; }
|
||||
th, td { border: 1px solid #cbd5e1; padding: 5px 6px; font-size: 9.5px; vertical-align: top; }
|
||||
.num { text-align: right; }
|
||||
tfoot td { background: #f8fafc; font-weight: 700; }
|
||||
.notice { margin-top: 10px; border-left: 4px solid #0f766e; background: #f0fdfa; padding: 8px 10px; font-size: 10px; color: #134e4a; }
|
||||
.signatures { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-top: 34px; }
|
||||
.line { border-top: 1px solid #334155; padding-top: 7px; font-size: 9px; color: #475569; min-height: 34px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="top">
|
||||
<div>
|
||||
<div class="brand">Ethio-Djibouti Railway S.C.</div>
|
||||
<h1>Carriage Acceptance Sheet</h1>
|
||||
<div class="subtitle">Booking ${esc(booking.reference)} — ${esc(booking.tradeDirection)}</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
Sheet No.
|
||||
<strong>CAS-${esc(booking.reference)}</strong>
|
||||
Generated: ${esc(new Date().toLocaleString('en-GB'))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary">
|
||||
<div class="tile"><span>Marshalled at</span><strong>${esc(header.marshalledAt ?? departureStation)}</strong></div>
|
||||
<div class="tile"><span>Arrival at</span><strong>${esc(header.arrivalAt ?? arrivalStation)}</strong></div>
|
||||
<div class="tile"><span>Date and time</span><strong>${esc(sheetDate.toLocaleString('en-GB'))}</strong></div>
|
||||
<div class="tile"><span>Train No.</span><strong>${esc(header.trainNumber)}</strong></div>
|
||||
<div class="tile"><span>Customer</span><strong>${esc(booking.company?.name)}</strong></div>
|
||||
<div class="tile"><span>Cargo</span><strong>${esc(cargoName)}</strong></div>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="num">SN</th>
|
||||
<th>Type of Wagon</th>
|
||||
<th>Wagon No.</th>
|
||||
<th class="num">Tare Weight</th>
|
||||
<th class="num">Equated Length</th>
|
||||
<th class="num">Load Capacity</th>
|
||||
<th>Arrival Station</th>
|
||||
<th>Cargo Name</th>
|
||||
<th>Departure Station</th>
|
||||
<th>Container No.</th>
|
||||
<th>Seal No.</th>
|
||||
<th class="num">Price (${esc(currency)})</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="3">Total wagons: ${wagons.length} (full ${fullWagons} / empty ${wagons.length - fullWagons})</td>
|
||||
<td class="num">${num(totals.tare, 2)}</td>
|
||||
<td class="num">${num(totals.length)}</td>
|
||||
<td class="num">${num(totals.capacity)}</td>
|
||||
<td colspan="5">Gross weight (tare + load): ${num(totals.tare + totals.load)} T</td>
|
||||
<td class="num">${money(totalAmount)}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<div class="notice">
|
||||
The wagons listed above are accepted for carriage under booking ${esc(booking.reference)}.
|
||||
Wagon identity, container and seal numbers must be verified against the physical consist
|
||||
before the sheet is signed.
|
||||
</div>
|
||||
|
||||
<div class="signatures">
|
||||
<div class="line">Signed by — EDR operations / date</div>
|
||||
<div class="line">Signed by — customer or agent / date</div>
|
||||
<div class="line">Signed by — marshalling yard / date</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
/** Resolve trade direction from yard countries; reject client mismatch. */
|
||||
/**
|
||||
* An intercity corridor is valid when both yards are Ethiopian and at least
|
||||
|
||||
@@ -132,6 +132,8 @@ export const URL_CONSTANTS = {
|
||||
CONTRACT_DOCUMENT: (id: string) => `/bookings/${id}/contract/document`,
|
||||
CONTRACT_SIGN: (id: string) => `/bookings/${id}/contract/sign`,
|
||||
CONTRACT_DOWNLOAD: (id: string) => `/bookings/${id}/contract`,
|
||||
CARRIAGE_ACCEPTANCE_SHEET: (id: string) =>
|
||||
`/bookings/${id}/carriage-acceptance-sheet`,
|
||||
SUMMARY: (id: string) => `/bookings/${id}/summary`,
|
||||
CUSTOMER_SIGN: (id: string) => `/bookings/${id}/customer/sign`,
|
||||
MARKETING_APPROVE: (id: string) => `/bookings/${id}/marketing/approve`,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import toast from "react-hot-toast";
|
||||
import {
|
||||
ArrowLeft,
|
||||
FileSignature,
|
||||
FileText,
|
||||
FolderOpen,
|
||||
Layers,
|
||||
LayoutGrid,
|
||||
@@ -50,6 +52,7 @@ import {
|
||||
useBookingMutations,
|
||||
} from "@/hooks/bookings/useBookings";
|
||||
import { useScrollToHash } from "@/hooks/useScrollToHash";
|
||||
import { bookingsService } from "@/services/bookings.service";
|
||||
|
||||
export default function BookingRequestDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
@@ -269,6 +272,33 @@ export default function BookingRequestDetailPage() {
|
||||
View / sign contract
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
fullWidth
|
||||
variant="default"
|
||||
leftSection={<FileText size={16} />}
|
||||
onClick={async () => {
|
||||
try {
|
||||
const blob =
|
||||
await bookingsService.downloadCarriageAcceptanceSheet(
|
||||
booking.id,
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `carriage-acceptance-${booking.reference}.pdf`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Carriage acceptance sheet is not available yet",
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Carriage acceptance sheet
|
||||
</Button>
|
||||
{booking.customsClearingEnabled && (
|
||||
<Button
|
||||
fullWidth
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fragment, useMemo, useState } from "react";
|
||||
import { Fragment, useMemo, useState, useEffect } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
ActionIcon,
|
||||
@@ -69,6 +69,13 @@ export default function ContainerReturnsPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const { data: returnedContainers = [] } = useQuery({
|
||||
queryKey: ["empty-container-returns"],
|
||||
queryFn: async () => {
|
||||
return await importOperationsService.listEmptyReturns().catch(() => []);
|
||||
},
|
||||
});
|
||||
|
||||
const bookingIds = unloadedQueue.map((item) => item.bookingId).filter(Boolean) as string[];
|
||||
const containerReturnsQuery = useQuery({
|
||||
queryKey: ["container-returns", bookingIds],
|
||||
@@ -249,6 +256,42 @@ export default function ContainerReturnsPage() {
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
{returnedContainers.length > 0 && (
|
||||
<>
|
||||
<Text fw={600} mb="xs">Returned Containers</Text>
|
||||
<Table.ScrollContainer minWidth={1000} mb="lg">
|
||||
<Table striped highlightOnHover verticalSpacing="xs">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Container Number</Table.Th>
|
||||
<Table.Th>Booking Ref</Table.Th>
|
||||
<Table.Th>Returned Date</Table.Th>
|
||||
<Table.Th>Facility</Table.Th>
|
||||
<Table.Th>Yard</Table.Th>
|
||||
<Table.Th>Condition</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{returnedContainers.map((ret: any) => (
|
||||
<Table.Tr key={ret.id}>
|
||||
<Table.Td>{ret.containerNumber}</Table.Td>
|
||||
<Table.Td>{ret.bookingId ? "Associated" : "—"}</Table.Td>
|
||||
<Table.Td>{ret.returnDate ? new Date(ret.returnDate).toLocaleDateString() : "—"}</Table.Td>
|
||||
<Table.Td>{ret.facility || "—"}</Table.Td>
|
||||
<Table.Td>{ret.yard || "—"}</Table.Td>
|
||||
<Table.Td>{ret.condition || "—"}</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="sm">{ret.status}</Badge>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
</>
|
||||
)}
|
||||
|
||||
{filteredGroups.length === 0 ? (
|
||||
<Alert color="gray">No {filterType !== "all" ? filterType : ""} container returns found.</Alert>
|
||||
) : (
|
||||
@@ -517,9 +560,12 @@ function StandaloneReturnModal({ opened, onClose, onSubmit, loading }: Standalon
|
||||
const [containerNumber, setContainerNumber] = useState<string>("");
|
||||
const [returnDate, setReturnDate] = useState<string>(new Date().toISOString().split("T")[0]);
|
||||
const [warehouse, setWarehouse] = useState<string | null>(null);
|
||||
const [yard, setYard] = useState<string>("");
|
||||
const [zone, setZone] = useState<string>("");
|
||||
const [condition, setCondition] = useState<string>("");
|
||||
const [handoverNote, setHandoverNote] = useState<string>("");
|
||||
|
||||
// Auto-populate yard and zone from selected warehouse
|
||||
const { data: warehousesResponse } = useQuery({
|
||||
queryKey: ["warehouses-list"],
|
||||
queryFn: async () => {
|
||||
@@ -528,6 +574,18 @@ function StandaloneReturnModal({ opened, onClose, onSubmit, loading }: Standalon
|
||||
});
|
||||
|
||||
const warehouses = (warehousesResponse as any)?.data ?? warehousesResponse ?? [];
|
||||
const selectedWarehouseData = warehouse && Array.isArray(warehouses) ? warehouses.find((wh: any) => wh.id === warehouse) : null;
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedWarehouseData) {
|
||||
setYard(selectedWarehouseData.yard || selectedWarehouseData.code || "");
|
||||
setZone(selectedWarehouseData.zone || "");
|
||||
} else {
|
||||
setYard("");
|
||||
setZone("");
|
||||
}
|
||||
}, [selectedWarehouseData]);
|
||||
|
||||
const warehouseOptions = Array.isArray(warehouses)
|
||||
? warehouses.map((wh: any) => ({
|
||||
value: wh.id,
|
||||
@@ -562,6 +620,8 @@ function StandaloneReturnModal({ opened, onClose, onSubmit, loading }: Standalon
|
||||
setContainerNumber("");
|
||||
setReturnDate(new Date().toISOString().split("T")[0]);
|
||||
setWarehouse(null);
|
||||
setYard("");
|
||||
setZone("");
|
||||
setCondition("");
|
||||
setHandoverNote("");
|
||||
onClose();
|
||||
@@ -592,6 +652,22 @@ function StandaloneReturnModal({ opened, onClose, onSubmit, loading }: Standalon
|
||||
searchable
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Yard"
|
||||
placeholder="Auto-populated from warehouse"
|
||||
value={yard}
|
||||
disabled
|
||||
readOnly
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label="Zone"
|
||||
placeholder="Auto-populated from warehouse"
|
||||
value={zone}
|
||||
disabled
|
||||
readOnly
|
||||
/>
|
||||
|
||||
<input
|
||||
type="date"
|
||||
value={returnDate}
|
||||
|
||||
@@ -455,6 +455,13 @@ export const bookingsService = {
|
||||
return (unwrap(response.data) ?? []) as BookingDetail[];
|
||||
},
|
||||
|
||||
downloadCarriageAcceptanceSheet: async (id: string): Promise<Blob> => {
|
||||
const response = await client.get(B.CARRIAGE_ACCEPTANCE_SHEET(id), {
|
||||
responseType: "blob",
|
||||
});
|
||||
return ensurePdfBlob(response.data as Blob);
|
||||
},
|
||||
|
||||
getDjClearanceQueue: async (): Promise<BookingDetail[]> => {
|
||||
const response = await client.get(B.CLEARANCE_DJ_QUEUE);
|
||||
return (unwrap(response.data) ?? []) as BookingDetail[];
|
||||
|
||||
Reference in New Issue
Block a user