list trucks + loadable containers → load)

This commit is contained in:
Hagernesh
2026-07-06 12:40:34 +00:00
parent fe9503638b
commit 551a5fdeed
8 changed files with 402 additions and 2 deletions

View File

@@ -285,6 +285,19 @@ export class WarehouseInventoryController {
return res.send(buffer);
}
@Get('customer-truck-exit-paper/:assignmentId')
@ApiOperation({ summary: 'Per-truck exit paper PDF (containers loaded on one customer truck)' })
async truckExitPaper(
@Param('assignmentId', ParseUUIDPipe) assignmentId: string,
@Res() res: Response,
) {
const { filename, buffer } = await this.inventoryService.truckExitPaper(assignmentId);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
res.setHeader('Content-Length', buffer.length);
return res.send(buffer);
}
@Get(':id/grn-document')
@ApiOperation({ summary: 'View goods received note PDF' })
async grnDocument(@Param('id', ParseUUIDPipe) id: string, @Res() res: Response) {

View File

@@ -2308,6 +2308,115 @@ export class WarehouseInventoryService {
};
}
/**
* Per-truck exit paper: one paper covering the containers loaded on a specific
* customer truck (used when multiple trucks leave separately). Gated on the
* handover being signed and warehouse fees paid.
*/
async truckExitPaper(assignmentId: string): Promise<{ filename: string; buffer: Buffer }> {
const [truck] = await this.dataSource.query(
`SELECT a.booking_id AS "bookingId", a.plate_number AS "plateNumber",
a.driver_name AS "driverName", a.truck_type AS "truckType",
a.gross_weight_kg AS "grossWeightKg", a.departed_at AS "departedAt",
b.reference AS "bookingReference", company.name AS "customerName"
FROM freight.customer_truck_assignments a
JOIN freight.bookings b ON b.id = a.booking_id AND b.deleted_at IS NULL
LEFT JOIN freight.companies company ON company.id = b.company_id
WHERE a.id = $1 AND a.deleted_at IS NULL`,
[assignmentId],
);
if (!truck) throw new NotFoundException(`Truck assignment ${assignmentId} not found`);
if (!(await this.handover.isFullySigned(truck.bookingId))) {
throw new BadRequestException('Handover must be signed before the exit paper can be generated');
}
const [inv]: Array<{ id: string }> = await this.dataSource.query(
`SELECT id FROM freight.warehouse_inventory
WHERE booking_id = $1 AND deleted_at IS NULL ORDER BY created_at LIMIT 1`,
[truck.bookingId],
);
if (inv?.id) await this.invoices.assertClearanceAllowed(inv.id);
const containers: Array<{ containerNumber: string; goods: string | null }> =
await this.dataSource.query(
`SELECT c.container_number AS "containerNumber",
COALESCE(ct.cargo_type_name, b.cargo_free_text) AS goods
FROM freight.customer_truck_containers c
JOIN freight.bookings b ON b.id = c.booking_id
LEFT JOIN freight.cargo_types ct ON ct.id = b.cargo_type_id
WHERE c.assignment_id = $1 AND c.deleted_at IS NULL
ORDER BY c.container_number`,
[assignmentId],
);
const html = this.buildTruckExitPaperHtml({
reference: `REL-${String(truck.bookingReference).replace(/^BK-?/i, '')}-${truck.plateNumber}`,
bookingReference: truck.bookingReference,
customerName: truck.customerName,
plateNumber: truck.plateNumber,
driverName: truck.driverName,
truckType: truck.truckType,
grossWeightKg: Number(truck.grossWeightKg ?? 0),
gateOut: truck.departedAt,
containers,
});
return {
filename: `exit-${String(truck.plateNumber).replace(/[^a-zA-Z0-9_-]+/g, '-')}.pdf`,
buffer: await this.releaseDocuments.renderDocumentHtml(html, 'Warehouse exit paper'),
};
}
private buildTruckExitPaperHtml(data: {
reference: string;
bookingReference: string;
customerName: string | null;
plateNumber: string;
driverName: string;
truckType: string;
grossWeightKg: number;
gateOut: string | Date | null;
containers: Array<{ containerNumber: string; goods: string | null }>;
}): string {
const esc = (v: unknown) =>
String(v ?? '-').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const gateOut = data.gateOut ? new Date(data.gateOut).toLocaleString('en-GB') : '-';
const rows: Array<[string, string]> = [
['Booking Reference', data.bookingReference],
['Customer / Consignee', data.customerName ?? '-'],
['Pickup Truck Plate', data.plateNumber],
['Driver', data.driverName],
['Truck Type', data.truckType],
['Gross Weight (Loaded on Truck)', `${data.grossWeightKg.toLocaleString()} kg`],
['Gate-Out Time', gateOut],
['Clearance Status', 'CLEARED FOR WAREHOUSE EXIT'],
];
const containerRows = data.containers.length
? data.containers
.map((c) => `<tr><td>${esc(c.containerNumber)}</td><td>${esc(c.goods)}</td></tr>`)
.join('')
: '<tr><td colspan="2">No containers loaded on this truck.</td></tr>';
return `<!doctype html><html><head><meta charset="utf-8" /><title>Warehouse Exit Paper</title>
<style>
body { font-family: "Times New Roman", Georgia, serif; color: #061323; margin: 24px; }
h1 { font-size: 24px; text-transform: uppercase; margin: 0 0 4px; }
table { width: 100%; border-collapse: collapse; margin-top: 8px; }
th, td { border: 1px solid #b9c7d8; padding: 8px 10px; font-size: 12px; text-align: left; vertical-align: top; }
th { background: #f8fafc; width: 34%; font-weight: 800; }
.section { margin-top: 18px; font-weight: 800; color: #064c27; text-transform: uppercase; letter-spacing: .1em; }
.ref strong { font-size: 16px; }
</style></head>
<body>
<div style="color:#064c27;font-weight:800;text-transform:uppercase;">Ethio-Djibouti Railway S.C.</div>
<h1>Warehouse Release / Exit Paper</h1>
<div class="ref">Document / Release No. <strong>${esc(data.reference)}</strong></div>
<div class="section">Release Particulars</div>
<table><tbody>${rows.map(([l, v]) => `<tr><th>${esc(l)}</th><td>${esc(v)}</td></tr>`).join('')}</tbody></table>
<div class="section">Containers Leaving on This Truck</div>
<table><thead><tr><th style="width:40%">Container Number</th><th>Goods</th></tr></thead>
<tbody>${containerRows}</tbody></table>
</body></html>`;
}
/** Hand import goods to the customer + capture proof of delivery (READY_FOR_PICKUP → DELIVERED). */
async grnDocument(id: string): Promise<{ filename: string; buffer: Buffer }> {
const [row] = await this.dataSource.query(