pERTRUCK EXIT

This commit is contained in:
Hagernesh
2026-07-06 13:16:34 +00:00
parent 551a5fdeed
commit 756ad73e9c
6 changed files with 90 additions and 28 deletions

View File

@@ -38,7 +38,7 @@ export class ContainerReceiptService {
SET received_to_port = true,
received_at = COALESCE(bcu.received_at, NOW()),
updated_at = NOW()
FROM freight.booking_containers bc,
FROM freight.booking_container bc,
freight.customer_truck_containers ctc
WHERE bc.id = bcu.booking_container_id
AND bc.booking_id = $1
@@ -60,7 +60,7 @@ export class ContainerReceiptService {
bcu.received_at AS "receivedAt",
bcu.grn_number AS "grnNumber"
FROM freight.booking_container_units bcu
JOIN freight.booking_containers bc
JOIN freight.booking_container bc
ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL
WHERE bc.booking_id = $1
AND bcu.deleted_at IS NULL
@@ -92,7 +92,7 @@ export class ContainerReceiptService {
const pending: ReceivedUnitRow[] = await manager.query(
`SELECT bcu.id, bcu.container_number AS "containerNumber"
FROM freight.booking_container_units bcu
JOIN freight.booking_containers bc
JOIN freight.booking_container bc
ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL
WHERE bc.booking_id = $1
AND bcu.deleted_at IS NULL
@@ -109,7 +109,7 @@ export class ContainerReceiptService {
const [{ batches }]: Array<{ batches: string }> = await manager.query(
`SELECT COUNT(DISTINCT bcu.grn_number) AS batches
FROM freight.booking_container_units bcu
JOIN freight.booking_containers bc
JOIN freight.booking_container bc
ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL
WHERE bc.booking_id = $1 AND bcu.grn_number IS NOT NULL AND bcu.deleted_at IS NULL`,
[bookingId],
@@ -129,7 +129,7 @@ export class ContainerReceiptService {
const [{ remaining }]: Array<{ remaining: string }> = await manager.query(
`SELECT COUNT(*) AS remaining
FROM freight.booking_container_units bcu
JOIN freight.booking_containers bc
JOIN freight.booking_container bc
ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL
WHERE bc.booking_id = $1 AND bcu.deleted_at IS NULL AND bcu.grn_number IS NULL`,
[bookingId],

View File

@@ -269,7 +269,7 @@ export class CustomerTruckService {
const [row]: Array<{ kg: string }> = await this.dataSource.query(
`SELECT COALESCE(SUM(bcu.vgm_tons), 0) * 1000 AS kg
FROM freight.booking_container_units bcu
JOIN freight.booking_containers bc
JOIN freight.booking_container bc
ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL
WHERE bc.booking_id = $1
AND bcu.container_number = ANY($2::varchar[])
@@ -369,7 +369,7 @@ export class CustomerTruckService {
const rows: Array<{ containerNumber: string }> = await this.dataSource.query(
`SELECT bcu.container_number AS "containerNumber"
FROM freight.booking_container_units bcu
JOIN freight.booking_containers bc
JOIN freight.booking_container bc
ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL
WHERE bc.booking_id = $1 AND bcu.deleted_at IS NULL`,
[bookingId],

View File

@@ -930,7 +930,7 @@ export class WarehouseInventoryService {
SET received_to_port = true,
received_at = COALESCE(bcu.received_at, NOW()),
updated_at = NOW()
FROM freight.booking_containers bc
FROM freight.booking_container bc
WHERE bc.id = bcu.booking_container_id
AND bc.booking_id = $1
AND bc.deleted_at IS NULL
@@ -1801,7 +1801,7 @@ export class WarehouseInventoryService {
SET received_to_port = true,
received_at = COALESCE(bcu.received_at, NOW()),
updated_at = NOW()
FROM freight.booking_containers bc, freight.containers cont
FROM freight.booking_container bc, freight.containers cont
WHERE bc.id = bcu.booking_container_id
AND bc.booking_id = $1
AND bc.deleted_at IS NULL
@@ -2248,7 +2248,7 @@ export class WarehouseInventoryService {
FROM freight.customer_truck_containers cc
JOIN freight.booking_container_units bcu
ON bcu.container_number = cc.container_number AND bcu.deleted_at IS NULL
JOIN freight.booking_containers bc
JOIN freight.booking_container bc
ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL
AND bc.booking_id = c.booking_id
WHERE cc.assignment_id = a.id AND cc.deleted_at IS NULL
@@ -2514,9 +2514,17 @@ export class WarehouseInventoryService {
return {
filename: `grn-${String(row.grnNumber).replace(/[^a-zA-Z0-9_-]+/g, '-')}.pdf`,
// Generic render — NOT the release-order fallback (would mislabel the GRN
// as a "Gate Clearance / Release Order" when Chromium is unavailable).
buffer: await this.releaseDocuments.renderDocumentHtml(html, 'Goods Received Note'),
// Styled fallback titled as a GRN (not a release order) for Chromium-less render.
buffer: await this.releaseDocuments.renderStyledDocument(
html,
{
titleLines: ['GOODS RECEIVED', 'NOTE'],
subtitle: 'OFFICIAL WAREHOUSE GOODS RECEIVED NOTE',
sectionTitle: 'RECEIVED PARTICULARS',
refLabel: 'GRN No.',
},
'Goods Received Note',
),
};
}
@@ -2721,9 +2729,17 @@ export class WarehouseInventoryService {
return {
filename: `handover-${String(reference).replace(/[^a-zA-Z0-9_-]+/g, '-')}.pdf`,
// Generic render — NOT the release-order fallback (would mislabel the
// handover as a "Gate Clearance / Release Order" when Chromium is down).
buffer: await this.releaseDocuments.renderDocumentHtml(html, 'Import Goods Handover'),
// Styled fallback titled as a handover (not a release order) for Chromium-less render.
buffer: await this.releaseDocuments.renderStyledDocument(
html,
{
titleLines: ['IMPORT GOODS', 'HANDOVER', 'DOCUMENT'],
subtitle: 'EDR TO CUSTOMER WAREHOUSE HANDOVER',
sectionTitle: 'HANDOVER PARTICULARS',
refLabel: 'Document / Handover No.',
},
'Import Goods Handover',
),
};
}

View File

@@ -32,16 +32,38 @@ export class WarehouseReleaseDocumentService {
return this.pdf.htmlToPdfBuffer(html, { label });
}
private htmlToBasicPdfBuffer(html: string): Buffer {
/**
* Render document HTML with a STYLED hand-built fallback (the release layout,
* but with a custom title + section heading) for when Chromium is unavailable.
* Handover / GRN use this so their fallback looks like a proper document —
* not a plain-text dump, and not mislabelled as a release order.
*/
renderStyledDocument(
html: string,
fallbackOpts: { titleLines?: string[]; subtitle?: string; sectionTitle?: string; refLabel?: string },
label = 'Document',
): Promise<Buffer> {
return this.pdf.htmlToPdfBuffer(html, {
label,
fallback: (preparedHtml) => this.htmlToBasicPdfBuffer(preparedHtml, fallbackOpts),
});
}
private htmlToBasicPdfBuffer(
html: string,
opts?: { titleLines?: string[]; subtitle?: string; sectionTitle?: string; refLabel?: string },
): Buffer {
const doc = this.extractReleaseDocument(html);
const titleLines = (opts?.titleLines ?? ['WAREHOUSE GATE', 'CLEARANCE / RELEASE', 'ORDER']).slice(0, 3);
const subtitle = opts?.subtitle ?? 'OFFICIAL WAREHOUSE RELEASE AND EXIT AUTHORIZATION';
const sectionTitle = opts?.sectionTitle ?? 'RELEASE PARTICULARS';
const refLabel = opts?.refLabel ?? 'Document / Release No.';
const body: string[] = [
this.lineOp(36, 810, 559, 810, '0 0 0', 2.2),
this.textOp('ETHIO-DJIBOUTI RAILWAY S.C.', 36, 787, 9, 'F2', '0.08 0.32 0.18'),
this.textOp('WAREHOUSE GATE', 36, 764, 24, 'F2', '0.02 0.08 0.16'),
this.textOp('CLEARANCE / RELEASE', 36, 742, 24, 'F2', '0.02 0.08 0.16'),
this.textOp('ORDER', 36, 720, 24, 'F2', '0.02 0.08 0.16'),
this.textOp('OFFICIAL WAREHOUSE RELEASE AND EXIT AUTHORIZATION', 36, 696, 8.5, 'F1', '0.25 0.34 0.45'),
this.textOp('Document / Release No.', 424, 781, 8.5, 'F1', '0.15 0.22 0.32'),
...titleLines.map((line, i) => this.textOp(line, 36, 764 - i * 22, 24, 'F2', '0.02 0.08 0.16')),
this.textOp(subtitle, 36, 764 - titleLines.length * 22 + 2, 8.5, 'F1', '0.25 0.34 0.45'),
this.textOp(refLabel, 424, 781, 8.5, 'F1', '0.15 0.22 0.32'),
this.textOp(doc.reference, 504 - doc.reference.length * 2.2, 761, 15, 'F2', '0.02 0.08 0.16'),
this.textOp(`Issued: ${doc.issuedAt}`, 424, 742, 8.5, 'F1', '0.15 0.22 0.32'),
this.lineOp(36, 682, 559, 682, '0.08 0.32 0.18', 2),
@@ -50,7 +72,7 @@ export class WarehouseReleaseDocumentService {
...this.wrapLines(doc.notice, 68)
.slice(0, 4)
.map((line, index) => this.textOp(line, 52, 659 - index * 12, 9.2, 'F1')),
this.textOp('RELEASE PARTICULARS', 36, 604, 10, 'F2', '0.08 0.32 0.18'),
this.textOp(sectionTitle, 36, 604, 10, 'F2', '0.08 0.32 0.18'),
];
let y = 586;

View File

@@ -1,11 +1,12 @@
import { Alert, Badge, Button, Group, Loader, Modal, MultiSelect, Stack, Text } from '@mantine/core';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Truck } from 'lucide-react';
import { FileText, Truck } from 'lucide-react';
import { useState } from 'react';
import { useToast } from '@/hooks/use-toast';
import { warehouseService } from '@/services/warehouse.service';
import { extractErrorMessage } from './options';
import { openPdfBlob } from './pdf';
interface TruckDispatchModalProps {
opened: boolean;
@@ -50,6 +51,15 @@ export function TruckDispatchModal({ opened, onClose, bookingId, bookingReferenc
onError: (e) => toast({ variant: 'destructive', title: 'Load failed', description: extractErrorMessage(e) }),
});
const openTruckExitPaper = async (assignmentId: string, plate: string) => {
try {
const res = await warehouseService.downloadTruckExitPaper(assignmentId);
openPdfBlob(res.data, `exit-${plate}.pdf`);
} catch (e) {
toast({ variant: 'destructive', title: 'Exit paper not ready', description: extractErrorMessage(e) });
}
};
return (
<Modal
opened={opened}
@@ -102,7 +112,16 @@ export function TruckDispatchModal({ opened, onClose, bookingId, bookingReferenc
disabled={departed || !t.arrivedAt}
nothingFoundMessage="No loadable containers"
/>
<Group justify="flex-end">
<Group justify="flex-end" gap="xs">
<Button
size="xs"
variant="light"
color="orange"
leftSection={<FileText size={14} />}
onClick={() => openTruckExitPaper(t.id, t.plateNumber)}
>
Exit Paper
</Button>
<Button
size="xs"
color="edr-green"

View File

@@ -71,14 +71,14 @@ const cleanParams = (params: object) =>
export const warehouseService = {
/** Customer self-haul trucks assigned to a booking (portal multi-truck). */
getCustomerTrucks: async (bookingId: string): Promise<Freight.ICustomerTruck[]> => {
const { data } = await apiClient.get(`/api/bookings/${bookingId}/customer-trucks`);
const { data } = await apiClient.get(`/bookings/${bookingId}/customer-trucks`);
return data?.data ?? data ?? [];
},
/** Booking container numbers not yet loaded onto any truck. */
getLoadableContainers: async (bookingId: string): Promise<string[]> => {
const { data } = await apiClient.get(
`/api/bookings/${bookingId}/customer-trucks/loadable-containers`,
`/bookings/${bookingId}/customer-trucks/loadable-containers`,
);
return data?.data ?? data ?? [];
},
@@ -90,7 +90,7 @@ export const warehouseService = {
containerNumbers: string[],
): Promise<Freight.ICustomerTruck[]> => {
const { data } = await apiClient.post(
`/api/bookings/${bookingId}/customer-trucks/${assignmentId}/load`,
`/bookings/${bookingId}/customer-trucks/${assignmentId}/load`,
{ containerNumbers },
);
return data?.data ?? data ?? [];
@@ -176,6 +176,11 @@ export const warehouseService = {
apiClient.get<Blob>(URL_CONSTANTS.WAREHOUSE_INVENTORY.HANDOVER_DOCUMENT(id), {
responseType: 'blob',
}),
/** Per-truck exit paper PDF (containers loaded on one customer truck). */
downloadTruckExitPaper: (assignmentId: string) =>
apiClient.get<Blob>(`/warehouse-inventory/customer-truck-exit-paper/${assignmentId}`, {
responseType: 'blob',
}),
deliver: (id: string, payload: DeliverInventoryPayload) =>
apiClient.post<WarehouseInventoryItem>(URL_CONSTANTS.WAREHOUSE_INVENTORY.DELIVER(id), payload),