mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
Invoice and clearance seal approval
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
import type { WarehouseFeeInvoice, WarehouseInventoryItem } from '@/types/warehouse';
|
||||
import type { BookingDetail } from '@/types/booking';
|
||||
|
||||
type PdfLine = { text: string; size?: number; bold?: boolean; x?: number; yGap?: number; color?: 'black' | 'green' };
|
||||
type PdfLine = {
|
||||
text: string;
|
||||
size?: number;
|
||||
bold?: boolean;
|
||||
x?: number;
|
||||
yGap?: number;
|
||||
color?: 'black' | 'green';
|
||||
align?: 'left' | 'center' | 'right';
|
||||
};
|
||||
|
||||
export interface WarehouseExitPaperContext {
|
||||
invoice: WarehouseFeeInvoice;
|
||||
@@ -58,6 +66,33 @@ const buildCircularSeal = (cx: number, cy: number, label: 'PAID' | 'CLEARED') =>
|
||||
'Q',
|
||||
].join('\n');
|
||||
|
||||
const estimateTextWidth = (text: string, size: number) => text.length * size * 0.52;
|
||||
|
||||
const textX = (text: string, size: number, align: PdfLine['align'] = 'left', x?: number) => {
|
||||
if (typeof x === 'number') return x;
|
||||
if (align === 'center') return Math.max(36, (595 - estimateTextWidth(text, size)) / 2);
|
||||
if (align === 'right') return Math.max(36, 535 - estimateTextWidth(text, size));
|
||||
return 60;
|
||||
};
|
||||
|
||||
const lineOp = (x1: number, y1: number, x2: number, y2: number, color = '0.65 0.7 0.76') =>
|
||||
`q\n${color} RG\n0.8 w\n${x1} ${y1} m\n${x2} ${y2} l\nS\nQ`;
|
||||
|
||||
const textOp = (
|
||||
text: string,
|
||||
x: number,
|
||||
y: number,
|
||||
size = 10,
|
||||
bold = false,
|
||||
color = '0 0 0',
|
||||
) => `BT\n${color} rg\n/${bold ? 'F2' : 'F1'} ${size} Tf\n${x} ${y} Td\n(${escapePdfText(text)}) Tj\nET`;
|
||||
|
||||
const buildAuthorizationBand = (label: 'PAID' | 'CLEARED') => [
|
||||
lineOp(60, 242, 535, 242),
|
||||
textOp('AUTHORIZED SEAL', 382, 218, 9, true, GREEN),
|
||||
buildCircularSeal(452, 155, label),
|
||||
];
|
||||
|
||||
function buildSimplePdf(lines: PdfLine[], rawOps: string[] = []): Blob {
|
||||
let y = 800;
|
||||
const streamLines = lines.map((line) => {
|
||||
@@ -65,7 +100,7 @@ function buildSimplePdf(lines: PdfLine[], rawOps: string[] = []): Blob {
|
||||
const size = line.size ?? 10;
|
||||
const font = line.bold ? '/F2' : '/F1';
|
||||
const color = line.color === 'green' ? `${GREEN} rg` : '0 0 0 rg';
|
||||
return `BT\n${color}\n${font} ${size} Tf\n${line.x ?? 46} ${y} Td\n(${escapePdfText(line.text)}) Tj\nET`;
|
||||
return `BT\n${color}\n${font} ${size} Tf\n${textX(line.text, size, line.align, line.x)} ${y} Td\n(${escapePdfText(line.text)}) Tj\nET`;
|
||||
});
|
||||
const stream = [...rawOps, ...streamLines].join('\n');
|
||||
const objects = [
|
||||
@@ -94,34 +129,43 @@ 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 lines: PdfLine[] = [
|
||||
{ text: 'Ethio-Djibouti Railway S.C.', size: 11, bold: true, yGap: 0 },
|
||||
{ text: `Warehouse Fee ${kind === 'RECEIPT' ? 'Receipt' : 'Invoice'}`, size: 22, bold: true, yGap: 26 },
|
||||
{ text: `Document No: ${invoice.invoiceNumber}`, bold: true, yGap: 34 },
|
||||
{ text: `Status: ${invoice.status.replace(/_/g, ' ')}` },
|
||||
{ text: `Invoice Type: ${invoice.invoiceType.replace(/_/g, ' ')}` },
|
||||
{ text: `Booking ID: ${invoice.bookingId ?? '-'}` },
|
||||
{ text: `Inventory ID: ${invoice.inventoryId}` },
|
||||
{ text: `Issued: ${fmtDate(invoice.issuedAt)}` },
|
||||
{ text: `Paid At: ${fmtDate(invoice.paidAt)}` },
|
||||
{ text: 'Items', size: 14, bold: true, yGap: 26 },
|
||||
{ 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: `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) => [
|
||||
{ text: item.description, bold: true },
|
||||
{ text: item.description, bold: true, align: 'center' as const },
|
||||
{
|
||||
text: `${item.feeType.replace(/_/g, ' ')} | Qty ${Number(item.quantity ?? 0).toLocaleString()} | Rate ${money(item.unitRate, item.currency)} | Amount ${money(item.amount, item.currency)}`,
|
||||
yGap: 13,
|
||||
align: 'center' as const,
|
||||
},
|
||||
]),
|
||||
{ text: 'Totals', size: 14, bold: true, yGap: 28 },
|
||||
{ text: `Subtotal: ${money(invoice.subtotalAmount, invoice.currency)}` },
|
||||
{ text: `Tax: ${money(invoice.taxAmount, invoice.currency)}` },
|
||||
{ text: `Total: ${money(invoice.totalAmount, invoice.currency)}`, bold: true },
|
||||
{ text: `Paid: ${money(invoice.paidAmount, invoice.currency)}` },
|
||||
{ text: `Balance: ${money(invoice.balanceAmount, invoice.currency)}`, bold: true },
|
||||
{ text: 'Prepared by EDR warehouse finance', yGap: 34 },
|
||||
{ text: 'Authorized seal / signature: ______________________________' },
|
||||
{ text: 'TOTALS', size: 13, bold: true, yGap: 30, align: 'center' },
|
||||
{ text: `Subtotal: ${money(invoice.subtotalAmount, invoice.currency)}`, align: 'center' },
|
||||
{ text: `Tax: ${money(invoice.taxAmount, invoice.currency)}`, align: 'center' },
|
||||
{ text: `Total: ${money(invoice.totalAmount, invoice.currency)}`, bold: true, align: 'center' },
|
||||
{ text: `Paid: ${money(invoice.paidAmount, invoice.currency)}`, align: 'center' },
|
||||
{ text: `Balance: ${money(invoice.balanceAmount, invoice.currency)}`, bold: true, align: 'center' },
|
||||
];
|
||||
return buildSimplePdf(lines, paid ? [buildCircularSeal(462, 712, 'PAID')] : []);
|
||||
const authorizationOps = [
|
||||
...buildAuthorizationBand('PAID'),
|
||||
textOp('Prepared by EDR warehouse finance', 72, 196, 10),
|
||||
textOp('Finance officer name / signature / date:', 72, 164, 10),
|
||||
lineOp(245, 162, 360, 162, '0 0 0'),
|
||||
];
|
||||
const invoiceOps = [
|
||||
lineOp(60, 242, 535, 242),
|
||||
textOp('Prepared by EDR warehouse finance', 72, 196, 10),
|
||||
textOp('Finance officer name / signature / date:', 72, 164, 10),
|
||||
lineOp(245, 162, 360, 162, '0 0 0'),
|
||||
];
|
||||
return buildSimplePdf(lines, paid ? authorizationOps : invoiceOps);
|
||||
}
|
||||
|
||||
const firstText = (...values: Array<unknown>) => {
|
||||
@@ -168,21 +212,26 @@ export function buildWarehouseExitPaperPdf(input: WarehouseFeeInvoice | Warehous
|
||||
const weightTons = firstText(tons(booking?.cargoTotalWeightVgm), tons(inventory?.weight));
|
||||
|
||||
return buildSimplePdf([
|
||||
{ text: 'Ethio-Djibouti Railway S.C.', size: 11, bold: true, yGap: 0 },
|
||||
{ text: 'Warehouse Release / Exit Paper', size: 22, bold: true, yGap: 26 },
|
||||
{ text: `Release Reference: ${releaseReference}`, bold: true, yGap: 34 },
|
||||
{ text: `Invoice No: ${invoice.invoiceNumber}` },
|
||||
{ text: `Booking Reference: ${firstText(booking?.reference, (inventory as unknown as { bookingReference?: string })?.bookingReference, invoice.bookingId, releasedItem?.bookingId)}` },
|
||||
{ text: `Customer: ${customerName}` },
|
||||
{ text: `Warehouse: ${firstText(inventory?.warehouse?.name, inventory?.warehouse?.code, invoice.warehouseId, releasedItem?.warehouseId)}` },
|
||||
{ text: `Yard: ${firstText(inventory?.yard?.name, inventory?.yard?.code, invoice.yardId, releasedItem?.yardId)}` },
|
||||
{ text: `Zone: ${firstText(inventory?.zone?.name, inventory?.zone?.code, invoice.zoneId, releasedItem?.zoneId)}` },
|
||||
{ text: `Booking Container: ${containerSummary(booking, inventory)}` },
|
||||
{ text: `Weight: ${weightTons}` },
|
||||
{ text: `Inventory Status: ${releasedItem?.status ?? inventory?.status ?? 'RELEASED'}` },
|
||||
{ text: `Release Date & Time: ${fmtDate(releasedAt)}` },
|
||||
{ text: 'This document authorizes the listed booking/goods to leave the warehouse gate.', yGap: 30 },
|
||||
{ text: 'Warehouse officer name / signature / date: ______________________________', yGap: 40 },
|
||||
{ text: 'Customer or driver name / signature / date: ______________________________', yGap: 28 },
|
||||
], [buildCircularSeal(462, 712, 'CLEARED')]);
|
||||
{ text: 'Ethio-Djibouti Railway S.C.', size: 12, bold: true, yGap: 0, align: 'center' },
|
||||
{ text: 'Warehouse Release / Exit Paper', size: 23, bold: true, yGap: 28, align: 'center' },
|
||||
{ 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: `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: `Booking Container: ${containerSummary(booking, inventory)}`, align: 'center' },
|
||||
{ text: `Weight: ${weightTons}`, align: 'center' },
|
||||
{ text: `Inventory Status: ${releasedItem?.status ?? inventory?.status ?? 'RELEASED'}`, 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' },
|
||||
], [
|
||||
...buildAuthorizationBand('CLEARED'),
|
||||
textOp('Warehouse officer name / signature / date:', 72, 190, 10),
|
||||
lineOp(260, 188, 360, 188, '0 0 0'),
|
||||
textOp('Customer or driver name / signature / date:', 72, 148, 10),
|
||||
lineOp(260, 146, 360, 146, '0 0 0'),
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user