mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Invoice and clearance seal approval
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)} />
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user