diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.module.ts b/apps/edr-freight-api/src/modules/bookings/bookings.module.ts
index 4b3e3bcca..96ed3b701 100644
--- a/apps/edr-freight-api/src/modules/bookings/bookings.module.ts
+++ b/apps/edr-freight-api/src/modules/bookings/bookings.module.ts
@@ -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),
diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts
index dac739fbf..9f72c7409 100644
--- a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts
+++ b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts
@@ -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]) => `
${this.escapeHtml(label)} ${this.escapeHtml(value || '-')} `)
- .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]) =>
- `${this.escapeHtml(label)} ${this.escapeHtml(value || '-')} `,
- )
- .join('');
- return ``;
- })
+ const truckRows = truckList
+ .map(
+ (t, i) => `
+ ${i + 1}
+ ${esc(t.plateNumber)}
+ ${esc(t.driverName)}
+ ${esc(t.truckType)}
+ ${esc(t.containers)}
+ ${t.arrivedAt ? esc(new Date(t.arrivedAt).toLocaleString('en-GB')) : 'Awaiting arrival'}
+ `,
+ )
.join('');
const copy = (watermark: string) => `
- ${this.escapeHtml(watermark)}
-
+ ${esc(watermark)}
+
+
Ethio-Djibouti Railway S.C.
Freight Order
-
Customer external truck assignment — ${truckList.length} truck${truckList.length !== 1 ? 's' : ''}
+
Customer external truck assignment — ${truckList.length} truck${truckList.length !== 1 ? 's' : ''}
-
${this.escapeHtml(booking.reference)}
-
-
- ${truckBlocks}
+
+ Booking
+ ${esc(booking.reference)}
+ Generated: ${esc(new Date().toLocaleString('en-GB'))}
+
+
+
+
Client ${esc(booking.company?.name)}
+
Client ID ${esc(booking.companyId)}
+
Trade direction ${esc(booking.tradeDirection)}
+
Freight type ${esc(booking.freightType)}
+
Assigned at ${esc(assignedAt)}
+
Booking status ${esc(booking.status)}
+
+
+
+
+ #
+ Truck plate
+ Driver
+ Truck type
+ Containers loaded
+ Arrival
+
+
+ ${truckRows}
+
+
+ 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.
+
-
Customer / Carrier Signature
-
Port Operations Verification
-
Gate Security Verification
+
Customer / Carrier signature — date
+
Port operations verification — date
+
Gate security verification — date
`;
@@ -342,21 +356,30 @@ export class BookingsService {
+ Freight Order
diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts
index 2b98856d0..92617db6d 100644
--- a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts
+++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts
@@ -207,6 +207,7 @@ export interface EligibleBookingRow {
customerTin: string | null;
customerPhone: string | null;
containerNumber: string | null;
+ sealNumbers: string | null;
containerQuantity: number | null;
containerPackagingType: string | null;
cargoDescription: string | null;
@@ -774,7 +775,8 @@ export class WarehouseInventoryService {
company.name AS "customer",
company.tin AS "customerTin",
COALESCE(company.contact_person_phone, company.phone, company.general_manager_phone, company.etrade_phone) AS "customerPhone",
- bc.container_numbers AS "containerNumber",
+ COALESCE(bcu.unit_numbers, bc.container_numbers) AS "containerNumber",
+ bcu.seal_numbers AS "sealNumbers",
bc.container_quantity AS "containerQuantity",
bc.container_packaging_type AS "containerPackagingType",
(NULLIF(TRIM(COALESCE(b.last_mile_delivery_address, '')), '') IS NOT NULL
@@ -831,6 +833,14 @@ export class WarehouseInventoryService {
LEFT JOIN freight.container_types container_type ON container_type.id = booking_container.container_type_id
WHERE booking_container.booking_id = b.id AND booking_container.deleted_at IS NULL
) bc ON true
+ LEFT JOIN LATERAL (
+ SELECT string_agg(NULLIF(unit.container_number, ''), ', ' ORDER BY unit.container_number) AS unit_numbers,
+ string_agg(DISTINCT NULLIF(unit.seal_number, ''), ', ') AS seal_numbers
+ FROM freight.booking_container_units unit
+ JOIN freight.booking_container line
+ ON line.id = unit.booking_container_id AND line.deleted_at IS NULL
+ WHERE line.booking_id = b.id AND unit.deleted_at IS NULL
+ ) bcu ON true
LEFT JOIN LATERAL (
SELECT first_mile.id, first_mile.status, first_mile.vehicle_id
FROM freight.first_mile first_mile
@@ -881,11 +891,18 @@ export class WarehouseInventoryService {
}> = [];
await this.dataSource.transaction(async (manager) => {
- await this.validateLocation(manager, {
+ const { warehouse, yard, zone } = await this.validateLocation(manager, {
warehouseId: dto.warehouseId,
yardId: dto.yardId,
zoneId: dto.zoneId,
});
+ // The receive location is whatever the operator selected above — never a
+ // hand-typed string. Stamp it on the truck entrance for the GRN/notes.
+ if (dto.truckEntrance && !dto.truckEntrance.warehouseCodeLocation) {
+ dto.truckEntrance.warehouseCodeLocation = [warehouse.code, yard.code, zone.code]
+ .filter(Boolean)
+ .join(' / ');
+ }
for (const bookingId of dto.bookingIds) {
const skip = (reason: string) => {
diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/ReceiveInventoryModal.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/ReceiveInventoryModal.tsx
index a1b91decb..de41c1a82 100644
--- a/apps/edr-freight-web/backoffice/src/components/warehouses/ReceiveInventoryModal.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/warehouses/ReceiveInventoryModal.tsx
@@ -150,9 +150,6 @@ interface TruckEntranceFormState {
assignedEquipmentNumber: string;
customsSealNumber: string;
declarationNumber: string;
- incoterms: string;
- hsCodes: string;
- itemCode: string;
itemDescription: string;
packagingType: string;
unitCount: number | '';
@@ -162,7 +159,6 @@ interface TruckEntranceFormState {
volumeDimensions: string;
conditionAtReceipt: string;
damagedRejectedQuantity: number | '';
- warehouseCodeLocation: string;
driverName: string;
driverPhone: string;
driverLicenseNumber: string;
@@ -205,9 +201,6 @@ const emptyTruckEntrance = (): TruckEntranceFormState => ({
assignedEquipmentNumber: '',
customsSealNumber: '',
declarationNumber: '',
- incoterms: '',
- hsCodes: '',
- itemCode: '',
itemDescription: '',
packagingType: '',
unitCount: '',
@@ -217,7 +210,6 @@ const emptyTruckEntrance = (): TruckEntranceFormState => ({
volumeDimensions: '',
conditionAtReceipt: '',
damagedRejectedQuantity: '',
- warehouseCodeLocation: '',
driverName: '',
driverPhone: '',
driverLicenseNumber: '',
@@ -239,9 +231,6 @@ const toTruckEntrancePayload = (form: TruckEntranceFormState): TruckEntrancePayl
assignedEquipmentNumber: form.assignedEquipmentNumber.trim() || undefined,
customsSealNumber: form.customsSealNumber.trim() || undefined,
declarationNumber: form.declarationNumber.trim() || undefined,
- incoterms: form.incoterms.trim() || undefined,
- hsCodes: form.hsCodes.trim() || undefined,
- itemCode: form.itemCode.trim() || undefined,
itemDescription: form.itemDescription.trim() || undefined,
packagingType: form.packagingType.trim() || undefined,
unitCount: form.unitCount === '' ? undefined : Number(form.unitCount),
@@ -251,7 +240,6 @@ const toTruckEntrancePayload = (form: TruckEntranceFormState): TruckEntrancePayl
volumeDimensions: form.volumeDimensions.trim() || undefined,
conditionAtReceipt: form.conditionAtReceipt.trim() || undefined,
damagedRejectedQuantity: form.damagedRejectedQuantity === '' ? undefined : Number(form.damagedRejectedQuantity),
- warehouseCodeLocation: form.warehouseCodeLocation.trim() || undefined,
driverName: form.driverName.trim(),
driverPhone: form.driverPhone.trim(),
driverLicenseNumber: form.driverLicenseNumber.trim() || undefined,
@@ -296,6 +284,10 @@ const truckEntranceFromBookings = (bookings: EligibleBooking[]): {
const truckType = commonNonEmptyValue(
bookings.map((booking) => booking.firstMileTruckType || booking.customerTruckType),
);
+ const customsSealNumber = commonNonEmptyValue(bookings.map((booking) => booking.sealNumbers));
+ // Booking's declared cargo weight (tonnes) — the receive-time net until re-weighed.
+ const bookingWeight = bookings.length === 1 ? Number(bookings[0]?.weight ?? '') : NaN;
+ const netWeightKg: number | '' = Number.isFinite(bookingWeight) && bookingWeight > 0 ? bookingWeight : '';
const edrDigitalBookingId =
bookings.length === 1
? bookings[0]?.reference ?? bookings[0]?.id ?? ''
@@ -321,9 +313,11 @@ const truckEntranceFromBookings = (bookings: EligibleBooking[]): {
customerPhone,
edrDigitalBookingId,
assignedEquipmentNumber,
+ customsSealNumber,
itemDescription,
packagingType,
unitCount,
+ netWeightKg,
grossWeightKg: '',
truckPlateNumber,
trailerPlateNumber,
@@ -331,6 +325,7 @@ const truckEntranceFromBookings = (bookings: EligibleBooking[]): {
driverPhone,
driverLicenseNumber,
truckType,
+ driverSignatoryName: driverName,
},
lockedFields: {
ownerName: Boolean(ownerName),
@@ -550,38 +545,19 @@ function TruckEntranceFields({
)}
Customs and compliance
-
- onChange({ ...value, declarationNumber: e.currentTarget.value })}
- />
- onChange({ ...value, incoterms: e.currentTarget.value })}
- />
-
onChange({ ...value, hsCodes: e.currentTarget.value })}
+ label="Declaration / Bill of Entry number"
+ value={value.declarationNumber}
+ onChange={(e) => onChange({ ...value, declarationNumber: e.currentTarget.value })}
/>
Physical cargo specifications
-
- onChange({ ...value, itemCode: e.currentTarget.value })}
- />
- onChange({ ...value, itemDescription: e.currentTarget.value })}
- />
-
+ onChange({ ...value, itemDescription: e.currentTarget.value })}
+ />
onChange({ ...value, damagedRejectedQuantity: v === '' ? '' : Number(v) })}
/>
- onChange({ ...value, warehouseCodeLocation: e.currentTarget.value })}
- />