mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
fix(bookings): freight order renders styled on Chromium-less servers
The customer-truck freight order went through ContractPdfService, whose only fallback is a crude text dump - so on servers without Chromium the document rendered as unstyled text. - render via the shared PdfRenderService with the styled tabular fallback (same path as marshalling / release documents) - rebuild the freight-order HTML in the standard document shape: brand header, booking summary tiles, one trucks table (plate / driver / type / containers / arrival), gate notice, signature lines - which is also what the fallback parser reads - keep the two watermarked copies (Port Operations, Gate Security & Carrier) for the Chromium path; drop the now-unused ContractPdfService injection Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import { RuleEngineModule } from '../rule-engine/rule-engine.module';
|
||||
import { FileUploadSettingsModule } from '../file-upload-settings/file-upload-settings.module';
|
||||
import { SignaturesModule } from '../signatures/signatures.module';
|
||||
import { BillingModule } from '../billing/billing.module';
|
||||
import { DocumentsModule } from '../billing/documents/documents.module';
|
||||
import { FirstMileModule } from '../first-mile/first-mile.module';
|
||||
import { LastMileModule } from '../last-mile/last-mile.module';
|
||||
import { BookingContractService } from './booking-contract.service';
|
||||
@@ -68,6 +69,7 @@ import { VehiclesModule } from "../vehicles/vehicles.module";
|
||||
CustomerTruckContainer,
|
||||
]),
|
||||
BillingModule,
|
||||
DocumentsModule,
|
||||
NotificationsModule,
|
||||
NotificationInboxModule,
|
||||
forwardRef(() => FirstMileModule),
|
||||
|
||||
@@ -50,7 +50,8 @@ import { Booking } from './entities/booking.entity';
|
||||
import { BookingContainerAllocation } from './entities/booking-container-allocation.entity';
|
||||
import { FileRecord } from '../files/entities/file.entity';
|
||||
import { CustomerTruckAssignmentDto } from './dto/customer-truck-assignment.dto';
|
||||
import { ContractPdfService } from '../../contracts/contract-pdf.service';
|
||||
import { PdfRenderService } from '../billing/documents/pdf-render.service';
|
||||
import { buildTabularFallbackPdf } from '../billing/documents/styled-pdf.util';
|
||||
|
||||
/** Paginated booking list: flat `total` (backoffice) + `meta` block (portal). */
|
||||
export interface PaginatedBookings {
|
||||
@@ -99,7 +100,7 @@ export class BookingsService {
|
||||
private readonly containerTypesService: ContainerTypesService,
|
||||
private readonly consolidationService: ConsolidationService,
|
||||
private readonly vehiclesService: VehiclesService,
|
||||
private readonly contractPdfService: ContractPdfService,
|
||||
private readonly pdfRender: PdfRenderService,
|
||||
private readonly events: EventEmitter2,
|
||||
) {}
|
||||
|
||||
@@ -170,7 +171,12 @@ export class BookingsService {
|
||||
);
|
||||
|
||||
const html = this.buildCustomerTruckFreightOrderHtml(booking, trucks);
|
||||
const buffer = await this.contractPdfService.htmlToPdfBuffer(html);
|
||||
// Chromium when available; otherwise the styled tabular fallback (never the
|
||||
// generic text dump — the freight order is an outward-facing gate document).
|
||||
const buffer = await this.pdfRender.htmlToPdfBuffer(html, {
|
||||
label: 'freight order',
|
||||
fallback: (prepared) => buildTabularFallbackPdf(prepared),
|
||||
});
|
||||
return {
|
||||
filename: `freight-order-${booking.reference.replace(/[^a-zA-Z0-9_-]+/g, '-')}.pdf`,
|
||||
buffer,
|
||||
@@ -262,21 +268,10 @@ export class BookingsService {
|
||||
containers: string | null;
|
||||
}>,
|
||||
): string {
|
||||
const esc = (v: unknown) => this.escapeHtml(String(v ?? '-'));
|
||||
const assignedAt = booking.customerTruckAssignedAt
|
||||
? new Date(booking.customerTruckAssignedAt).toLocaleString('en-GB')
|
||||
: '-';
|
||||
const bookingRows: Array<[string, string | null | undefined]> = [
|
||||
['Booking Reference', booking.reference],
|
||||
['Client Name', booking.company?.name],
|
||||
['Client ID', booking.companyId],
|
||||
['Trade Direction', booking.tradeDirection],
|
||||
['Freight Type', booking.freightType],
|
||||
['Assigned At', assignedAt],
|
||||
['Booking Status', booking.status],
|
||||
];
|
||||
const bookingRowHtml = bookingRows
|
||||
.map(([label, value]) => `<tr><th>${this.escapeHtml(label)}</th><td>${this.escapeHtml(value || '-')}</td></tr>`)
|
||||
.join('');
|
||||
|
||||
// Fall back to the legacy single-truck booking columns when there are no
|
||||
// multi-truck rows (bookings assigned before the multi-truck feature).
|
||||
@@ -297,44 +292,63 @@ export class BookingsService {
|
||||
]
|
||||
: [];
|
||||
|
||||
const truckBlocks = truckList
|
||||
.map((t, i) => {
|
||||
const rows: Array<[string, string | null | undefined]> = [
|
||||
['Truck Plate Number', t.plateNumber],
|
||||
['Driver Name', t.driverName],
|
||||
['Truck Type', t.truckType],
|
||||
['Containers Loaded', t.containers],
|
||||
[
|
||||
'Arrival',
|
||||
t.arrivedAt ? new Date(t.arrivedAt).toLocaleString('en-GB') : 'Awaiting arrival',
|
||||
],
|
||||
];
|
||||
const html = rows
|
||||
.map(
|
||||
([label, value]) =>
|
||||
`<tr><th>${this.escapeHtml(label)}</th><td>${this.escapeHtml(value || '-')}</td></tr>`,
|
||||
)
|
||||
.join('');
|
||||
return `<div class="truck"><h2>Truck ${i + 1}</h2><table>${html}</table></div>`;
|
||||
})
|
||||
const truckRows = truckList
|
||||
.map(
|
||||
(t, i) => `<tr>
|
||||
<td class="num">${i + 1}</td>
|
||||
<td>${esc(t.plateNumber)}</td>
|
||||
<td>${esc(t.driverName)}</td>
|
||||
<td>${esc(t.truckType)}</td>
|
||||
<td>${esc(t.containers)}</td>
|
||||
<td>${t.arrivedAt ? esc(new Date(t.arrivedAt).toLocaleString('en-GB')) : 'Awaiting arrival'}</td>
|
||||
</tr>`,
|
||||
)
|
||||
.join('');
|
||||
|
||||
const copy = (watermark: string) => `
|
||||
<section class="copy">
|
||||
<div class="watermark">${this.escapeHtml(watermark)}</div>
|
||||
<header>
|
||||
<div class="watermark">${esc(watermark)}</div>
|
||||
<div class="top">
|
||||
<div>
|
||||
<div class="brand">Ethio-Djibouti Railway S.C.</div>
|
||||
<h1>Freight Order</h1>
|
||||
<p>Customer external truck assignment — ${truckList.length} truck${truckList.length !== 1 ? 's' : ''}</p>
|
||||
<div class="subtitle">Customer external truck assignment — ${truckList.length} truck${truckList.length !== 1 ? 's' : ''}</div>
|
||||
</div>
|
||||
<strong>${this.escapeHtml(booking.reference)}</strong>
|
||||
</header>
|
||||
<table>${bookingRowHtml}</table>
|
||||
${truckBlocks}
|
||||
<div class="meta">
|
||||
Booking
|
||||
<strong>${esc(booking.reference)}</strong>
|
||||
Generated: ${esc(new Date().toLocaleString('en-GB'))}
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary">
|
||||
<div class="tile"><span>Client</span><strong>${esc(booking.company?.name)}</strong></div>
|
||||
<div class="tile"><span>Client ID</span><strong>${esc(booking.companyId)}</strong></div>
|
||||
<div class="tile"><span>Trade direction</span><strong>${esc(booking.tradeDirection)}</strong></div>
|
||||
<div class="tile"><span>Freight type</span><strong>${esc(booking.freightType)}</strong></div>
|
||||
<div class="tile"><span>Assigned at</span><strong>${esc(assignedAt)}</strong></div>
|
||||
<div class="tile"><span>Booking status</span><strong>${esc(booking.status)}</strong></div>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="num">#</th>
|
||||
<th>Truck plate</th>
|
||||
<th>Driver</th>
|
||||
<th>Truck type</th>
|
||||
<th>Containers loaded</th>
|
||||
<th>Arrival</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${truckRows}</tbody>
|
||||
</table>
|
||||
<div class="notice">
|
||||
Present this freight order at the warehouse gate. Each truck may only collect the
|
||||
containers listed against it; the handover must be signed before any truck leaves.
|
||||
</div>
|
||||
<div class="signatures">
|
||||
<div>Customer / Carrier Signature</div>
|
||||
<div>Port Operations Verification</div>
|
||||
<div>Gate Security Verification</div>
|
||||
<div class="line">Customer / Carrier signature — date</div>
|
||||
<div class="line">Port operations verification — date</div>
|
||||
<div class="line">Gate security verification — date</div>
|
||||
</div>
|
||||
</section>`;
|
||||
|
||||
@@ -342,21 +356,30 @@ export class BookingsService {
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Freight Order</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; color: #10202f; margin: 0; }
|
||||
.copy { position: relative; min-height: 46vh; padding: 28px 32px; page-break-inside: avoid; border-bottom: 1px dashed #94a3b8; }
|
||||
.watermark { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 34px; font-weight: 800; color: rgba(16, 32, 47, 0.08); transform: rotate(-18deg); pointer-events: none; }
|
||||
header { display: flex; justify-content: space-between; align-items: flex-start; border-bottom: 3px solid #0a9f6a; padding-bottom: 14px; margin-bottom: 18px; }
|
||||
h1 { margin: 0; font-size: 28px; letter-spacing: 0; }
|
||||
h2 { margin: 18px 0 8px; font-size: 14px; color: #0a6f4d; }
|
||||
p { margin: 4px 0 0; color: #64748b; }
|
||||
strong { font-size: 16px; color: #0a9f6a; }
|
||||
table { width: 100%; border-collapse: collapse; position: relative; z-index: 1; margin-bottom: 6px; }
|
||||
th, td { border: 1px solid #cbd5e1; padding: 9px 10px; text-align: left; font-size: 12px; }
|
||||
th { width: 34%; background: #f1f5f9; }
|
||||
.truck { page-break-inside: avoid; }
|
||||
.signatures { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-top: 34px; font-size: 11px; color: #475569; position: relative; z-index: 1; }
|
||||
.signatures div { border-top: 1px solid #334155; padding-top: 8px; min-height: 28px; }
|
||||
@page { size: A4 portrait; margin: 10mm; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; color: #0f172a; font-family: Arial, sans-serif; }
|
||||
.copy { position: relative; padding: 24px 28px; page-break-inside: avoid; border-bottom: 1px dashed #94a3b8; }
|
||||
.watermark { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 30px; font-weight: 800; color: rgba(15, 23, 42, 0.07); transform: rotate(-18deg); pointer-events: none; }
|
||||
.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 { margin-top: 4px; color: #64748b; font-size: 12px; }
|
||||
.meta { text-align: right; font-size: 11px; color: #475569; min-width: 210px; }
|
||||
.meta strong { display: block; margin: 4px 0; color: #0f172a; font-size: 15px; }
|
||||
.summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 14px 0; }
|
||||
.tile { border: 1px solid #cbd5e1; padding: 8px; min-height: 48px; }
|
||||
.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; position: relative; z-index: 1; }
|
||||
th { background: #f8fafc; color: #475569; text-align: left; }
|
||||
th, td { border: 1px solid #cbd5e1; padding: 6px 7px; font-size: 10.5px; vertical-align: top; }
|
||||
.num { text-align: right; width: 26px; }
|
||||
.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: 30px; position: relative; z-index: 1; }
|
||||
.line { border-top: 1px solid #334155; padding-top: 7px; font-size: 9px; color: #475569; min-height: 30px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user