mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
pERTRUCK EXIT
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user