Invoice and clearance seal approval

This commit is contained in:
hagiye
2026-06-27 01:13:25 +03:00
parent 8005282f9e
commit b2b668a392
8 changed files with 265 additions and 63 deletions

View File

@@ -1736,6 +1736,7 @@ export class WarehouseInventoryService {
const [row] = await this.dataSource.query(
`SELECT inv.id,
inv.release_date AS "releaseDate",
inv.release_order_reference AS "releaseOrderReference",
inv.quantity,
inv.weight,
inv.status,
@@ -1777,8 +1778,10 @@ export class WarehouseInventoryService {
throw new BadRequestException('A release order must be issued before downloading the exit paper');
}
const reference = `REL-${id.slice(0, 8).toUpperCase()}`;
const bookingReference = row?.bookingReference || row?.bookingId || 'N/A';
const bookingReference = row?.bookingReference || 'N/A';
const reference =
row?.releaseOrderReference ||
(row?.bookingReference ? `REL-${String(row.bookingReference).replace(/^BK-?/i, '')}` : 'REL-N/A');
const issuedAt = new Date(row.releaseDate);
const html = this.buildReleaseDocumentHtml({
reference,
@@ -1796,6 +1799,7 @@ export class WarehouseInventoryService {
yard: [row?.yardName, row?.yardCode].filter(Boolean).join(' / ') || null,
zone: [row?.zoneName, row?.zoneCode].filter(Boolean).join(' / ') || null,
inventoryStatus: row?.status ?? null,
clearanceStatus: 'CLEARED FOR WAREHOUSE EXIT',
});
return {
@@ -2300,6 +2304,7 @@ export class WarehouseInventoryService {
yard: string | null;
zone: string | null;
inventoryStatus: string | null;
clearanceStatus: string;
}): string {
const esc = (value: unknown) =>
String(value ?? '-')
@@ -2316,71 +2321,84 @@ export class WarehouseInventoryService {
minute: '2-digit',
});
const rows = [
['Booking reference', data.bookingReference],
['Customer', data.customerName],
['Booking status', data.bookingStatus],
['Freight type', data.freightType],
['Trade direction', data.tradeDirection],
['Container number', data.containerNumber],
['Cargo / goods', data.cargoDescription],
['Booking Reference', data.bookingReference],
['Customer / Consignee', data.customerName],
['Booking Status', data.bookingStatus],
['Freight Type', data.freightType],
['Trade Direction', data.tradeDirection],
['Container Number', data.containerNumber],
['Cargo / Goods Description', data.cargoDescription],
['Quantity', data.quantity],
['Weight', `${data.weight.toLocaleString()} kg`],
['Declared Weight', `${data.weight.toLocaleString()} kg`],
['Warehouse', data.warehouse],
['Yard', data.yard],
['Zone', data.zone],
['Inventory status', data.inventoryStatus],
['Inventory Status', data.inventoryStatus],
['Clearance Status', data.clearanceStatus],
];
return `<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Warehouse Release Exit Paper</title>
<title>Warehouse Gate Clearance / Release Order</title>
<style>
body { font-family: Arial, sans-serif; color: #0f172a; margin: 0; }
.doc { padding: 18px 8px; }
.top { display: flex; justify-content: space-between; gap: 24px; border-bottom: 3px solid #0f766e; padding-bottom: 18px; }
.brand { font-size: 13px; color: #475569; text-transform: uppercase; letter-spacing: .08em; }
h1 { margin: 8px 0 0; font-size: 30px; }
.ref { text-align: right; font-size: 13px; color: #475569; }
.ref strong { display: block; color: #0f172a; font-size: 18px; margin-top: 6px; }
.notice { margin: 22px 0; padding: 14px 16px; background: #ecfdf5; border: 1px solid #99f6e4; border-radius: 8px; font-weight: 700; }
table { width: 100%; border-collapse: collapse; margin-top: 16px; }
th { width: 32%; text-align: left; color: #475569; background: #f8fafc; }
th, td { border: 1px solid #cbd5e1; padding: 10px 12px; font-size: 13px; }
.signatures { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; margin-top: 42px; }
.line { border-top: 1px solid #334155; padding-top: 8px; font-size: 12px; color: #475569; }
.footer { margin-top: 28px; font-size: 11px; color: #64748b; line-height: 1.5; }
body { font-family: "Times New Roman", Georgia, serif; color: #111827; margin: 0; }
.doc { padding: 10px 8px 0; position: relative; }
.top { display: grid; grid-template-columns: 1fr auto; gap: 24px; border-bottom: 2px solid #14532d; padding-bottom: 14px; }
.brand { font-size: 13px; color: #14532d; text-transform: uppercase; letter-spacing: .1em; font-weight: 700; }
h1 { margin: 7px 0 0; font-size: 27px; line-height: 1.1; text-transform: uppercase; letter-spacing: .03em; }
.subtitle { margin-top: 6px; font-size: 12px; color: #475569; text-transform: uppercase; letter-spacing: .08em; }
.ref { text-align: right; font-size: 12px; color: #475569; min-width: 210px; }
.ref strong { display: block; color: #111827; font-size: 17px; margin: 4px 0 8px; }
.seal { position: absolute; right: 32px; top: 126px; width: 124px; height: 124px; border: 4px double #14532d; border-radius: 999px; color: #14532d; display: flex; align-items: center; justify-content: center; text-align: center; font-weight: 800; font-size: 17px; line-height: 1.15; transform: rotate(-13deg); opacity: .86; text-transform: uppercase; }
.seal::before { content: ""; position: absolute; inset: 10px; border: 1px solid #14532d; border-radius: inherit; }
.notice { margin: 20px 154px 18px 0; padding: 13px 15px; background: #f0fdf4; border: 1px solid #86efac; border-left: 5px solid #14532d; font-size: 13px; line-height: 1.45; }
.section-title { margin: 18px 0 8px; font-size: 13px; font-weight: 800; color: #14532d; text-transform: uppercase; letter-spacing: .08em; }
table { width: 100%; border-collapse: collapse; }
th { width: 31%; text-align: left; color: #334155; background: #f8fafc; font-weight: 700; }
th, td { border: 1px solid #cbd5e1; padding: 8px 10px; font-size: 12.5px; vertical-align: top; }
.clause { margin-top: 16px; border: 1px solid #cbd5e1; padding: 12px 14px; font-size: 12.5px; line-height: 1.45; }
.signatures { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; margin-top: 42px; }
.line { border-top: 1px solid #111827; padding-top: 8px; font-size: 12px; color: #334155; }
.footer { margin-top: 22px; border-top: 1px solid #cbd5e1; padding-top: 9px; font-size: 10.5px; color: #475569; line-height: 1.45; }
</style>
</head>
<body>
<div class="doc">
<div class="top">
<div>
<div class="brand">EDR Warehouse Operations</div>
<h1>Warehouse Release / Exit Paper</h1>
<div class="brand">Ethio-Djibouti Railway S.C.</div>
<h1>Warehouse Gate Clearance / Release Order</h1>
<div class="subtitle">Official warehouse release and exit authorization</div>
</div>
<div class="ref">
Release reference
Document / Release No.
<strong>${esc(data.reference)}</strong>
Issued: ${esc(issuedAt)}
</div>
</div>
<div class="seal">EDR<br />Warehouse<br />Cleared</div>
<div class="notice">
This document authorizes the listed booking/goods to leave the warehouse after release checks.
This clearance document confirms that the listed booking/goods are authorized for warehouse exit, subject to gate identity verification and confirmation that no blocking warehouse fees remain unpaid.
</div>
<div class="section-title">Release Particulars</div>
<table>
<tbody>
${rows.map(([label, value]) => `<tr><th>${esc(label)}</th><td>${esc(value)}</td></tr>`).join('')}
</tbody>
</table>
<div class="section-title">Authorization Clause</div>
<div class="clause">
The warehouse officer and gate staff shall verify this document against the booking reference, customer or driver identity,
cargo details, clearance status, and payment records before permitting exit from the warehouse premises.
</div>
<div class="signatures">
<div class="line">Warehouse officer name / signature / date</div>
<div class="line">Authorized warehouse officer name / signature / date</div>
<div class="line">Customer or driver name / signature / date</div>
</div>
<div class="footer">
Present this release paper at the warehouse gate. Gate staff should verify booking reference,
customer/driver identity, cargo details, and any unpaid blocking fees before exit.
Present this original release order at the warehouse gate. This document is valid only for the booking/goods stated above and must be retained or recorded by gate operations according to warehouse procedure.
</div>
</div>
</body>

View File

@@ -28,6 +28,22 @@ export interface PayInvoiceDto {
const BLOCKING_STATUSES: WarehouseInvoiceStatus[] = ['ISSUED', 'PARTIALLY_PAID'];
const ACTIVE_STATUSES: WarehouseInvoiceStatus[] = ['ISSUED', 'PARTIALLY_PAID', 'PAID'];
export interface InvoiceDocumentDetails {
bookingReference: string | null;
customerName: string | null;
inventoryReference: string | null;
inventoryInfo: string | null;
inventoryStatus: string | null;
containerNumber: string | null;
cargoDescription: string | null;
clearanceStatus: string;
warehouseName: string | null;
yardName: string | null;
zoneName: string | null;
}
export type WarehouseFeeInvoiceWithDisplay = WarehouseFeeInvoice & Partial<InvoiceDocumentDetails>;
@Injectable()
export class WarehouseInvoiceService {
constructor(
@@ -152,16 +168,18 @@ export class WarehouseInvoiceService {
}
// ── Reads ────────────────────────────────────────────────────────────────
async findById(id: string): Promise<WarehouseFeeInvoice & { items: unknown[] }> {
async findById(id: string): Promise<WarehouseFeeInvoiceWithDisplay & { items: unknown[] }> {
const invoice = await this.invoiceRepository.findById(id);
if (!invoice) throw new NotFoundException(`Invoice ${id} not found`);
const items = await this.itemRepository.findAll({ where: { invoiceId: id } });
return { ...invoice, items } as WarehouseFeeInvoice & { items: unknown[] };
const details = await this.getInvoiceDocumentDetails(invoice);
return { ...invoice, ...details, items } as WarehouseFeeInvoiceWithDisplay & { items: unknown[] };
}
async document(id: string): Promise<{ filename: string; buffer: Buffer }> {
const invoice = await this.findById(id);
const html = this.buildInvoiceDocumentHtml(invoice, 'INVOICE');
const details = await this.getInvoiceDocumentDetails(invoice);
const html = this.buildInvoiceDocumentHtml(invoice, 'INVOICE', details);
return {
filename: `warehouse-invoice-${this.safeFilename(invoice.invoiceNumber)}.pdf`,
buffer: await this.documents.htmlToPdfBuffer(html),
@@ -173,7 +191,8 @@ export class WarehouseInvoiceService {
if (Number(invoice.paidAmount) <= 0) {
throw new BadRequestException('A receipt is available only after payment is recorded.');
}
const html = this.buildInvoiceDocumentHtml(invoice, 'RECEIPT');
const details = await this.getInvoiceDocumentDetails(invoice);
const html = this.buildInvoiceDocumentHtml(invoice, 'RECEIPT', details);
return {
filename: `warehouse-receipt-${this.safeFilename(invoice.invoiceNumber)}.pdf`,
buffer: await this.documents.htmlToPdfBuffer(html),
@@ -257,9 +276,66 @@ export class WarehouseInvoiceService {
}
}
private async getInvoiceDocumentDetails(invoice: WarehouseFeeInvoice): Promise<InvoiceDocumentDetails> {
const [row] = await this.dataSource.query(
`SELECT b.reference AS "bookingReference",
company.name AS "customerName",
COALESCE(inv.release_order_reference, b.reference) AS "inventoryReference",
inv.status AS "inventoryStatus",
COALESCE(container.container_number, booking_container.container_number) AS "containerNumber",
COALESCE(cargo_type.cargo_type_name, b.cargo_free_text, cargo.description) AS "cargoDescription",
CONCAT_WS(
' / ',
NULLIF(inv.status, ''),
NULLIF(COALESCE(container.container_number, booking_container.container_number), ''),
NULLIF(COALESCE(cargo_type.cargo_type_name, b.cargo_free_text, cargo.description), '')
) AS "inventoryInfo",
wh.name AS "warehouseName",
yard.name AS "yardName",
zone.name AS "zoneName",
CASE
WHEN inv.release_date IS NOT NULL THEN 'RELEASE ISSUED'
WHEN $2 = 'PAID' THEN 'FEE PAID - READY FOR RELEASE'
ELSE 'PENDING PAYMENT'
END AS "clearanceStatus"
FROM freight.warehouse_fee_invoices fee
LEFT JOIN freight.warehouse_inventory inv ON inv.id = fee.inventory_id AND inv.deleted_at IS NULL
LEFT JOIN freight.bookings b ON b.id = fee.booking_id AND b.deleted_at IS NULL
LEFT JOIN freight.companies company ON company.id = COALESCE(fee.customer_id, b.company_id)
LEFT JOIN freight.containers container ON container.id = inv.container_id AND container.deleted_at IS NULL
LEFT JOIN freight.booking_container booking_container ON (
booking_container.booking_id = b.id
AND booking_container.deleted_at IS NULL
)
LEFT JOIN freight.cargoes cargo ON cargo.id = inv.cargo_id AND cargo.deleted_at IS NULL
LEFT JOIN freight.cargo_types cargo_type ON cargo_type.id = COALESCE(cargo.cargo_type_id, b.cargo_type_id)
LEFT JOIN freight.warehouses wh ON wh.id = fee.warehouse_id
LEFT JOIN freight.warehouse_yards yard ON yard.id = fee.yard_id
LEFT JOIN freight.warehouse_zones zone ON zone.id = fee.zone_id
WHERE fee.id = $1
LIMIT 1`,
[invoice.id, invoice.status],
);
return {
bookingReference: row?.bookingReference ?? null,
customerName: row?.customerName ?? null,
inventoryReference: row?.inventoryReference ?? null,
inventoryInfo: row?.inventoryInfo ?? null,
inventoryStatus: row?.inventoryStatus ?? null,
containerNumber: row?.containerNumber ?? null,
cargoDescription: row?.cargoDescription ?? null,
warehouseName: row?.warehouseName ?? null,
yardName: row?.yardName ?? null,
zoneName: row?.zoneName ?? null,
clearanceStatus: row?.clearanceStatus ?? (invoice.status === 'PAID' ? 'FEE PAID - READY FOR RELEASE' : 'PENDING PAYMENT'),
};
}
private buildInvoiceDocumentHtml(
invoice: WarehouseFeeInvoice & { items: unknown[] },
invoice: WarehouseFeeInvoiceWithDisplay & { items: unknown[] },
kind: 'INVOICE' | 'RECEIPT',
details: InvoiceDocumentDetails,
): string {
const esc = (value: unknown) =>
String(value ?? '-')
@@ -329,8 +405,13 @@ export class WarehouseInvoiceService {
<div class="summary">
<div><span>Status</span>${esc(invoice.status.replace(/_/g, ' '))}</div>
<div><span>Invoice type</span>${esc(invoice.invoiceType.replace(/_/g, ' '))}</div>
<div><span>Booking ID</span>${esc(invoice.bookingId)}</div>
<div><span>Inventory ID</span>${esc(invoice.inventoryId)}</div>
<div><span>Booking reference</span>${esc(details.bookingReference)}</div>
<div><span>Customer</span>${esc(details.customerName)}</div>
<div><span>Inventory reference</span>${esc(details.inventoryReference)}</div>
<div><span>Inventory info</span>${esc(details.inventoryInfo)}</div>
<div><span>Clearance</span>${esc(details.clearanceStatus)}</div>
<div><span>Warehouse</span>${esc(details.warehouseName)}</div>
<div><span>Yard / Zone</span>${esc([details.yardName, details.zoneName].filter(Boolean).join(' / ') || null)}</div>
<div><span>Period</span>${esc(date(invoice.periodStart))} - ${esc(date(invoice.periodEnd))}</div>
<div><span>Payment</span>${esc(lastPayment ? `${lastPayment.method ?? 'MANUAL'} / ${date(lastPayment.paidAt)}` : '-')}</div>
</div>

View File

@@ -100,20 +100,33 @@ export class WarehouseReleaseDocumentService {
private htmlToBasicPdfBuffer(html: string): Buffer {
const text = this.htmlToPlainText(html);
const lines = this.wrapLines(text, 92).slice(0, 72);
const lines = this.wrapLines(text, 86).slice(0, 52);
const body = lines
.map((line, index) => {
const prefix = index === 0 ? '50 790 Td' : '0 -12 Td';
return `${prefix} (${this.escapePdfText(line)}) Tj`;
const y = 770 - index * 12;
const isTitle = index < 2 || /clearance|release order/i.test(line);
const size = index === 0 ? 13 : isTitle ? 11 : 9.6;
const font = isTitle ? 'F2' : 'F1';
return this.textOp(line, 48, y, size, font);
})
.join('\n');
const stream = `BT\n/F1 10 Tf\n12 TL\n${body}\nET`;
const stream = [
this.lineOp(48, 752, 548, 752),
this.circularSealOps(470, 690),
body,
this.lineOp(48, 126, 238, 126, '0 0 0'),
this.textOp('Authorized warehouse officer name / signature / date', 48, 110, 9, 'F1'),
this.lineOp(312, 126, 548, 126, '0 0 0'),
this.textOp('Customer or driver name / signature / date', 312, 110, 9, 'F1'),
this.textOp('OFFICIAL WAREHOUSE GATE CLEARANCE DOCUMENT', 145, 52, 9, 'F2', '0.08 0.32 0.18'),
].join('\n');
const objects = [
'<< /Type /Catalog /Pages 2 0 R >>',
'<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>',
'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R /F2 5 0 R >> >> /Contents 6 0 R >>',
'<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman >>',
'<< /Type /Font /Subtype /Type1 /BaseFont /Times-Bold >>',
`<< /Length ${Buffer.byteLength(stream, 'latin1')} >>\nstream\n${stream}\nendstream`,
];
@@ -178,4 +191,50 @@ export class WarehouseReleaseDocumentService {
private escapePdfText(value: string): string {
return value.replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)');
}
private textOp(
text: string,
x: number,
y: number,
size: number,
font: 'F1' | 'F2' = 'F1',
color = '0 0 0',
): string {
return `BT\n${color} rg\n/${font} ${size} Tf\n${x} ${y} Td\n(${this.escapePdfText(text)}) Tj\nET`;
}
private lineOp(x1: number, y1: number, x2: number, y2: number, color = '0.08 0.32 0.18'): string {
return `q\n${color} RG\n0.8 w\n${x1} ${y1} m\n${x2} ${y2} l\nS\nQ`;
}
private circularSealOps(cx: number, cy: number): string {
return [
'q',
'0.08 0.32 0.18 RG',
'0.08 0.32 0.18 rg',
'2.2 w',
this.circlePath(cx, cy, 51),
'S',
'0.8 w',
this.circlePath(cx, cy, 41),
'S',
this.textOp('EDR', cx - 14, cy + 18, 14, 'F2', '0.08 0.32 0.18'),
this.textOp('WAREHOUSE', cx - 31, cy + 2, 9, 'F2', '0.08 0.32 0.18'),
this.textOp('CLEARED', cx - 27, cy - 16, 11, 'F2', '0.08 0.32 0.18'),
'Q',
].join('\n');
}
private circlePath(cx: number, cy: number, r: number): string {
const k = 0.5522847498;
const c = r * k;
return [
`${cx + r} ${cy} m`,
`${cx + r} ${cy + c} ${cx + c} ${cy + r} ${cx} ${cy + r} c`,
`${cx - c} ${cy + r} ${cx - r} ${cy + c} ${cx - r} ${cy} c`,
`${cx - r} ${cy - c} ${cx - c} ${cy - r} ${cx} ${cy - r} c`,
`${cx + c} ${cy - r} ${cx + r} ${cy - c} ${cx + r} ${cy} c`,
'h',
].join('\n');
}
}

View File

@@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite --port 5183",
"prebuild": "node -e \"const fs=require('fs'); fs.rmSync('dist',{recursive:true,force:true});\"",
"build": "vite build",
"preview": "vite preview --port 5183",
"lint": "eslint src",

View File

@@ -24,6 +24,15 @@ function DetailRow({ label, value }: { label: string; value: React.ReactNode })
}
export function InventoryDetailModal({ opened, onClose, item }: InventoryDetailModalProps) {
const bookingReference = item?.booking?.reference ?? '-';
const inventorySummary = [
item?.status?.replace(/_/g, ' '),
item?.releaseOrderReference ? `Release ${item.releaseOrderReference}` : null,
item?.warehouse ? `${item.warehouse.name} (${item.warehouse.code})` : null,
]
.filter(Boolean)
.join(' / ');
return (
<Modal opened={opened} onClose={onClose} title="Inventory detail" centered size="xl">
{!item ? (
@@ -33,10 +42,10 @@ export function InventoryDetailModal({ opened, onClose, item }: InventoryDetailM
<Group justify="space-between" align="flex-start">
<Stack gap={2}>
<Text size="lg" fw={800}>
{item.booking?.reference ?? item.bookingId ?? item.id}
{bookingReference}
</Text>
<Text size="sm" c="dimmed">
Inventory ID: {item.id}
{inventorySummary || 'Inventory information'}
</Text>
</Stack>
<InventoryStatusBadge status={item.status} />
@@ -51,12 +60,12 @@ export function InventoryDetailModal({ opened, onClose, item }: InventoryDetailM
<Divider label="Booking & item" labelPosition="left" />
<SimpleGrid cols={{ base: 1, sm: 3 }}>
<DetailRow label="Booking reference" value={bookingReference} />
<DetailRow label="Booking status" value={item.booking?.status ?? '-'} />
<DetailRow label="Payment status" value={item.booking?.paymentStatus ?? '-'} />
<DetailRow label="Trade direction" value={item.booking?.tradeDirection ?? '-'} />
<DetailRow label="Container ID" value={item.containerId ?? '-'} />
<DetailRow label="Cargo ID" value={item.cargoId ?? '-'} />
<DetailRow label="Goods ID" value={item.goodsId ?? '-'} />
<DetailRow label="Inventory status" value={item.status.replace(/_/g, ' ')} />
<DetailRow label="Release reference" value={item.releaseOrderReference ?? '-'} />
<DetailRow label="Quantity" value={formatNumber(item.quantity)} />
<DetailRow label="Weight" value={`${formatNumber(item.weight)} kg`} />
<DetailRow label="Volume" value={item.volume == null ? '-' : formatNumber(item.volume)} />

View File

@@ -130,12 +130,22 @@ function buildSimplePdf(lines: PdfLine[], rawOps: string[] = []): Blob {
export function buildWarehouseInvoicePdf(invoice: WarehouseFeeInvoice, kind: 'INVOICE' | 'RECEIPT') {
const paid = kind === 'RECEIPT' || invoice.status === 'PAID';
const title = `Warehouse Fee ${kind === 'RECEIPT' ? 'Receipt' : 'Invoice'}`;
const bookingReference = firstText(invoice.bookingReference);
const customerName = firstText(invoice.customerName);
const inventoryReference = firstText(invoice.inventoryReference);
const inventoryInfo = firstText(invoice.inventoryInfo, invoice.containerNumber, invoice.cargoDescription);
const clearanceStatus = firstText(
invoice.clearanceStatus,
paid ? 'FEE PAID - READY FOR RELEASE' : 'PENDING PAYMENT',
);
const lines: PdfLine[] = [
{ text: 'Ethio-Djibouti Railway S.C.', size: 12, bold: true, yGap: 0, align: 'center' },
{ text: title, size: 23, bold: true, yGap: 28, align: 'center' },
{ text: `Document No: ${invoice.invoiceNumber}`, size: 12, bold: true, yGap: 32, align: 'center' },
{ text: `Status: ${invoice.status.replace(/_/g, ' ')} Type: ${invoice.invoiceType.replace(/_/g, ' ')}`, align: 'center' },
{ text: `Booking: ${invoice.bookingId ?? '-'} Inventory: ${invoice.inventoryId}`, align: 'center' },
{ text: `Booking Reference: ${bookingReference} Customer: ${customerName}`, align: 'center' },
{ text: `Inventory Reference: ${inventoryReference} Inventory Info: ${inventoryInfo}`, align: 'center' },
{ text: `Clearance: ${clearanceStatus}`, align: 'center' },
{ text: `Issued: ${fmtDate(invoice.issuedAt)} Paid At: ${fmtDate(invoice.paidAt)}`, align: 'center' },
{ text: 'ITEMS', size: 13, bold: true, yGap: 30, align: 'center' },
...(invoice.items ?? []).flatMap((item) => [
@@ -201,15 +211,33 @@ export function buildWarehouseExitPaperPdf(input: WarehouseFeeInvoice | Warehous
const releasedItem = context.releasedItem;
const inventory = context.inventory ?? releasedItem;
const releasedAt = context.releasedAt ?? new Date();
const releaseReference = `REL-${invoice.inventoryId.slice(0, 8).toUpperCase()}`;
const releaseReference = firstText(
inventory?.releaseOrderReference,
releasedItem?.releaseOrderReference,
invoice.inventoryReference,
booking?.reference ? `REL-${booking.reference.replace(/^BK-?/i, '')}` : null,
);
const customerName = firstText(
booking?.company?.name,
booking?.company?.companyName,
booking?.company?.label,
booking?.company?.contactPersonName,
invoice.customerId,
invoice.customerName,
);
const weightTons = firstText(tons(booking?.cargoTotalWeightVgm), tons(inventory?.weight));
const inventoryInfo = firstText(
invoice.inventoryInfo,
invoice.containerNumber,
invoice.cargoDescription,
inventory?.status,
releasedItem?.status,
);
const bookingReference = firstText(
booking?.reference,
(inventory as unknown as { bookingReference?: string })?.bookingReference,
invoice.bookingReference,
releasedItem?.booking?.reference,
);
return buildSimplePdf([
{ text: 'Ethio-Djibouti Railway S.C.', size: 12, bold: true, yGap: 0, align: 'center' },
@@ -217,16 +245,18 @@ export function buildWarehouseExitPaperPdf(input: WarehouseFeeInvoice | Warehous
{ text: '[ GATE CLEARANCE ]', size: 14, bold: true, color: 'green', yGap: 24, align: 'center' },
{ text: `Release Reference: ${releaseReference}`, bold: true, yGap: 30, align: 'center' },
{ text: `Invoice No: ${invoice.invoiceNumber}`, align: 'center' },
{ text: `Booking Reference: ${firstText(booking?.reference, (inventory as unknown as { bookingReference?: string })?.bookingReference, invoice.bookingId, releasedItem?.bookingId)}`, align: 'center' },
{ text: `Booking Reference: ${bookingReference}`, align: 'center' },
{ text: `Customer: ${customerName}`, align: 'center' },
{ text: `Warehouse: ${firstText(inventory?.warehouse?.name, inventory?.warehouse?.code, invoice.warehouseId, releasedItem?.warehouseId)}`, align: 'center' },
{ text: `Yard: ${firstText(inventory?.yard?.name, inventory?.yard?.code, invoice.yardId, releasedItem?.yardId)}`, align: 'center' },
{ text: `Zone: ${firstText(inventory?.zone?.name, inventory?.zone?.code, invoice.zoneId, releasedItem?.zoneId)}`, align: 'center' },
{ text: `Inventory Info: ${inventoryInfo}`, align: 'center' },
{ text: `Warehouse: ${firstText(inventory?.warehouse?.name, inventory?.warehouse?.code)}`, align: 'center' },
{ text: `Yard: ${firstText(inventory?.yard?.name, inventory?.yard?.code)}`, align: 'center' },
{ text: `Zone: ${firstText(inventory?.zone?.name, inventory?.zone?.code)}`, align: 'center' },
{ text: `Booking Container: ${containerSummary(booking, inventory)}`, align: 'center' },
{ text: `Weight: ${weightTons}`, align: 'center' },
{ text: `Inventory Status: ${releasedItem?.status ?? inventory?.status ?? 'RELEASED'}`, align: 'center' },
{ text: `Clearance: ${invoice.clearanceStatus ?? 'CLEARED FOR WAREHOUSE EXIT'}`, bold: true, color: 'green', align: 'center' },
{ text: `Release Date & Time: ${fmtDate(releasedAt)}`, align: 'center' },
{ text: 'This document authorizes the listed booking/goods to leave the warehouse gate.', yGap: 30, align: 'center' },
{ text: 'This sealed document authorizes the listed booking/goods to leave the warehouse gate.', yGap: 30, align: 'center' },
], [
...buildAuthorizationBand('CLEARED'),
textOp('Warehouse officer name / signature / date:', 72, 190, 10),

View File

@@ -1,10 +1,6 @@
export const API_BASE_URL =
<<<<<<< HEAD
import.meta.env.VITE_API_URL?.replace(/\/+$/, '') || 'https://edrfreightapi.triaplc.com';
=======
import.meta.env.VITE_BASE_API_URL ||
import.meta.env.VITE_API_URL ||
'https://edrfreightapi.triaplc.com';
//export const API_BASE_URL = 'http://localhost:3001';
>>>>>>> 2f06817e4811190cdc9ef8a2975aeca1e31c3484

View File

@@ -737,8 +737,16 @@ export interface WarehouseFeeInvoice {
id: string;
invoiceNumber: string;
bookingId?: string | null;
bookingReference?: string | null;
customerId?: string | null;
customerName?: string | null;
inventoryId: string;
inventoryReference?: string | null;
inventoryInfo?: string | null;
inventoryStatus?: string | null;
containerNumber?: string | null;
cargoDescription?: string | null;
clearanceStatus?: string | null;
facilityId?: string | null;
warehouseId?: string | null;
yardId?: string | null;