first commit

This commit is contained in:
Isuru Bimsara
2026-08-14 22:46:02 +05:30
commit 81d0e65686
132 changed files with 24398 additions and 0 deletions
@@ -0,0 +1,372 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@page {
size: A4;
margin: 0;
}
body {
background: white;
font-family: Arial, Helvetica, sans-serif;
padding: 10px;
}
.sheet {
width: 100%;
margin: 0 auto;
background: white;
border: 1px solid #222;
}
.company-band {
display: grid;
grid-template-columns: 1fr 1.35fr 0.95fr;
min-height: 112px;
border-bottom: 1px solid #444;
}
.company-left,
.company-middle,
.company-right {
padding: 4px 6px;
font-size: 10px;
}
.company-left {
line-height: 1.35;
}
.company-middle {
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.logo-placeholder {
font-size: 28px;
font-weight: 700;
letter-spacing: 6px;
color: #1c5c9d;
}
.company-middle .tagline {
margin-top: 4px;
font-size: 10px;
color: #555;
}
.company-right {
display: flex;
align-items: flex-start;
justify-content: flex-end;
text-align: right;
line-height: 1.25;
}
.company-right .stack {
max-width: 100%;
}
.title-row {
border-bottom: 1px solid #444;
text-align: center;
padding: 5px 0 4px;
color: #7da3de;
font-size: 22px;
font-weight: 700;
}
.bill-row {
display: grid;
grid-template-columns: 1.55fr 1fr;
border-bottom: 1px solid #444;
min-height: 72px;
}
.bill-left,
.bill-right {
padding: 4px 6px;
font-size: 10px;
}
.bill-left {
border-right: 1px solid #444;
line-height: 1.25;
}
.bill-right {
display: flex;
flex-direction: column;
justify-content: center;
gap: 2px;
line-height: 1.2;
}
.bill-line {
display: flex;
justify-content: space-between;
gap: 6px;
}
.meta-row {
display: grid;
grid-template-columns: 1fr 1fr;
border-bottom: 1px solid #444;
}
.meta-col {
padding: 4px 6px;
font-size: 10px;
line-height: 1.25;
}
.meta-col:first-child {
border-right: 1px solid #444;
}
.meta-line {
display: flex;
justify-content: space-between;
gap: 8px;
}
.meta-label {
font-weight: 700;
min-width: 112px;
}
.items-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.items-table th,
.items-table td {
border: 1px solid #444;
font-size: 10px;
padding: 2px 3px;
vertical-align: middle;
}
.items-table th {
text-align: center;
font-weight: 700;
background: #b7d2ea;
}
.no-col { width: 5%; }
.desc-col { width: 37%; }
.remarks-col { width: 33%; }
.amount-col { width: 12%; }
.confirm-col { width: 13%; }
.center {
text-align: center;
}
.right {
text-align: right;
}
.spacer-row td {
height: 26px;
}
.total-row td {
font-weight: 700;
background: #f5f5f5;
}
.approval-area {
height: 94px;
border-top: 1px solid #444;
display: flex;
align-items: flex-end;
justify-content: flex-end;
padding: 10px 12px 14px;
}
.approval-box {
text-align: center;
min-width: 230px;
}
.signature-line {
margin-top: 18px;
border-top: 1px solid #222;
width: 100%;
}
.footer-row {
border-top: 1px solid #444;
text-align: center;
padding: 4px 0 5px;
font-size: 10px;
}
.watermark-line {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-size: 10px;
}
.line-dots {
letter-spacing: 3px;
text-align: right;
}
.company-logo-text {
text-align: center;
color: #1c5c9d;
font-size: 14px;
line-height: 1.1;
font-weight: 700;
}
.company-logo-small {
font-size: 10px;
font-weight: 400;
color: #333;
}
.header-stack {
line-height: 1.15;
}
.small-note {
font-size: 9px;
color: #444;
}
.highlight {
background: #ffeb3b;
padding: 0 2px;
}
.multi-line {
white-space: pre-line;
}
</style>
</head>
<body>
<%
const creditCompany = typeof company !== 'undefined' && company ? company : {};
const creditHeader = typeof header !== 'undefined' && header ? header : {};
const creditBillTo = typeof billTo !== 'undefined' && billTo ? billTo : {};
const creditMeta = typeof meta !== 'undefined' && meta ? meta : {};
const creditVessel = typeof vessel !== 'undefined' && vessel ? vessel : {};
const creditOrderMeta = typeof orderMeta !== 'undefined' && orderMeta ? orderMeta : {};
const creditItems = typeof items !== 'undefined' && Array.isArray(items) ? items : [];
const creditApprovedByLabel = typeof approvedByLabel !== 'undefined' && approvedByLabel ? approvedByLabel : 'Approved by GLMS Finance Dept.';
const creditDepartmentLabel = typeof departmentLabel !== 'undefined' && departmentLabel ? departmentLabel : 'GLMS - Accounts Department';
%>
<div class="sheet">
<div class="company-band">
<div class="company-left">
<div><strong><%= creditCompany.name || 'GREEK-LANKA MARITIME SERVICES (PVT) LTD' %></strong></div>
<div><%= creditCompany.addressLine1 || '' %></div>
<div><%= creditCompany.addressLine2 || '' %></div>
<div><%= creditCompany.addressLine3 || '' %></div>
<div><%= creditCompany.postal || '' %></div>
<div><%= creditCompany.contact || '' %></div>
<div><%= creditCompany.br || '' %></div>
<div><%= creditCompany.license || '' %></div>
</div>
<div class="company-middle">
<div>
<div class="company-logo-text">GREEK<br/>LANKA</div>
<div class="company-logo-small">We Serve the Ocean</div>
</div>
</div>
<div class="company-right">
<div class="stack">
<div class="header-stack">
<div style="font-size: 14px; font-weight:700; color:#7da3de; letter-spacing:0.5px;">CREDIT NOTE</div>
</div>
</div>
</div>
</div>
<div class="title-row"><%= creditHeader.title || 'CREDIT NOTE' %></div>
<div class="bill-row">
<div class="bill-left">
<div><strong>Bill To :</strong> <%= creditBillTo.name || 'Master & Owner of MV TEAM VENTURES' %></div>
<div class="multi-line"><%- (creditBillTo.details || '').replace(/\n/g, '<br/>') || 'VRIDHI MARITIME SHIP MANAGEMENT & OPERATION LLC<br/>Office No: 2004, The Prism Tower, Dubai' %></div>
</div>
<div class="bill-right">
<div class="bill-line"><span><strong>CRN NO :</strong></span><span><%= creditMeta.crnNo || 'N/A' %></span></div>
<div class="bill-line"><span><strong>DATE :</strong></span><span><%= creditMeta.date || 'N/A' %></span></div>
</div>
</div>
<div class="meta-row">
<div class="meta-col">
<div class="meta-line"><span class="meta-label">Vessel Name :</span> <span><%= creditVessel.name || 'N/A' %></span></div>
<div class="meta-line"><span class="meta-label">Port :</span> <span><%= creditVessel.port || 'N/A' %></span></div>
<div class="meta-line"><span class="meta-label">Name of Agent :</span> <span><%= creditVessel.agent || 'N/A' %></span></div>
</div>
<div class="meta-col">
<div class="meta-line"><span class="meta-label">GRT :</span> <span><%= creditVessel.grt || 'N/A' %></span></div>
<div class="meta-line"><span class="meta-label">IMO Number :</span> <span><%= creditVessel.imo || 'N/A' %></span></div>
<div class="meta-line"><span class="meta-label">Port/Country :</span> <span><%= creditVessel.portCountry || 'N/A' %></span></div>
</div>
</div>
<div style="overflow-x:auto;">
<table class="items-table">
<thead>
<tr>
<th class="no-col">DESCRIPTION</th>
<th class="amount-col">AMOUNT</th>
</tr>
</thead>
<tbody>
<% if (creditItems.length > 0) { %>
<% creditItems.forEach((item, index) => { %>
<tr class="spacer-row">
<td>
<%= item.description || item.desc || '' %>
<% if (item.note) { %>
<div class="small-note"><%= item.note %></div>
<% } %>
</td>
<td class="right"><%= (typeof item.amount !== 'undefined') ? item.amount : '' %></td>
</tr>
<% }); %>
<% } %>
<tr class="total-row">
<td class="right">Total Amount</td>
<td class="right"><%= total || totalAmount || 0 %></td>
</tr>
</tbody>
</table>
</div>
<div class="approval-area">
<div class="approval-box">
<div style="height: 42px;"></div>
<div class="signature-line"></div>
<div style="margin-top: 4px;"><%= creditApprovedByLabel %></div>
</div>
</div>
<div class="footer-row">
<%= creditDepartmentLabel %>
</div>
</div>
</body>
</html>
@@ -0,0 +1,131 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/templates/documents/creditNote/creditNote.template.js
const { log } = require("../../../utils/consoleLog.utill");
module.exports = async (data) => {
log("📋 Preparing Credit Note data for PDF...");
let payload = data;
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch (error) {
payload = {};
}
}
const source = payload.documentData || payload.document_data || payload.data || payload;
const pick = (...keys) => {
for (const key of keys) {
const value = source?.[key];
if (value !== undefined && value !== null && value !== "") {
return value;
}
}
return "";
};
// Normalize incoming fields and provide defaults matching the screenshot
const company = {
name: pick("companyName", "company_name") || "GREEK-LANKA MARITIME SERVICES (PVT) LTD",
addressLine1: pick("companyAddressLine1", "company_address_line1") || "No. 56/2, Dharmapala Mw, Kotte",
addressLine2: pick("companyAddressLine2", "company_address_line2") || "Sri Jayewardenepura",
addressLine3: pick("companyAddressLine3", "company_address_line3") || "Sri Lanka",
postal: pick("companyPostal", "company_postal") || "Postal Code : 10100",
contact: pick("companyContact", "company_contact") || "Tel:+94 11 2083206 / Mobile (24/7) : +94 777 232 271",
br: pick("companyBR", "company_br") || "BR No : PV 0022630",
license: pick("companyLicense", "company_license") || "License No : SA00317-2024",
logoUrl: pick("logoUrl", "logo_url") ||
"https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
};
const header = {
title: pick("title") || "CREDIT NOTE",
};
const billTo = {
name: pick("billToName", "bill_to_name") || "Master & Owner of MV TEAM VENTURES",
details: pick("billToDetails", "bill_to_details") || "VRIDHI MARITIME SHIP MANAGEMENT & OPERATION LLC\nOffice No: 2004, The Prism Tower, Dubai",
};
const meta = {
crnNo: pick("crnNo", "crn_no", "creditNoteNumber", "credit_note_number", "reference_no") || "GLMS/COLOMBO/474/CRN01",
date: pick("date") || new Date().toLocaleDateString(),
};
const vessel = {
name: pick("vesselName", "vessel_name") || 'MV "TEAM VENTURES"',
grt: pick("grt", "GRT") || "25,543 MT",
port: pick("port", "port_name") || "REPAIRS AT COLOMBO DOCK YARD",
imo: pick("imo", "imo_number", "imoNumber") || "9339765",
agent: pick("nameOfAgent", "name_of_agent", "agent") || "",
portCountry: pick("portCountry", "port_country") || "COLOMBO / SRI LANKA",
};
const orderMeta = {
orderDate: pick("orderDate", "order_date") || meta.date,
orderNo: pick("orderNo", "order_no") || "",
portReference: pick("portReference", "port_reference") || "",
};
// Items: array of { description, amount }
const items = (Array.isArray(source.items) ? source.items : []).map((it) => ({
description: it.description || it.desc || it.item_name || '',
amount: Number(it.amount || it.value || it.total || it.totalAmount || 0),
quantity: it.quantity ?? it.qty ?? '',
note: it.note || it.notes || '',
}));
const computedTotal = items.reduce((s, i) => s + (i.amount || 0), 0);
const templateData = {
company,
header,
billTo,
meta,
vessel,
orderMeta,
items,
totalAmount: Number(pick("totalAmount", "total") || computedTotal),
approvedBy: pick("approvedBy", "approved_by") || 'GLMS Finance Dept.',
approvedByLabel: pick("approvedByLabel", "approved_by_label") || 'Approved by GLMS Finance Dept.',
departmentLabel: pick("departmentLabel", "department_label") || 'GLMS - Accounts Department',
};
// Top-level conveniences for templates and pdfRenderer
templateData.logoUrl = company.logoUrl;
templateData.creditNoteNumber = templateData.meta.crnNo;
templateData.date = templateData.meta.date;
templateData.billToName = templateData.billTo.name;
templateData.billToDetails = templateData.billTo.details;
templateData.vessel = templateData.vessel;
// Provide invoiceNumber if present
templateData.invoiceNumber = pick("invoiceNumber", "invoice_number", "invoice_no") || '';
// Provide subtotal/tax/discount/total for EJS summary table compatibility
templateData.subtotal = Number(pick("subtotal", "sub_total") || computedTotal);
templateData.tax = Number(pick("tax") || 0);
templateData.discount = Number(pick("discount") || 0);
templateData.total = Number(pick("totalAmount", "total") || templateData.subtotal + templateData.tax - templateData.discount);
// Provide reason / notes
templateData.reason = pick("reason", "remarks", "notes") || '';
// Provide paymentTerms and notes for EJS
templateData.paymentTerms = pick("paymentTerms", "payment_terms") || '';
templateData.notes = pick("notes", "note") || '';
log("✅ Credit Note data prepared", JSON.stringify(templateData));
return templateData;
};
@@ -0,0 +1,266 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@page {
size: A4;
margin: 0;
}
body {
background: #fff;
font-family: Arial, Helvetica, sans-serif;
padding: 8px;
}
.sheet {
width: 100%;
border: 1px solid #222;
background: #fff;
}
.top-band {
display: grid;
grid-template-columns: 1.45fr 1fr;
border-bottom: 1px solid #444;
min-height: 76px;
}
.top-left,
.top-right {
padding: 4px 6px;
font-size: 10px;
}
.top-right {
text-align: right;
line-height: 1.2;
}
.top-line {
margin-bottom: 2px;
}
.highlight-box {
display: grid;
grid-template-columns: 1.6fr 1fr;
border-bottom: 1px solid #444;
}
.highlight-left,
.highlight-right {
padding: 4px 6px;
min-height: 58px;
font-size: 10px;
}
.highlight-left {
border-right: 1px solid #444;
}
.highlight-row {
display: grid;
grid-template-columns: 155px 1fr;
gap: 6px;
margin-bottom: 2px;
}
.label {
font-weight: 700;
}
.permission-line {
text-align: center;
padding: 10px 8px 4px;
font-size: 10px;
}
.dots {
text-align: center;
letter-spacing: 3px;
color: #555;
padding: 2px 0 8px;
}
.company-name {
text-align: center;
font-size: 18px;
font-weight: 700;
padding: 6px 0 10px;
}
.company-subtitle {
text-align: center;
font-size: 13px;
font-weight: 700;
padding-bottom: 10px;
}
.date-row {
display: flex;
justify-content: flex-end;
border-bottom: 1px solid #444;
padding: 0 6px 4px;
}
.date-box {
min-width: 138px;
background: #ffeb3b;
font-weight: 700;
padding: 2px 8px;
text-align: right;
}
.form-code {
font-size: 9px;
padding: 2px 4px 3px;
}
.items-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.items-table th,
.items-table td {
border: 1px solid #444;
font-size: 10px;
padding: 2px 3px;
vertical-align: middle;
}
.items-table th {
text-align: center;
font-weight: 700;
background: #fff;
}
.items-table thead th {
height: 33px;
}
.no-col { width: 8%; }
.qty-col { width: 7%; }
.unit-col { width: 7%; }
.description-col { width: 40%; }
.rate-col { width: 13%; }
.total-col { width: 12%; }
.confirm-col { width: 13%; }
.center {
text-align: center;
}
.right {
text-align: right;
}
.spacer-row td {
height: 24px;
}
.muted {
color: #666;
}
</style>
</head>
<body>
<%
const customTitle = typeof title !== 'undefined' && title ? title : 'CUSTOM DOCUMENT';
const customDate = typeof date !== 'undefined' && date ? date : 'N/A';
const customTelePhone = typeof telePhone !== 'undefined' && telePhone ? telePhone : '';
const customEmail = typeof emailAddress !== 'undefined' && emailAddress ? emailAddress : '';
const customOfficeAddressLine1 = typeof officeAddressLine1 !== 'undefined' && officeAddressLine1 ? officeAddressLine1 : '';
const customOfficeAddressLine2 = typeof officeAddressLine2 !== 'undefined' && officeAddressLine2 ? officeAddressLine2 : '';
const customOfficeAddressLine3 = typeof officeAddressLine3 !== 'undefined' && officeAddressLine3 ? officeAddressLine3 : '';
const customDirectorOfCustoms = typeof directorOfCustoms !== 'undefined' && directorOfCustoms ? directorOfCustoms : '';
const customVehicleNo = typeof vehicleNo !== 'undefined' && vehicleNo ? vehicleNo : '';
const customPermitNo = typeof permitNo !== 'undefined' && permitNo ? permitNo : '';
const customChiefSecurityOfficer = typeof chiefSecurityOfficer !== 'undefined' && chiefSecurityOfficer ? chiefSecurityOfficer : '';
const customExportGate = typeof exportGate !== 'undefined' && exportGate ? exportGate : '';
const customSlpa = typeof slpa !== 'undefined' && slpa ? slpa : '';
const customGateNo = typeof gateNo !== 'undefined' && gateNo ? gateNo : '';
const customPermissionText = typeof permissionText !== 'undefined' && permissionText ? permissionText : '';
const customCompanyName = typeof companyName !== 'undefined' && companyName ? companyName : 'Oceanic Maritime Solutions (Pvt) Ltd';
const customSubtitle = typeof subtitle !== 'undefined' && subtitle ? subtitle : 'LICENSED SHIPCHANDLERS';
const customSectionCode = typeof sectionCode !== 'undefined' && sectionCode ? sectionCode : 'SC/FORM/06';
const customDateLabel = typeof dateLabel !== 'undefined' && dateLabel ? dateLabel : 'DATE :';
const customItems = typeof items !== 'undefined' && Array.isArray(items) ? items : [];
const rows = Math.max(7, customItems.length);
%>
<div class="sheet">
<div class="top-band">
<div class="top-left">
<div class="top-line"><span class="label">Tele :</span> <%= customTelePhone || '' %></div>
<div class="top-line"><span class="label">E-mail Address :</span> <%= customEmail || '' %></div>
</div>
<div class="top-right">
<div><%= customOfficeAddressLine1 %></div>
<div><%= customOfficeAddressLine2 %></div>
<div><%= customOfficeAddressLine3 %></div>
</div>
</div>
<div class="highlight-box">
<div class="highlight-left">
<div class="highlight-row"><div class="label">The Director of Customs</div><div><%= customDirectorOfCustoms %></div></div>
<div class="highlight-row"><div class="label">Export Gate</div><div><span style="color:#e53935;">Colombo 1.</span></div></div>
<div class="highlight-row"><div class="label">Vehicle No. / Permit No.</div><div><%= customVehicleNo %> / <%= customPermitNo %></div></div>
</div>
<div class="highlight-right">
<div class="highlight-row"><div class="label">Chief Security Officer</div><div><%= customChiefSecurityOfficer %></div></div>
<div class="highlight-row"><div class="label">SLPA</div><div><%= customSlpa %></div></div>
<div class="highlight-row"><div class="label">Gate No.</div><div><%= customGateNo %></div></div>
</div>
</div>
<div class="permission-line"><%= customPermissionText %></div>
<div class="dots">............................................................</div>
<div class="company-name"><%= customCompanyName %></div>
<div class="company-subtitle"><%= customSubtitle %></div>
<div class="date-row">
<div class="date-box"><%= customDateLabel %> <%= customDate %></div>
</div>
<div class="form-code"><%= customSectionCode %></div>
<table class="items-table">
<thead>
<tr>
<th class="no-col">NO</th>
<th class="qty-col">QTY</th>
<th class="unit-col">UNIT</th>
<th class="description-col">DESCRIPTION</th>
<th class="rate-col">RATE</th>
<th class="total-col">TOTAL</th>
<th class="confirm-col">Confirm<br/>Order Rate</th>
</tr>
</thead>
<tbody>
<% for (let index = 0; index < rows; index++) { %>
<% const item = customItems[index] || {}; %>
<tr class="spacer-row">
<td class="center"><%= index + 1 %></td>
<td class="center"><%= item.quantity || item.qty || '' %></td>
<td class="center"><%= item.unit || '' %></td>
<td><%= item.description || item.item_name || item.name || '' %></td>
<td class="right"><%= item.rate || item.unit_price || '-' %></td>
<td class="right"><%= item.total || item.amount || '-' %></td>
<td><%= item.confirmOrderRate || item.confirm_order_rate || '' %></td>
</tr>
<% } %>
</tbody>
</table>
</div>
</body>
</html>
@@ -0,0 +1,81 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/templates/documents/customDocument/customDocument.template.js
const { log } = require("../../../utils/consoleLog.utill");
module.exports = async (data) => {
log("📋 Preparing Custom Document data for PDF...");
let payload = data;
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch (error) {
payload = {};
}
}
const source = payload.documentData || payload.document_data || payload.data || payload;
const pick = (...keys) => {
for (const key of keys) {
const value = source?.[key];
if (value !== undefined && value !== null && value !== "") {
return value;
}
}
return "";
};
const templateData = {
logoUrl:
pick("logoUrl", "logo_url") ||
"https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
title: pick("title") || "CUSTOM DOCUMENT",
documentNumber: pick("documentNumber", "document_number") || "",
date: pick("date") || new Date().toLocaleDateString(),
telePhone: pick("telePhone", "telephone", "phone", "tel") || "",
emailAddress: pick("emailAddress", "email", "e_mail") || "",
officeAddressLine1: pick("officeAddressLine1", "office_address_line1") || "",
officeAddressLine2: pick("officeAddressLine2", "office_address_line2") || "",
officeAddressLine3: pick("officeAddressLine3", "office_address_line3") || "",
directorOfCustoms: pick("directorOfCustoms", "director_of_customs") || "",
vehicleNo: pick("vehicleNo", "vehicle_no") || "",
permitNo: pick("permitNo", "permit_no") || "",
chiefSecurityOfficer: pick("chiefSecurityOfficer", "chief_security_officer") || "",
exportGate: pick("exportGate", "export_gate") || "",
slpa: pick("slpa") || "",
gateNo: pick("gateNo", "gate_no") || "",
permissionText: pick("permissionText", "permission_text") || "Please grant permission to pass these Customs entry papers to supply goods to the vessel",
companyName: pick("companyName", "company_name") || "Oceanic Maritime Solutions (Pvt) Ltd",
subtitle: pick("subtitle") || "LICENSED SHIPCHANDLERS",
sectionCode: pick("sectionCode", "section_code") || "SC/FORM/06",
dateLabel: pick("dateLabel", "date_label") || "DATE :",
items: Array.isArray(source.items) ? source.items : [],
itemHeaders:
pick("itemHeaders", "item_headers") ||
["NO", "QTY", "UNIT", "DESCRIPTION", "RATE", "TOTAL", "Confirm Order Rate"],
footerText: pick("footerText", "footer_text") || "",
};
log("✅ Custom Document data prepared");
return templateData;
};
@@ -0,0 +1,189 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
box-sizing: border-box;
}
@page {
margin: 0;
}
body {
margin: 0;
padding: 14px;
font-family: Arial, Helvetica, sans-serif;
background: #fff;
}
.sheet {
width: 100%;
border: 2px solid #2a5aa8;
background: #fff;
}
.logo-row {
height: 82px;
border-bottom: 1px solid #4c4c4c;
display: flex;
align-items: center;
justify-content: center;
padding: 6px 12px 2px;
}
.logo-row img {
max-height: 64px;
object-fit: contain;
}
.title-row {
border-bottom: 1px solid #4c4c4c;
text-align: center;
padding: 6px 0 7px;
color: #2f5fa6;
font-size: 26px;
font-weight: 700;
letter-spacing: 0.5px;
}
.details {
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: 0;
min-height: 84px;
border-bottom: 1px solid #bdbdbd;
}
.left-pane,
.right-pane {
padding: 10px 8px 8px;
}
.left-pane {
border-right: 1px solid #d4d4d4;
}
.label-line {
font-size: 11px;
line-height: 1.35;
font-weight: 700;
text-transform: uppercase;
color: #111;
}
.label-line + .label-line {
margin-top: 4px;
}
.right-pane {
display: flex;
flex-direction: column;
justify-content: center;
gap: 8px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
}
.right-row {
display: flex;
justify-content: flex-start;
gap: 6px;
}
.right-row .value {
font-weight: 400;
text-transform: none;
}
.items-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.items-table th,
.items-table td {
border: 1px solid #5d5d5d;
font-size: 11px;
padding: 5px 6px;
vertical-align: top;
}
.items-table th {
background: #a7d8df;
color: #111;
text-transform: uppercase;
text-align: center;
font-weight: 700;
}
.no-col { width: 8%; }
.desc-col { width: 36%; }
.remarks-col { width: 25%; }
.unit-col { width: 14%; }
.qty-col { width: 17%; }
.items-table tbody td {
height: 16px;
}
.center {
text-align: center;
}
.right {
text-align: right;
}
</style>
</head>
<body>
<div class="sheet">
<div class="logo-row">
<% if (logoBase64) { %>
<img src="<%= logoBase64 %>" alt="Company Logo" />
<% } %>
</div>
<div class="title-row"><%= title || 'DELIVERY NOTE' %></div>
<div class="details">
<div class="left-pane">
<div class="label-line">Bill To: <span style="font-weight:700;"><%= billToName || 'N/A' %></span></div>
<div class="label-line"><%= billToAddress || 'N/A' %></div>
</div>
<div class="right-pane">
<div class="right-row"><span>Supply Date:</span> <span class="value"><%= supplyDate || 'N/A' %></span></div>
<div class="right-row"><span>PO Number:</span> <span class="value"><%= poNumber || 'N/A' %></span></div>
</div>
</div>
<table class="items-table">
<thead>
<tr>
<th class="no-col">No.</th>
<th class="desc-col">Description</th>
<th class="remarks-col">Remarks</th>
<th class="unit-col">Unit</th>
<th class="qty-col">Quantity</th>
</tr>
</thead>
<tbody>
<% const rows = Math.max(10, (items && items.length) || 0); %>
<% for (let index = 0; index < rows; index++) { %>
<% const item = items && items[index] ? items[index] : {}; %>
<tr>
<td class="center"><%= index + 1 %></td>
<td><%= item.description || item.item_name || item.name || '' %></td>
<td><%= item.remarks || '' %></td>
<td class="center"><%= item.unit || '' %></td>
<td class="right"><%= item.quantity || item.qty || '' %></td>
</tr>
<% } %>
</tbody>
</table>
</div>
</body>
</html>
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/templates/documents/deliveryNote/deliveryNote.template.js
const { log } = require("../../../utils/consoleLog.utill");
module.exports = async (data) => {
log("📋 Preparing Delivery Note data for PDF...");
const templateData = {
logoUrl:
data.logoUrl ||
"https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
title: data.title || "DELIVERY NOTE",
referenceNumber:
data.referenceNumber || data.reference_no || data.deliveryNoteNumber || data.delivery_note_number || "N/A",
date: data.date || new Date().toLocaleDateString(),
billToName: data.billToName || data.bill_to_name || "N/A",
billToAddress: data.billToAddress || data.bill_to_address || "N/A",
supplyDate: data.supplyDate || data.supply_date || data.date || new Date().toLocaleDateString(),
poNumber: data.poNumber || data.po_number || "N/A",
items: Array.isArray(data.items) ? data.items : [],
};
log("✅ Delivery Note data prepared");
return templateData;
};
@@ -0,0 +1,345 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@page {
size: A4;
margin: 0;
}
body {
background: white;
font-family: Arial, Helvetica, sans-serif;
padding: 10px;
}
.sheet {
width: 100%;
margin: 0 auto;
background: white;
border: 1px solid #222;
}
.title-bar {
background: #173d64;
color: #fff;
text-align: center;
font-size: 22px;
font-weight: 700;
letter-spacing: 1px;
padding: 4px 0 5px;
border-bottom: 1px solid #222;
}
.top-area {
display: grid;
grid-template-columns: 1.4fr 1fr 0.65fr;
min-height: 95px;
border-bottom: 1px solid #444;
}
.top-col {
padding: 5px 6px;
border-right: 1px solid #444;
font-size: 10px;
}
.top-col:last-child {
border-right: none;
}
.field-grid {
display: flex;
flex-direction: column;
gap: 1px;
}
.field-row {
display: grid;
grid-template-columns: 110px 1fr;
align-items: center;
min-height: 17px;
border: 1px solid #444;
border-bottom: none;
}
.field-row:last-child {
border-bottom: 1px solid #444;
}
.label {
font-weight: 700;
padding: 1px 4px;
}
.value {
padding: 1px 4px;
}
.top-left {
display: flex;
flex-direction: column;
gap: 0;
}
.top-middle {
display: flex;
flex-direction: column;
gap: 0;
}
.top-right {
display: flex;
flex-direction: column;
gap: 0;
}
.meta-grid {
display: grid;
grid-template-columns: 1.4fr 1fr 0.65fr;
border-bottom: 1px solid #444;
}
.meta-box {
min-height: 82px;
padding: 4px 6px;
border-right: 1px solid #444;
font-size: 10px;
}
.meta-box:last-child {
border-right: none;
}
.meta-table {
width: 100%;
border-collapse: collapse;
}
.meta-table td {
border: 1px solid #444;
padding: 2px 4px;
font-size: 10px;
}
.meta-table td.label-cell {
width: 105px;
font-weight: 700;
}
.details-note {
font-size: 9px;
margin-top: 2px;
}
.items-title {
font-size: 12px;
font-weight: 700;
text-align: center;
padding: 2px 0 3px;
border-bottom: 1px solid #444;
background: #efefef;
}
.items-wrap {
overflow-x: auto;
}
.items-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.items-table th,
.items-table td {
border: 1px solid #666;
font-size: 10px;
padding: 2px 3px;
vertical-align: middle;
}
.items-table th {
background: #d9d9d9;
font-weight: 700;
text-align: center;
}
.no-col { width: 5%; }
.product-col { width: 24%; }
.qty-col { width: 5%; }
.unit-col { width: 5%; }
.supp-col { width: 6%; }
.pono-col { width: 6%; }
.rcvdqty-col { width: 7%; }
.issuedqty-col { width: 7%; }
.expdate-col { width: 8%; }
.rcvdtime-col { width: 7%; }
.date-col { width: 7%; }
.rejects-col { width: 6%; }
.fatcol-col { width: 6%; }
.remark-col { width: 7%; }
.center {
text-align: center;
}
.right {
text-align: right;
}
.spacer-row td {
height: 21px;
}
.signature-section {
margin-top: 20px;
display: flex;
justify-content: space-between;
gap: 20px;
}
.signature-block {
flex: 1;
text-align: center;
border-top: 1px solid #333;
padding-top: 34px;
font-size: 11px;
}
.footer-note {
text-align: center;
margin-top: 12px;
border-top: 1px solid #ddd;
padding-top: 5px;
}
</style>
</head>
<body>
<%
const dispatchTitle = typeof title !== 'undefined' && title ? title : 'DISPATCH NOTE';
const dispatchReferenceNumber = typeof referenceNumber !== 'undefined' && referenceNumber ? referenceNumber : 'N/A';
const dispatchDate = typeof date !== 'undefined' && date ? date : 'N/A';
const dispatchVessel = typeof vessel !== 'undefined' && vessel ? vessel : 'N/A';
const dispatchCaptain = typeof captain !== 'undefined' && captain ? captain : 'N/A';
const dispatchCook = typeof cook !== 'undefined' && cook ? cook : 'N/A';
const dispatchAgent = typeof agent !== 'undefined' && agent ? agent : 'N/A';
const dispatchDetails = typeof details !== 'undefined' && details ? details : '';
const dispatchPlaceOfDelivery = typeof placeOfDelivery !== 'undefined' && placeOfDelivery ? placeOfDelivery : 'N/A';
const dispatchEta = typeof eta !== 'undefined' && eta ? eta : 'N/A';
const dispatchEtd = typeof etd !== 'undefined' && etd ? etd : 'N/A';
const dispatchNumberOfCrew = typeof numberOfCrew !== 'undefined' && numberOfCrew ? numberOfCrew : 'N/A';
const dispatchNextMainOrderIn = typeof nextMainOrderIn !== 'undefined' && nextMainOrderIn ? nextMainOrderIn : 'N/A';
const dispatchPort = typeof port !== 'undefined' && port ? port : 'N/A';
const dispatchCompany = typeof company !== 'undefined' && company ? company : 'N/A';
const dispatchFileNo = typeof fileNo !== 'undefined' && fileNo ? fileNo : 'N/A';
const dispatchItems = typeof items !== 'undefined' && Array.isArray(items) ? items : [];
%>
<div class="sheet">
<div class="title-bar"><%= dispatchTitle %></div>
<div class="top-area">
<div class="top-col">
<div class="field-grid">
<div class="field-row"><div class="label">Vessel:</div><div class="value"><%= dispatchVessel %></div></div>
<div class="field-row"><div class="label">Captain:</div><div class="value"><%= dispatchCaptain %></div></div>
<div class="field-row"><div class="label">Cook:</div><div class="value"><%= dispatchCook %></div></div>
<div class="field-row"><div class="label">Agent:</div><div class="value"><%= dispatchAgent %></div></div>
<div class="field-row"><div class="label">Details:</div><div class="value"><%= dispatchDetails %></div></div>
</div>
<div class="details-note">SC/FORM/04</div>
</div>
<div class="top-col">
<div class="field-grid">
<div class="field-row"><div class="label">Place of delivery:</div><div class="value"><%= dispatchPlaceOfDelivery %></div></div>
<div class="field-row"><div class="label">ETA:</div><div class="value"><%= dispatchEta %></div></div>
<div class="field-row"><div class="label">ETD:</div><div class="value"><%= dispatchEtd %></div></div>
<div class="field-row"><div class="label">Number of crew:</div><div class="value"><%= dispatchNumberOfCrew %></div></div>
<div class="field-row"><div class="label">Next main order in:</div><div class="value"><%= dispatchNextMainOrderIn %></div></div>
</div>
</div>
<div class="top-col">
<div class="field-grid">
<div class="field-row"><div class="label">Port</div><div class="value"><%= dispatchPort %></div></div>
<div class="field-row"><div class="label">Date</div><div class="value"><%= dispatchDate %></div></div>
<div class="field-row"><div class="label">Company</div><div class="value"><%= dispatchCompany %></div></div>
<div class="field-row"><div class="label">File No</div><div class="value"><%= dispatchFileNo %></div></div>
</div>
</div>
</div>
<div class="items-title">Item Receiving Authorization and Vessel Delivery Oder details</div>
<div class="items-wrap">
<table class="items-table">
<thead>
<tr>
<th class="no-col">No</th>
<th class="product-col">Product</th>
<th class="qty-col">Qty</th>
<th class="unit-col">Unit</th>
<th class="supp-col">SUPP</th>
<th class="pono-col">PO NO</th>
<th class="rcvdqty-col">Rcvd Qty</th>
<th class="issuedqty-col">Issued Qty</th>
<th class="expdate-col">Exp Date</th>
<th class="rcvdtime-col">Rcvd Time</th>
<th class="date-col">Date</th>
<th class="rejects-col">Rejects</th>
<th class="fatcol-col">Fat Con.</th>
<th class="remark-col">Remark</th>
</tr>
</thead>
<tbody>
<% const rows = Math.max(5, dispatchItems.length); %>
<% for (let index = 0; index < rows; index++) { %>
<% const item = dispatchItems[index] || {}; %>
<tr class="spacer-row">
<td class="center"><%= index + 1 %></td>
<td><%= item.product || item.description || item.item_name || item.name || '' %></td>
<td class="center"><%= item.qty || item.quantity || '' %></td>
<td class="center"><%= item.unit || '' %></td>
<td class="center"><%= item.supp || item.supplier || '' %></td>
<td class="center"><%= item.poNo || item.po_number || item.poNumber || '' %></td>
<td class="center"><%= item.receivedQty || item.rcvdQty || '' %></td>
<td class="center"><%= item.issuedQty || '' %></td>
<td class="center"><%= item.expDate || item.exp_date || '' %></td>
<td class="center"><%= item.receivedTime || item.rcvdTime || '' %></td>
<td class="center"><%= item.date || '' %></td>
<td class="center"><%= item.rejects || '' %></td>
<td class="center"><%= item.fatCon || item.fat_con || '' %></td>
<td><%= item.remark || item.remarks || '' %></td>
</tr>
<% } %>
</tbody>
</table>
</div>
<% if (showSignatures) { %>
<div class="signature-section">
<div class="signature-block">
<strong>Prepared By</strong>
<div style="margin-top: 28px;">___________________</div>
<div style="margin-top: 5px;"><%= preparedBy || 'Name' %></div>
</div>
<div class="signature-block">
<strong>Approved By</strong>
<div style="margin-top: 28px;">___________________</div>
<div style="margin-top: 5px;"><%= approvedBy || 'Name' %></div>
</div>
</div>
<% } %>
<div class="footer-note">
<p style="font-size: 10px; color: #999;">This is an official dispatch note</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,74 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/templates/documents/dispatchNote/dispatchNote.template.js
const { log } = require("../../../utils/consoleLog.utill");
module.exports = async (data) => {
log("📋 Preparing Dispatch Note data for PDF...");
let payload = data;
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch (error) {
payload = {};
}
}
const source = payload.documentData || payload.document_data || payload.data || payload;
const pick = (...keys) => {
for (const key of keys) {
const value = source?.[key];
if (value !== undefined && value !== null && value !== "") {
return value;
}
}
return "";
};
const templateData = {
logoUrl:
pick("logoUrl", "logo_url") ||
"https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
title: pick("title") || "DISPATCH NOTE",
referenceNumber: pick("referenceNumber", "reference_number", "dnNo", "dn_no") || "N/A",
date: pick("date") || new Date().toLocaleDateString(),
vessel: pick("vessel", "vesselName", "vessel_name") || "",
captain: pick("captain", "captainName", "captain_name") || "",
cook: pick("cook", "cookName", "cook_name") || "",
agent: pick("agent", "agentName", "agent_name") || "",
details: pick("details", "remarks", "note") || "",
placeOfDelivery: pick("placeOfDelivery", "place_of_delivery") || "",
eta: pick("eta") || "",
etd: pick("etd") || "",
numberOfCrew: pick("numberOfCrew", "number_of_crew", "crewCount", "crew_count") || "",
nextMainOrderIn: pick("nextMainOrderIn", "next_main_order_in") || "",
port: pick("port", "portName", "port_name") || "",
company: pick("company", "companyName", "company_name") || "",
fileNo: pick("fileNo", "file_no") || "",
items: Array.isArray(source.items) ? source.items : [],
showSignatures: pick("showSignatures") !== false,
preparedBy: pick("preparedBy", "prepared_by") || "",
approvedBy: pick("approvedBy", "approved_by") || "",
};
log("✅ Dispatch Note data prepared");
return templateData;
};
+308
View File
@@ -0,0 +1,308 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@page {
size: A4;
margin: 0;
}
body {
background: white;
font-family: Arial, Helvetica, sans-serif;
padding: 12px;
}
.page {
width: 100%;
margin: 0 auto;
background: white;
border: 1px solid #333;
}
.logo-section {
height: 72px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #c9c9c9;
}
.logo-section img {
max-height: 52px;
object-fit: contain;
}
.title-section {
text-align: center;
padding: 5px 0 4px;
border-bottom: 1px solid #333;
font-size: 22px;
font-weight: 700;
color: #2d5c96;
letter-spacing: 0.4px;
}
.top-grid {
display: grid;
grid-template-columns: 1.6fr 1fr;
min-height: 70px;
border-bottom: 1px solid #c9c9c9;
}
.top-left,
.top-right,
.mid-left,
.mid-right {
padding: 4px 6px;
font-size: 10px;
color: #111;
}
.top-left {
border-right: 1px solid #e2e2e2;
display: flex;
align-items: flex-start;
}
.bill-to-label {
font-weight: 700;
min-width: 48px;
}
.bill-to-name {
font-weight: 700;
}
.bill-to-address {
margin-top: 2px;
line-height: 1.25;
}
.top-right {
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 2px;
text-align: right;
font-weight: 700;
}
.top-right-row {
display: flex;
justify-content: flex-end;
gap: 6px;
}
.top-right-row .value {
font-weight: 400;
min-width: 110px;
text-align: left;
}
.info-grid {
display: grid;
grid-template-columns: 1.4fr 1fr;
border-bottom: 1px solid #c9c9c9;
}
.mid-left {
border-right: 1px solid #e2e2e2;
}
.mid-row {
display: flex;
justify-content: space-between;
gap: 10px;
margin-bottom: 2px;
}
.info-label {
font-weight: 700;
}
.info-value {
text-align: right;
min-width: 90px;
}
.items-wrap {
overflow-x: auto;
}
.items-table {
width: 100%;
border-collapse: collapse;
}
.items-table thead th {
background: #a8d9de;
color: #111;
font-weight: bold;
font-size: 10px;
text-align: center;
padding: 5px 4px;
border: 1px solid #4b4b4b;
}
.items-table td {
border: 1px solid #5d5d5d;
font-size: 10px;
height: 15px;
padding: 2px 4px;
vertical-align: middle;
}
.center {
text-align: center;
}
.right {
text-align: right;
}
.col-no { width: 8%; }
.col-desc { width: 30%; }
.col-remarks { width: 24%; }
.col-qty { width: 9%; }
.col-unit { width: 9%; }
.col-price { width: 10%; }
.col-total { width: 10%; }
.spacer-row td {
height: 18px;
}
.bottom-table {
display: grid;
grid-template-columns: 1.4fr 1fr;
border-top: 1px solid #c9c9c9;
}
.bottom-left,
.bottom-right {
padding: 4px 6px;
font-size: 10px;
}
.bottom-left {
border-right: 1px solid #e2e2e2;
}
.bottom-row {
display: flex;
flex-direction: column;
gap: 2px;
}
.bottom-line {
display: flex;
justify-content: space-between;
gap: 8px;
}
.footer-row {
border-top: 1px solid #333;
text-align: right;
padding: 4px 6px;
font-size: 9px;
color: #555;
}
</style>
</head>
<body>
<%
const invoiceTitle = typeof title !== 'undefined' && title ? title : 'INVOICE';
const invoiceJobReference = typeof jobReference !== 'undefined' && jobReference ? jobReference : 'N/A';
const invoicePoNumber = typeof poNumber !== 'undefined' && poNumber ? poNumber : 'N/A';
const invoiceDate = typeof date !== 'undefined' && date ? date : 'N/A';
const invoicePageLabel = typeof pageLabel !== 'undefined' && pageLabel ? pageLabel : 'Page 01 of 02';
const invoiceBillToName = typeof billToName !== 'undefined' && billToName ? billToName : 'Client Name';
const invoiceBillToAddress = typeof billToAddress !== 'undefined' && billToAddress ? billToAddress : 'Address';
const invoiceVesselName = typeof vesselName !== 'undefined' && vesselName ? vesselName : 'N/A';
const invoicePurposeOfCall = typeof purposeOfCall !== 'undefined' && purposeOfCall ? purposeOfCall : 'N/A';
const invoiceSupplyDate = typeof supplyDate !== 'undefined' && supplyDate ? supplyDate : 'N/A';
const invoiceGrt = typeof grt !== 'undefined' && grt ? grt : 'N/A';
const invoiceImoNumber = typeof imoNumber !== 'undefined' && imoNumber ? imoNumber : 'N/A';
const invoicePortCountry = typeof portCountry !== 'undefined' && portCountry ? portCountry : 'N/A';
const invoiceItems = typeof items !== 'undefined' && Array.isArray(items) ? items : [];
%>
<div class="page">
<div class="logo-section">
<% if (logoBase64) { %>
<img src="<%= logoBase64 %>" alt="Company Logo" />
<% } %>
</div>
<div class="title-section"><%= invoiceTitle %></div>
<div class="top-grid">
<div class="top-left">
<div>
<div><span class="bill-to-label">Bill To:</span> <span class="bill-to-name"><%= invoiceBillToName %></span></div>
<div class="bill-to-address"><%= invoiceBillToAddress %></div>
</div>
</div>
<div class="top-right">
<div class="top-right-row"><span>Job Reference :</span> <span class="value"><%= invoiceJobReference %></span></div>
<div class="top-right-row"><span>PO Number :</span> <span class="value"><%= invoicePoNumber %></span></div>
<div class="top-right-row"><span>Date :</span> <span class="value"><%= invoiceDate %></span></div>
<div class="top-right-row"><span></span> <span class="value"><%= invoicePageLabel %></span></div>
</div>
</div>
<div class="info-grid">
<div class="mid-left">
<div class="mid-row"><span class="info-label">Vessel Name :</span> <span class="info-value"><%= invoiceVesselName %></span></div>
<div class="mid-row"><span class="info-label">Purpose of Call :</span> <span class="info-value"><%= invoicePurposeOfCall %></span></div>
<div class="mid-row"><span class="info-label">Supply Date :</span> <span class="info-value"><%= invoiceSupplyDate %></span></div>
</div>
<div class="mid-right">
<div class="mid-row"><span class="info-label">GRT :</span> <span class="info-value"><%= invoiceGrt %></span></div>
<div class="mid-row"><span class="info-label">IMO Number :</span> <span class="info-value"><%= invoiceImoNumber %></span></div>
<div class="mid-row"><span class="info-label">Port/Country :</span> <span class="info-value"><%= invoicePortCountry %></span></div>
</div>
</div>
<div class="items-wrap">
<table class="items-table">
<thead>
<tr>
<th class="col-no">No.</th>
<th class="col-desc">Description</th>
<th class="col-remarks">Remarks</th>
<th class="col-qty">Qty</th>
<th class="col-unit">Unit</th>
<th class="col-price">Unit Price (USD)</th>
<th class="col-total">Final Amount (USD)</th>
</tr>
</thead>
<tbody>
<% const rows = Math.max(10, invoiceItems.length); %>
<% for (let index = 0; index < rows; index++) { %>
<% const item = invoiceItems[index] || {}; %>
<% const quantity = Number(item.quantity || item.qty || 0); %>
<% const unitPrice = Number(item.unit_price || item.unitPrice || 0); %>
<tr class="spacer-row">
<td class="center"><%= index + 1 %></td>
<td><%= item.description || item.item_name || item.name || '' %></td>
<td><%= item.remarks || '' %></td>
<td class="center"><%= item.quantity || item.qty || '' %></td>
<td class="center"><%= item.unit || '' %></td>
<td class="right"><%= item.unit_price || item.unitPrice || '' %></td>
<td class="right"><%= quantity && unitPrice ? quantity * unitPrice : (item.total || item.amount || '') %></td>
</tr>
<% } %>
</tbody>
</table>
</div>
</div>
</body>
</html>
@@ -0,0 +1,111 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/templates/documents/invoice/invoice.template.js
const { log } = require("../../../utils/consoleLog.utill");
module.exports = async (data) => {
log("📋 Preparing Invoice data for PDF...");
let payload = data;
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch (error) {
payload = {};
}
}
const source =
payload.documentData ||
payload.document_data ||
payload.data ||
payload;
const deepFind = (node, keys, visited = new Set()) => {
if (!node || typeof node !== "object" || visited.has(node)) {
return "";
}
visited.add(node);
for (const key of keys) {
const value = node[key];
if (value !== undefined && value !== null && value !== "") {
return value;
}
}
for (const child of Object.values(node)) {
const found = deepFind(child, keys, visited);
if (found !== "") {
return found;
}
}
return "";
};
const pick = (...keys) => {
const direct = deepFind(source, keys);
if (direct !== "") {
return direct;
}
return deepFind(payload, keys);
};
const templateData = {
logoUrl:
pick("logoUrl", "logo_url") ||
"https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
title: pick("title") || "INVOICE",
jobReference:
pick(
"jobReference",
"job_reference",
"referenceNumber",
"reference_no",
"jobRef",
"job_ref",
) || "",
poNumber: pick("poNumber", "po_number", "poNo", "po_no") || "",
date: pick("date") || new Date().toLocaleDateString(),
pageLabel: pick("pageLabel", "page_label") || "Page 01 of 02",
billToName: pick("billToName", "bill_to_name") || "Client Name",
billToAddress: pick("billToAddress", "bill_to_address") || "Address",
vesselName: pick("vesselName", "vessel_name") || "",
purposeOfCall: pick("purposeOfCall", "purpose_of_call") || "",
supplyDate: pick("supplyDate", "supply_date") || "",
grt: pick("grt", "GRT") || "",
imoNumber: pick("imoNumber", "imo_number", "imo") || "",
portCountry: pick("portCountry", "port_country") || "",
items: Array.isArray(source.items) ? source.items : [],
subtotal: Number(pick("subtotal", "sub_total") || 0),
tax: Number(pick("tax") || 0),
discount: Number(pick("discount") || 0),
total: Number(pick("total") || 0),
paymentTerms: pick("paymentTerms", "payment_terms") || "",
notes: pick("notes") || "",
};
log("✅ Invoice data prepared");
return templateData;
};
+23
View File
@@ -0,0 +1,23 @@
body {
font-family: Arial;
padding: 20px;
}
h1 {
text-align: center;
}
table {
width: 100%;
border-collapse: collapse;
}
td,
th {
border: 1px solid #ddd;
padding: 8px;
}
th {
background: #f4f4f4;
}
@@ -0,0 +1,24 @@
<html>
<head>
<style>
{{styles}}
</style>
</head>
<body>
<h1>PreCost Estimate</h1>
<p>Customer: {{customerName}}</p>
<table>
<tr>
<th>Item</th>
<th>Description</th>
<th>Qty</th>
<th>Unit Price</th>
<th>Total</th>
</tr>
{{items}}
</table>
</body>
</html>
+285
View File
@@ -0,0 +1,285 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: white;
font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
background: white;
}
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
border-bottom: 2px solid #1a7a4d;
padding-bottom: 15px;
}
.header-section img {
height: 80px;
}
.doc-title {
font-size: 28px;
font-weight: bold;
color: #1a7a4d;
text-align: center;
flex: 1;
}
.po-number {
text-align: right;
color: #666;
font-size: 12px;
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 30px;
background: #e8f5e9;
padding: 15px;
border-radius: 4px;
}
.info-left, .info-right {
display: flex;
flex-direction: column;
gap: 8px;
}
.info-row {
display: flex;
justify-content: space-between;
}
.info-label {
font-weight: bold;
color: #2e7d32;
min-width: 150px;
}
.info-value {
color: #333;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.items-table thead {
background-color: #2e7d32;
color: white;
}
.items-table th {
padding: 12px;
text-align: left;
font-weight: bold;
font-size: 12px;
}
.items-table td {
padding: 12px;
border-bottom: 1px solid #ddd;
}
.items-table tbody tr:nth-child(even) {
background-color: #f5f5f5;
}
.summary-area {
display: flex;
justify-content: flex-end;
margin-top: 30px;
margin-bottom: 30px;
}
.summary-table {
width: 400px;
}
.summary-table td {
padding: 10px;
border: 1px solid #ddd;
}
.summary-table td:first-child {
font-weight: bold;
background-color: #f5f5f5;
width: 60%;
}
.summary-table td:last-child {
text-align: right;
}
.total-row {
background-color: #2e7d32;
color: white;
font-weight: bold;
font-size: 14px;
}
.terms-section {
margin-top: 30px;
border-top: 1px solid #ddd;
padding-top: 15px;
}
.terms-section h4 {
color: #2e7d32;
font-size: 12px;
margin-bottom: 8px;
}
.terms-section p {
font-size: 11px;
color: #666;
line-height: 1.5;
}
.footer-note {
text-align: center;
margin-top: 40px;
border-top: 2px solid #ddd;
padding-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<!-- HEADER SECTION -->
<div class="header-section">
<div>
<% if (logoBase64) { %>
<img src="<%= logoBase64 %>" alt="Company Logo" />
<% } %>
</div>
<div style="flex: 1; text-align: center;">
<div class="doc-title">PURCHASE ORDER</div>
</div>
<div class="po-number">
<strong>PO #:</strong> <%= poNumber || 'N/A' %><br/>
<strong>Date:</strong> <%= date || 'N/A' %>
</div>
</div>
<!-- INFO GRID -->
<div class="info-grid">
<div class="info-left">
<div class="info-row">
<span class="info-label">VENDOR:</span>
</div>
<div style="margin-left: 0;">
<strong><%= vendorName || 'Vendor Name' %></strong><br/>
<%= vendorAddress || 'Address' %>
</div>
</div>
<div class="info-right">
<div class="info-row">
<span class="info-label">DELIVERY DATE:</span>
<span class="info-value"><%= deliveryDate || 'N/A' %></span>
</div>
<div class="info-row">
<span class="info-label">PAYMENT TERMS:</span>
<span class="info-value"><%= paymentTerms || 'Net 30' %></span>
</div>
<div class="info-row">
<span class="info-label">VESSEL:</span>
<span class="info-value"><%= vesselName || 'N/A' %></span>
</div>
</div>
</div>
<!-- ITEMS TABLE -->
<div style="overflow-x: auto;">
<table class="items-table">
<thead>
<tr>
<th>ITEM NO</th>
<th>DESCRIPTION</th>
<th>QTY</th>
<th>UNIT</th>
<th>UNIT PRICE</th>
<th>TOTAL</th>
</tr>
</thead>
<tbody>
<% if (items && items.length > 0) { %>
<% items.forEach((item, index) => { %>
<tr>
<td><%= index + 1 %></td>
<td><%= item.description || item.item_name || '' %></td>
<td><%= item.quantity || 0 %></td>
<td><%= item.unit || '' %></td>
<td><%= item.unit_price || 0 %></td>
<td><%= (item.quantity || 0) * (item.unit_price || 0) %></td>
</tr>
<% }); %>
<% } %>
</tbody>
</table>
</div>
<!-- SUMMARY SECTION -->
<div class="summary-area">
<table class="summary-table">
<tr>
<td>SUB TOTAL</td>
<td><%= subtotal || 0 %></td>
</tr>
<tr>
<td>TAX (%)</td>
<td><%= tax || 0 %></td>
</tr>
<tr>
<td>SHIPPING</td>
<td><%= shipping || 0 %></td>
</tr>
<tr class="total-row">
<td>TOTAL</td>
<td><%= total || 0 %></td>
</tr>
</table>
</div>
<!-- TERMS SECTION -->
<% if (paymentTermsDetails || notes) { %>
<div class="terms-section">
<% if (paymentTermsDetails) { %>
<h4>TERMS & CONDITIONS</h4>
<p><%= paymentTermsDetails %></p>
<% } %>
<% if (notes) { %>
<h4>SPECIAL INSTRUCTIONS</h4>
<p><%= notes %></p>
<% } %>
</div>
<% } %>
<!-- FOOTER -->
<div class="footer-note">
<p style="font-size: 10px; color: #999;">Please confirm receipt and delivery dates</p>
</div>
</div>
</body>
</html>
+44
View File
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/templates/documents/po/po.template.js
const { log } = require("../../../utils/consoleLog.utill");
module.exports = async (data) => {
log("📋 Preparing Purchase Order data for PDF...");
const templateData = {
logoUrl: "https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
poNumber: data.poNumber || data.po_number || "PO-001",
date: data.date || new Date().toLocaleDateString(),
vendorName: data.vendorName || data.vendor_name || "Vendor Name",
vendorAddress: data.vendorAddress || data.vendor_address || "Address",
deliveryDate: data.deliveryDate || data.delivery_date || "N/A",
paymentTerms: data.paymentTerms || data.payment_terms || "Net 30",
vesselName: data.vesselName || data.vessel_name || "N/A",
items: data.items || [],
subtotal: data.subtotal || data.sub_total || 0,
tax: data.tax || 0,
shipping: data.shipping || 0,
total: data.total || 0,
paymentTermsDetails: data.paymentTermsDetails || data.payment_terms_details || "",
notes: data.notes || "",
};
log("✅ Purchase Order data prepared");
return templateData;
};
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2026 Niolla
* All rights reserved.
*
* This source code is proprietary and confidential.
* Unauthorized copying, modification, distribution, or use
* of this file, via any medium, is strictly prohibited.
*/
// app/templates/documents/precost/preCost.template.js
const { log } = require("../../../utils/consoleLog.utill");
module.exports = async (data) => {
log("📋 Preparing PreCost data for PDF...");
log("📥 Incoming data:", JSON.stringify(data, null, 2));
const templateData = {
logoUrl:
"https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/company_logo_transperent_bg_uullzw.png",
vesselName: data.vessel_name || "MV Vessel",
supplierName: data.supplyPort || "N/A",
clientRfqNum: data.clientRfqNum || "N/A",
omsRfqNum: data.omsRfqNum || "N/A",
date: data.date || new Date().toLocaleDateString(),
items: data.items || [],
subtotal: data.sub_total || 0,
discount: data.discount || 0,
additionalCharges: data.additionalCharges || 0,
fullTotal: data.total_cost || data.total_cost_usd || (data.sub_total - data.discount + data.additionalCharges) || 0,
};
log("Data:", data);
log("fullTotal calculation:", {
total_cost: data.total_cost,
total_cost_usd: data.total_cost_usd,
calculated: data.sub_total - data.discount + data.additionalCharges,
final: templateData.fullTotal,
});
log("✅ PreCost data prepared:", JSON.stringify(templateData, null, 2));
return templateData;
};
+300
View File
@@ -0,0 +1,300 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: white;
font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
background: white;
}
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
border-bottom: 2px solid #1a7a4d;
padding-bottom: 15px;
}
.header-section img {
height: 80px;
}
.po-title {
font-size: 28px;
font-weight: bold;
color: #1a7a4d;
text-align: center;
flex: 1;
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 30px;
background: #e8f5e9;
padding: 15px;
border-radius: 4px;
}
.info-left,
.info-right {
display: flex;
flex-direction: column;
gap: 8px;
}
.info-row {
display: flex;
justify-content: space-between;
}
.info-label {
font-weight: bold;
color: #2e7d32;
min-width: 150px;
}
.info-value {
color: #333;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.items-table thead {
background-color: #2e7d32;
color: white;
}
.items-table th {
padding: 12px;
text-align: left;
font-weight: bold;
font-size: 12px;
}
.items-table td {
padding: 12px;
border-bottom: 1px solid #ddd;
}
.items-table tbody tr:nth-child(even) {
background-color: #f5f5f5;
}
.summary-area {
display: flex;
justify-content: flex-end;
margin-top: 30px;
margin-bottom: 30px;
}
.summary-table {
width: 400px;
}
.summary-table td {
padding: 10px;
border: 1px solid #ddd;
}
.summary-table td:first-child {
font-weight: bold;
background-color: #f5f5f5;
width: 60%;
}
.summary-table td:last-child {
text-align: right;
}
.total-row {
background-color: #2e7d32;
color: white;
font-weight: bold;
font-size: 14px;
}
.total-row td {
border-color: #1a7a4d;
}
.footer-note {
text-align: center;
margin-top: 40px;
border-top: 2px solid #ddd;
padding-top: 20px;
}
.footer-note img {
max-width: 100%;
height: auto;
}
.text-black{
color: black !important;
}
</style>
</head>
<body>
<div class="container">
<!-- HEADER SECTION -->
<div class="header-section">
<div>
<% if (logoBase64) { %>
<img src="<%= logoBase64 %>" alt="Company Logo" />
<% } %>
</div>
<div class="po-title">PRICE QUOTATION</div>
</div>
<!-- INFO GRID -->
<div class="info-grid">
<div class="info-left">
<div class="info-row">
<span class="info-label">VESSEL NAME :</span>
<span class="info-value">
<%= vesselName %>
</span>
</div>
<div class="info-row">
<span class="info-label">SUPPLY PORT :</span>
<span class="info-value">
<%= supplierName || 'Supplier Name' %>
</span>
</div>
</div>
<div class="info-right">
<div class="info-row">
<span class="info-label">CLIENT RFQ :</span>
<span class="info-value">
<%= clientRfqNum || 'RFQ-001' %>
</span>
</div>
<div class="info-row">
<span class="info-label">OMS RFQ :</span>
<span class="info-value">
<%= omsRfqNum || 'RFQ-001' %>
</span>
</div>
<div class="info-row">
<span class="info-label">DATE :</span>
<span class="info-value">
<%= date || 'Current Date' %>
</span>
</div>
</div>
</div>
<!-- ITEMS TABLE -->
<div style="overflow-x: auto;">
<table class="items-table">
<thead>
<tr>
<th>ITEM NO</th>
<th>DESCRIPTION</th>
<th>REMARKS</th>
<th>QTY</th>
<th>UNIT</th>
<th>UNIT PRICE</th>
<th>TOTAL PRICE</th>
</tr>
</thead>
<tbody>
<% if (items && items.length> 0) { %>
<% items.forEach((item, index)=> { %>
<tr>
<td>
<%= index + 1 %>
</td>
<td>
<%= item.item_name || '' %>
</td>
<td>
<%= item.customer_remark || '' %>
</td>
<td>
<%= item.quantity || 0 %>
</td>
<td>
<%= item.unit || '' %>
</td>
<td>
<%= item.unit_price || 0 %>
</td>
<td>
<%= (item.quantity || 0) * (item.unit_price || 0) %>
</td>
</tr>
<% }); %>
<% } %>
</tbody>
</table>
</div>
<!-- SUMMARY SECTION -->
<div class="summary-area">
<table class="summary-table">
<tr>
<td>SUB TOTAL</td>
<td>
<%= subtotal || 0 %>
</td>
</tr>
<% if (discount && Number(discount) !==0) { %>
<tr>
<td>DISCOUNT (%)</td>
<td>
<%= discount %>
</td>
</tr>
<% } %>
<% if (additionalCharges && Number(additionalCharges) !==0) { %>
<tr>
<td>ADDITIONAL CHARGES<br />(CUSTOM/HANDLING/TRANSPORT)</td>
<td>
<%= additionalCharges %>
</td>
</tr>
<% } %>
<tr class="total-row">
<td class="text-black" >GRAND TOTAL</td>
<td>
<%= fullTotal || 0 %>
</td>
</tr>
</table>
</div>
<!-- FOOTER -->
<div class="footer-note">
<img src="https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/footer_ax2jyz.png" alt="Footer">
</div>
</div>
</body>
</html>
+527
View File
@@ -0,0 +1,527 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: white;
display: flex;
justify-content: center;
align-items: center;
padding: 2rem 1rem;
font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
}
/* actual table style — clean, border-collapse */
.po-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
font-family: 'Segoe UI', system-ui, 'Calibri', sans-serif;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
}
/* header sections & company info area (using table-like but not inside main table) */
.header-section {
margin-bottom: 1.8rem;
text-align: center;
}
.po-title {
font-size: 2rem;
font-weight: 700;
letter-spacing: 1px;
color: #257339;
border-top: 2px solid #257339;
border-bottom: 2px solid #257339;
padding: 1rem;
margin-bottom: 1.5rem;
margin-top: 1.5rem;
font-family: 'Inter', 'Segoe UI', sans-serif;
}
/* two column info grid (company, supplier, dates etc) */
.info-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 1.2rem;
margin-bottom: 2rem;
background: #e1f3db;
padding: 1rem 1.2rem;
border: 1px solid #e2edf2;
}
.info-left {
flex: 2;
min-width: 240px;
}
.info-right {
flex: 1.2;
min-width: 200px;
}
.info-row {
display: flex;
align-items: baseline;
flex-wrap: wrap;
margin-bottom: 12px;
font-size: 0.9rem;
}
.info-label {
font-weight: 700;
width: 140px;
color: #257339;
letter-spacing: 0.3px;
}
.info-value {
font-weight: 500;
color: #1f2d3d;
border-bottom: 1px dashed #bdd3e6;
min-width: 200px;
padding: 0 0.2rem;
background: transparent;
}
.inline-placeholder {
color: #2c7da0;
font-weight: 500;
}
hr {
margin: 1rem 0;
}
/* main items table */
.items-table {
width: 100%;
border-collapse: collapse;
margin: 1.2rem 0 1rem;
font-size: 0.8rem;
}
.items-table th,
.items-table td {
border: 1px solid #cddfe7;
padding: 10px 6px;
vertical-align: top;
text-align: left;
}
.items-table th {
background-color: #257339;
color: white;
font-weight: 600;
font-size: 0.8rem;
letter-spacing: 0.5px;
text-align: center;
}
.items-table td {
background-color: #fff;
color: #1e2a3a;
}
.items-table input,
.items-table .pseudo-input {
width: 100%;
border: none;
background: transparent;
font-family: inherit;
font-size: 0.8rem;
padding: 4px 2px;
outline: none;
border-bottom: 1px dotted #acc8dd;
}
.items-table input:focus {
border-bottom: 1px solid #1f6e8c;
background: #fefce8;
}
/* numeric cells alignment */
.numeric-cell {
text-align: right;
}
.qty-cell input,
.price-cell input {
text-align: right;
}
.total-cell {
font-weight: 600;
background-color: #f8fafc;
text-align: right;
}
/* summary section (subtotal, discount, transport) */
.summary-area {
margin-top: 1.5rem;
display: flex;
flex-direction: column;
align-items: flex-end;
border-top: 2px solid #cbdde6;
padding-top: 1.2rem;
}
.summary-table {
width: 320px;
border-collapse: collapse;
font-size: 0.85rem;
}
.summary-table td {
padding: 6px 8px;
border: none;
}
.summary-table td:first-child {
font-weight: 700;
text-align: left;
}
.summary-table td:last-child {
text-align: right;
font-weight: 600;
}
.total-row {
border-top: 2px solid #2c5a74;
font-weight: 800;
font-size: 1rem;
}
.discount-input,
.transport-input {
border: 1px solid #b9d0df;
border-radius: 8px;
padding: 4px 10px;
width: 110px;
text-align: right;
font-weight: 500;
background: #fff;
}
.inline-action {
display: inline-flex;
align-items: center;
gap: 5px;
}
button.recalc {
background: #eef2f5;
border: 1px solid #b9cfdf;
border-radius: 30px;
padding: 4px 12px;
font-size: 0.7rem;
cursor: pointer;
transition: 0.1s;
margin-left: 10px;
font-weight: 500;
}
button.recalc:hover {
background: #cbdde6;
}
.footer-note {
margin-top: 2rem;
font-size: 0.7rem;
text-align: center;
color: #547b97;
border-top: 1px solid #d4e2ec;
padding-top: 1rem;
}
@media (max-width: 750px) {
.po-container {
padding: 1rem;
}
.info-grid {
flex-direction: column;
}
.summary-table {
width: 100%;
}
.items-table th,
.items-table td {
padding: 6px 3px;
font-size: 0.7rem;
}
}
.editable-highlight {
background-color: #fef9e3;
} * {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: white;
display: flex;
justify-content: center;
align-items: center;
padding: 2rem 1rem;
font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
}
/* actual table style — clean, border-collapse */
.po-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
font-family: 'Segoe UI', system-ui, 'Calibri', sans-serif;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
}
/* header sections & company info area (using table-like but not inside main table) */
.header-section {
margin-bottom: 1.8rem;
text-align: center;
}
.po-title {
font-size: 2rem;
font-weight: 700;
letter-spacing: 1px;
color: #257339;
border-top: 2px solid #257339;
border-bottom: 2px solid #257339;
padding: 1rem;
margin-bottom: 1.5rem;
margin-top: 1.5rem;
font-family: 'Inter', 'Segoe UI', sans-serif;
}
/* two column info grid (company, supplier, dates etc) */
.info-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 1.2rem;
margin-bottom: 2rem;
background: #e1f3db;
padding: 1rem 1.2rem;
border: 1px solid #e2edf2;
}
.info-left {
flex: 2;
min-width: 240px;
}
.info-right {
flex: 1.2;
min-width: 200px;
}
.info-row {
display: flex;
align-items: baseline;
flex-wrap: wrap;
margin-bottom: 12px;
font-size: 0.9rem;
}
.info-label {
font-weight: 700;
width: 140px;
color: #257339;
letter-spacing: 0.3px;
}
.info-value {
font-weight: 500;
color: #1f2d3d;
border-bottom: 1px dashed #bdd3e6;
min-width: 200px;
padding: 0 0.2rem;
background: transparent;
}
.inline-placeholder {
color: #2c7da0;
font-weight: 500;
}
hr {
margin: 1rem 0;
}
/* main items table */
.items-table {
width: 100%;
border-collapse: collapse;
margin: 1.2rem 0 1rem;
font-size: 0.8rem;
}
.items-table th,
.items-table td {
border: 1px solid #cddfe7;
padding: 10px 6px;
vertical-align: top;
text-align: left;
}
.items-table th {
background-color: #257339;
color: white;
font-weight: 600;
font-size: 0.8rem;
letter-spacing: 0.5px;
text-align: center;
}
.items-table td {
background-color: #fff;
color: #1e2a3a;
}
.items-table input,
.items-table .pseudo-input {
width: 100%;
border: none;
background: transparent;
font-family: inherit;
font-size: 0.8rem;
padding: 4px 2px;
outline: none;
border-bottom: 1px dotted #acc8dd;
}
.items-table input:focus {
border-bottom: 1px solid #1f6e8c;
background: #fefce8;
}
/* numeric cells alignment */
.numeric-cell {
text-align: right;
}
.qty-cell input,
.price-cell input {
text-align: right;
}
.total-cell {
font-weight: 600;
background-color: #f8fafc;
text-align: right;
}
/* summary section (subtotal, discount, transport) */
.summary-area {
margin-top: 1.5rem;
display: flex;
flex-direction: column;
align-items: flex-end;
border-top: 2px solid #cbdde6;
padding-top: 1.2rem;
}
.summary-table {
width: 320px;
border-collapse: collapse;
font-size: 0.85rem;
}
.summary-table td {
padding: 6px 8px;
border: none;
}
.summary-table td:first-child {
font-weight: 700;
text-align: left;
}
.summary-table td:last-child {
text-align: right;
font-weight: 600;
}
.total-row {
border-top: 2px solid #2c5a74;
font-weight: 800;
font-size: 1rem;
}
.discount-input,
.transport-input {
border: 1px solid #b9d0df;
border-radius: 8px;
padding: 4px 10px;
width: 110px;
text-align: right;
font-weight: 500;
background: #fff;
}
.inline-action {
display: inline-flex;
align-items: center;
gap: 5px;
}
button.recalc {
background: #eef2f5;
border: 1px solid #b9cfdf;
border-radius: 30px;
padding: 4px 12px;
font-size: 0.7rem;
cursor: pointer;
transition: 0.1s;
margin-left: 10px;
font-weight: 500;
}
button.recalc:hover {
background: #cbdde6;
}
.footer-note {
margin-top: 2rem;
font-size: 0.7rem;
text-align: center;
color: #547b97;
border-top: 1px solid #d4e2ec;
padding-top: 1rem;
}
@media (max-width: 750px) {
.po-container {
padding: 1rem;
}
.info-grid {
flex-direction: column;
}
.summary-table {
width: 100%;
}
.items-table th,
.items-table td {
padding: 6px 3px;
font-size: 0.7rem;
}
}
.editable-highlight {
background-color: #fef9e3;
}
@@ -0,0 +1,97 @@
<html>
<head>
<style>
{{styles}}
</style>
</head>
<body>
<div>
<!-- PURCHASE ORDER title -->
<div class="header-section">
<div>
<img src="{{logoBase64}}" alt="Company Logo" style="height:80px; margin-bottom:-8px;" />
</div>
<div class="po-title">PRICE QUOTATION</div>
</div>
<!-- Company & supplier info (dynamic / editable) -->
<div class="info-grid">
<div class="info-left">
<div class="info-row">
<span class="info-label">VESSEL NAME :</span>
<span class="info-value" id="companyName">Oceanic Maritime Solutions (Pvt) Ltd</span>
</div>
<div class="info-row">
<span class="info-label">SUPPLY PORT :</span>
<span class="info-value" id="supplierName">Supplier Name</span>
</div>
</div>
<div class="info-right">
<div class="info-row">
<span class="info-label">CLIENT RFQ :</span>
<span class="info-value" id="rfqNum">RFQ-001</span>
</div>
<div class="info-row">
<span class="info-label">OMS RFQ :</span>
<span class="info-value" id="rfqNum">RFQ-001</span>
</div>
<div class="info-row">
<span class="info-label">DATE :</span>
<span class="info-value" id="orderDate">Current Date</span>
</div>
</div>
</div>
<!-- items table: ITEM NO, DESCRIPTION, REMARKS, UNIT, QTY, UNIT PRICE, TOTAL PRICE -->
<div style="overflow-x: auto;">
<table class="items-table" id="poItemsTable">
<thead>
<tr>
<th>ITEM NO</th>
<th>DESCRIPTION</th>
<th>REMARKS</th>
<th>QTY</th>
<th>UNIT</th>
<th>UNIT PRICE</th>
<th>TOTAL PRICE</th>
</tr>
</thead>
<tbody id="itemsTableBody">
{{items}}
</tbody>
</table>
</div>
<!-- summary: subtotal, discount, transportation, full total -->
<div class="summary-area">
<table class="summary-table" id="summaryTable">
<tr>
<td>SUB TOTAL</td>
<td id="subTotalVal">{{subtotal}}</td>
</tr>
<tr>
<td>DISCOUNT <span style="font-weight:normal;">(%)</span>
</td>
<td id="discountAmount">{{discount}}</td>
</tr>
<tr>
<td>ADDITIONAL CHARGES (CUSTOM/HANDLING/TRANSPORT)
</td>
<td id="transportDisplay">{{transport}}</td>
</tr>
<tr class="total-row">
<td>GRAND TOTAL</td>
<td id="fullTotalVal">{{fullTotal}}</td>
</tr>
</table>
</div>
<div class="footer-note">
<img src="https://res.cloudinary.com/dtthuvvir/image/upload/v1780748661/footer_ax2jyz.png" alt="Footer">
</div>
</div>
</body>
</html>