chore: upload parte 1 - logica
DevSecOps Enterprise Pipeline / security-gate-and-deploy (push) Failing after 41s
DevSecOps Enterprise Pipeline / security-gate-and-deploy (push) Failing after 41s
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
import {
|
||||
createBrandedPdf,
|
||||
addBrandedFooter,
|
||||
COLORS,
|
||||
} from "./pdf-template";
|
||||
|
||||
interface SupplierPoItem {
|
||||
part_number: string;
|
||||
component_name: string;
|
||||
supplier_part_number?: string | null;
|
||||
quantity: number;
|
||||
unit_price: number;
|
||||
}
|
||||
|
||||
interface SupplierPoPdfData {
|
||||
po_number: string;
|
||||
date: string;
|
||||
status: string;
|
||||
supplier_name: string;
|
||||
supplier_code: string;
|
||||
supplier_email?: string | null;
|
||||
supplier_phone?: string | null;
|
||||
supplier_address?: string | null;
|
||||
supplier_contact?: string | null;
|
||||
supplier_country?: string | null;
|
||||
supplier_currency?: string | null;
|
||||
shipping_cost?: number;
|
||||
notes?: string | null;
|
||||
items: SupplierPoItem[];
|
||||
}
|
||||
|
||||
function makeCurrencyFormatter(currency: string) {
|
||||
return (val: number): string =>
|
||||
new Intl.NumberFormat("en-GB", {
|
||||
style: "currency",
|
||||
currency,
|
||||
}).format(val);
|
||||
}
|
||||
|
||||
export function generateSupplierPoPdf(data: SupplierPoPdfData) {
|
||||
const { doc, contentY } = createBrandedPdf({
|
||||
title: "PURCHASE ORDER",
|
||||
documentNumber: data.po_number,
|
||||
date: new Date(data.date).toLocaleDateString("en-GB"),
|
||||
recipient: {
|
||||
label: "SUPPLIER",
|
||||
name: data.supplier_name,
|
||||
code: data.supplier_code,
|
||||
contact: data.supplier_contact,
|
||||
address: data.supplier_address,
|
||||
phone: data.supplier_phone,
|
||||
email: data.supplier_email,
|
||||
},
|
||||
});
|
||||
|
||||
const pageWidth = doc.internal.pageSize.getWidth();
|
||||
const margin = 14;
|
||||
const currency = data.supplier_currency || "EUR";
|
||||
const formatCurrency = makeCurrencyFormatter(currency);
|
||||
const shippingCost = data.shipping_cost || 0;
|
||||
const isPortugal = (data.supplier_country || "").toLowerCase().trim() === "portugal";
|
||||
|
||||
// --- Order Items Section ---
|
||||
let y = contentY;
|
||||
const contentWidth = pageWidth - margin * 2;
|
||||
const rightEdge = pageWidth - margin;
|
||||
|
||||
const totalValue = data.items.reduce((sum, item) => sum + item.quantity * item.unit_price, 0);
|
||||
const totalQty = data.items.reduce((sum, item) => sum + item.quantity, 0);
|
||||
|
||||
// Section header
|
||||
doc.setFontSize(9);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setTextColor(...COLORS.primary);
|
||||
doc.text("ORDER ITEMS", margin, y);
|
||||
|
||||
y += 5;
|
||||
|
||||
// Column headers -- no grid, just text labels
|
||||
const colX = {
|
||||
num: margin + 1,
|
||||
part: margin + 10,
|
||||
name: margin + 42,
|
||||
ref: margin + 105,
|
||||
qty: rightEdge - 54,
|
||||
price: rightEdge - 30,
|
||||
total: rightEdge,
|
||||
};
|
||||
|
||||
doc.setFontSize(6);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setTextColor(...COLORS.muted);
|
||||
doc.text("#", colX.num, y);
|
||||
doc.text("PART NUMBER", colX.part, y);
|
||||
doc.text("COMPONENT", colX.name, y);
|
||||
doc.text("SUPPLIER REF", colX.ref, y);
|
||||
doc.text("QTY", colX.qty, y, { align: "right" });
|
||||
doc.text("UNIT PRICE", colX.price, y, { align: "right" });
|
||||
doc.text("TOTAL", colX.total, y, { align: "right" });
|
||||
|
||||
y += 1.5;
|
||||
doc.setDrawColor(...COLORS.border);
|
||||
doc.setLineWidth(0.3);
|
||||
doc.line(margin, y, rightEdge, y);
|
||||
y += 3;
|
||||
|
||||
// Item rows -- clean card-like rows with left accent
|
||||
const rowH = 9;
|
||||
data.items.forEach((item, i) => {
|
||||
const isAlt = i % 2 === 1;
|
||||
const rowY = y;
|
||||
|
||||
// Subtle alternating background
|
||||
if (isAlt) {
|
||||
doc.setFillColor(250, 250, 252);
|
||||
doc.rect(margin, rowY - 2.5, contentWidth, rowH, "F");
|
||||
}
|
||||
|
||||
// Left accent bar (blue)
|
||||
doc.setFillColor(...COLORS.accent);
|
||||
doc.rect(margin, rowY - 2.5, 1.2, rowH, "F");
|
||||
|
||||
// Row number
|
||||
doc.setFontSize(7);
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setTextColor(...COLORS.muted);
|
||||
doc.text(String(i + 1), colX.num + 2, rowY + 1.5);
|
||||
|
||||
// Part number -- monospaced feel
|
||||
doc.setFontSize(7.5);
|
||||
doc.setFont("courier", "bold");
|
||||
doc.setTextColor(...COLORS.primary);
|
||||
doc.text(item.part_number, colX.part, rowY + 1.5);
|
||||
|
||||
// Component name
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setTextColor(...COLORS.secondary);
|
||||
const nameMaxW = colX.ref - colX.name - 3;
|
||||
const truncatedName = doc.getTextWidth(item.component_name) > nameMaxW
|
||||
? item.component_name.substring(0, Math.floor(item.component_name.length * nameMaxW / doc.getTextWidth(item.component_name))) + "..."
|
||||
: item.component_name;
|
||||
doc.text(truncatedName, colX.name, rowY + 1.5);
|
||||
|
||||
// Supplier ref
|
||||
doc.setFontSize(7);
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setTextColor(...COLORS.muted);
|
||||
doc.text(item.supplier_part_number || "-", colX.ref, rowY + 1.5);
|
||||
|
||||
// Quantity -- emphasized
|
||||
doc.setFontSize(8);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setTextColor(...COLORS.primary);
|
||||
doc.text(String(item.quantity), colX.qty, rowY + 1.5, { align: "right" });
|
||||
|
||||
// Unit price
|
||||
doc.setFontSize(7.5);
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setTextColor(...COLORS.secondary);
|
||||
doc.text(formatCurrency(item.unit_price), colX.price, rowY + 1.5, { align: "right" });
|
||||
|
||||
// Line total
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setTextColor(...COLORS.primary);
|
||||
doc.text(formatCurrency(item.quantity * item.unit_price), colX.total, rowY + 1.5, { align: "right" });
|
||||
|
||||
y += rowH;
|
||||
});
|
||||
|
||||
// Bottom separator
|
||||
y += 1;
|
||||
doc.setDrawColor(...COLORS.border);
|
||||
doc.setLineWidth(0.3);
|
||||
doc.line(margin, y, rightEdge, y);
|
||||
|
||||
// --- Summary section ---
|
||||
y += 5;
|
||||
|
||||
const summaryW = 75;
|
||||
const summaryX = rightEdge - summaryW;
|
||||
const labelX = summaryX + 4;
|
||||
const valueX = rightEdge - 4;
|
||||
const lineSpacing = 6;
|
||||
|
||||
// Subtotal
|
||||
doc.setFontSize(7.5);
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setTextColor(...COLORS.muted);
|
||||
doc.text("Subtotal", labelX, y);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setTextColor(...COLORS.primary);
|
||||
doc.text(formatCurrency(totalValue), valueX, y, { align: "right" });
|
||||
y += lineSpacing;
|
||||
|
||||
// Shipping
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setTextColor(...COLORS.muted);
|
||||
doc.text("Shipping", labelX, y);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setTextColor(...COLORS.primary);
|
||||
doc.text(formatCurrency(shippingCost), valueX, y, { align: "right" });
|
||||
y += lineSpacing;
|
||||
|
||||
// VAT for Portugal
|
||||
let vatAmount = 0;
|
||||
if (isPortugal) {
|
||||
vatAmount = (totalValue + shippingCost) * 0.23;
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setTextColor(...COLORS.muted);
|
||||
doc.text("VAT (23%)", labelX, y);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setTextColor(...COLORS.primary);
|
||||
doc.text(formatCurrency(vatAmount), valueX, y, { align: "right" });
|
||||
y += lineSpacing;
|
||||
}
|
||||
|
||||
// Separator line
|
||||
y += 1;
|
||||
doc.setDrawColor(...COLORS.border);
|
||||
doc.setLineWidth(0.3);
|
||||
doc.line(summaryX, y, rightEdge, y);
|
||||
y += 4;
|
||||
|
||||
// Grand total box
|
||||
const grandTotal = totalValue + shippingCost + vatAmount;
|
||||
const totalBoxW = summaryW;
|
||||
const totalBoxX = summaryX;
|
||||
const totalBoxY = y - 3;
|
||||
|
||||
doc.setFillColor(...COLORS.accent);
|
||||
doc.roundedRect(totalBoxX, totalBoxY, totalBoxW, 12, 2, 2, "F");
|
||||
|
||||
doc.setFontSize(6.5);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setTextColor(220, 230, 245);
|
||||
doc.text("TOTAL", totalBoxX + 5, totalBoxY + 5);
|
||||
doc.setFontSize(11);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.text(formatCurrency(grandTotal), totalBoxX + totalBoxW - 5, totalBoxY + 9, { align: "right" });
|
||||
|
||||
const finalY = totalBoxY + 14;
|
||||
|
||||
// --- Notes ---
|
||||
if (data.notes) {
|
||||
const noteY = totalBoxY + 18;
|
||||
doc.setFillColor(...COLORS.bgLight);
|
||||
doc.setDrawColor(...COLORS.border);
|
||||
doc.setLineWidth(0.2);
|
||||
doc.roundedRect(margin, noteY, contentWidth, 16, 1.5, 1.5, "FD");
|
||||
doc.setFontSize(6.5);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setTextColor(...COLORS.accent);
|
||||
doc.text("NOTES", margin + 4, noteY + 5);
|
||||
doc.setFontSize(7.5);
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setTextColor(...COLORS.secondary);
|
||||
const noteLines = doc.splitTextToSize(data.notes, contentWidth - 8);
|
||||
doc.text(noteLines, margin + 4, noteY + 11);
|
||||
}
|
||||
|
||||
addBrandedFooter(doc);
|
||||
return doc;
|
||||
}
|
||||
Reference in New Issue
Block a user