Some checks failed
Deploy to Production / Deploy to Server (push) Failing after 20s
- Include compiled TypeScript with new /impressum, /privacy, /terms routes - Temporary commit of dist files for Docker deployment
163 lines
7.7 KiB
JavaScript
163 lines
7.7 KiB
JavaScript
export const templates = {
|
|
invoice: {
|
|
name: "Invoice",
|
|
description: "Professional invoice with line items, taxes, and payment details",
|
|
fields: [
|
|
{ name: "invoiceNumber", type: "string", required: true, description: "Invoice number" },
|
|
{ name: "date", type: "string", required: true, description: "Invoice date (YYYY-MM-DD)" },
|
|
{ name: "dueDate", type: "string", required: false, description: "Due date" },
|
|
{ name: "from", type: "object", required: true, description: "Sender: {name, address?, email?, phone?, vatId?}" },
|
|
{ name: "to", type: "object", required: true, description: "Recipient: {name, address?, email?, vatId?}" },
|
|
{ name: "items", type: "array", required: true, description: "Line items: [{description, quantity, unitPrice, taxRate?}]" },
|
|
{ name: "currency", type: "string", required: false, description: "Currency symbol (default: €)" },
|
|
{ name: "notes", type: "string", required: false, description: "Additional notes" },
|
|
{ name: "paymentDetails", type: "string", required: false, description: "Bank/payment info" },
|
|
],
|
|
render: renderInvoice,
|
|
},
|
|
receipt: {
|
|
name: "Receipt",
|
|
description: "Simple receipt for payments received",
|
|
fields: [
|
|
{ name: "receiptNumber", type: "string", required: true, description: "Receipt number" },
|
|
{ name: "date", type: "string", required: true, description: "Date" },
|
|
{ name: "from", type: "object", required: true, description: "Business: {name, address?}" },
|
|
{ name: "to", type: "object", required: false, description: "Customer: {name, email?}" },
|
|
{ name: "items", type: "array", required: true, description: "Items: [{description, amount}]" },
|
|
{ name: "currency", type: "string", required: false, description: "Currency symbol" },
|
|
{ name: "paymentMethod", type: "string", required: false, description: "Payment method" },
|
|
],
|
|
render: renderReceipt,
|
|
},
|
|
};
|
|
function esc(s) {
|
|
return String(s || "")
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """);
|
|
}
|
|
function renderInvoice(d) {
|
|
const cur = esc(d.currency || "€");
|
|
const items = d.items || [];
|
|
let subtotal = 0;
|
|
let totalTax = 0;
|
|
const rows = items
|
|
.map((item) => {
|
|
const qty = Number(item.quantity) || 1;
|
|
const price = Number(item.unitPrice) || 0;
|
|
const taxRate = Number(item.taxRate) || 0;
|
|
const lineTotal = qty * price;
|
|
const lineTax = lineTotal * (taxRate / 100);
|
|
subtotal += lineTotal;
|
|
totalTax += lineTax;
|
|
return `<tr>
|
|
<td>${esc(item.description)}</td>
|
|
<td style="text-align:right">${qty}</td>
|
|
<td style="text-align:right">${cur}${price.toFixed(2)}</td>
|
|
<td style="text-align:right">${taxRate}%</td>
|
|
<td style="text-align:right">${cur}${lineTotal.toFixed(2)}</td>
|
|
</tr>`;
|
|
})
|
|
.join("");
|
|
const total = subtotal + totalTax;
|
|
const from = d.from || {};
|
|
const to = d.to || {};
|
|
return `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 13px; color: #222; padding: 40px; }
|
|
.header { display: flex; justify-content: space-between; margin-bottom: 40px; }
|
|
.header h1 { font-size: 28px; color: #1a1a1a; }
|
|
.meta { text-align: right; }
|
|
.meta div { margin-bottom: 4px; }
|
|
.parties { display: flex; justify-content: space-between; margin-bottom: 30px; }
|
|
.party { width: 45%; }
|
|
.party h3 { font-size: 11px; text-transform: uppercase; color: #888; margin-bottom: 8px; }
|
|
.party p { margin-bottom: 2px; }
|
|
table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
|
|
th { background: #f8f8f8; text-align: left; padding: 10px; font-size: 11px; text-transform: uppercase; color: #666; border-bottom: 2px solid #ddd; }
|
|
td { padding: 10px; border-bottom: 1px solid #eee; }
|
|
.totals { text-align: right; margin-bottom: 30px; }
|
|
.totals div { margin-bottom: 4px; }
|
|
.totals .total { font-size: 18px; font-weight: 700; color: #1a1a1a; }
|
|
.footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #eee; font-size: 12px; color: #666; }
|
|
</style></head><body>
|
|
<div class="header">
|
|
<h1>INVOICE</h1>
|
|
<div class="meta">
|
|
<div><strong>#${esc(d.invoiceNumber)}</strong></div>
|
|
<div>Date: ${esc(d.date)}</div>
|
|
${d.dueDate ? `<div>Due: ${esc(d.dueDate)}</div>` : ""}
|
|
</div>
|
|
</div>
|
|
<div class="parties">
|
|
<div class="party">
|
|
<h3>From</h3>
|
|
<p><strong>${esc(from.name)}</strong></p>
|
|
${from.address ? `<p>${esc(from.address).replace(/\n/g, "<br>")}</p>` : ""}
|
|
${from.email ? `<p>${esc(from.email)}</p>` : ""}
|
|
${from.vatId ? `<p>VAT: ${esc(from.vatId)}</p>` : ""}
|
|
</div>
|
|
<div class="party">
|
|
<h3>To</h3>
|
|
<p><strong>${esc(to.name)}</strong></p>
|
|
${to.address ? `<p>${esc(to.address).replace(/\n/g, "<br>")}</p>` : ""}
|
|
${to.email ? `<p>${esc(to.email)}</p>` : ""}
|
|
${to.vatId ? `<p>VAT: ${esc(to.vatId)}</p>` : ""}
|
|
</div>
|
|
</div>
|
|
<table>
|
|
<thead><tr><th>Description</th><th style="text-align:right">Qty</th><th style="text-align:right">Price</th><th style="text-align:right">Tax</th><th style="text-align:right">Total</th></tr></thead>
|
|
<tbody>${rows}</tbody>
|
|
</table>
|
|
<div class="totals">
|
|
<div>Subtotal: ${cur}${subtotal.toFixed(2)}</div>
|
|
<div>Tax: ${cur}${totalTax.toFixed(2)}</div>
|
|
<div class="total">Total: ${cur}${total.toFixed(2)}</div>
|
|
</div>
|
|
${d.paymentDetails ? `<div class="footer"><strong>Payment Details</strong><br>${esc(d.paymentDetails).replace(/\n/g, "<br>")}</div>` : ""}
|
|
${d.notes ? `<div class="footer"><strong>Notes</strong><br>${esc(d.notes)}</div>` : ""}
|
|
</body></html>`;
|
|
}
|
|
function renderReceipt(d) {
|
|
const cur = esc(d.currency || "€");
|
|
const items = d.items || [];
|
|
let total = 0;
|
|
const rows = items
|
|
.map((item) => {
|
|
const amount = Number(item.amount) || 0;
|
|
total += amount;
|
|
return `<tr><td>${esc(item.description)}</td><td style="text-align:right">${cur}${amount.toFixed(2)}</td></tr>`;
|
|
})
|
|
.join("");
|
|
const from = d.from || {};
|
|
const to = d.to || {};
|
|
return `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
|
|
body { font-family: 'Courier New', monospace; font-size: 13px; max-width: 320px; margin: 0 auto; padding: 30px 20px; }
|
|
h1 { text-align: center; font-size: 18px; margin-bottom: 4px; }
|
|
.center { text-align: center; margin-bottom: 16px; }
|
|
hr { border: none; border-top: 1px dashed #999; margin: 12px 0; }
|
|
table { width: 100%; }
|
|
td { padding: 3px 0; }
|
|
.total { font-weight: bold; font-size: 16px; }
|
|
</style></head><body>
|
|
<h1>${esc(from.name)}</h1>
|
|
${from.address ? `<div class="center">${esc(from.address)}</div>` : ""}
|
|
<hr>
|
|
<div>Receipt #${esc(d.receiptNumber)}</div>
|
|
<div>Date: ${esc(d.date)}</div>
|
|
${to?.name ? `<div>Customer: ${esc(to.name)}</div>` : ""}
|
|
<hr>
|
|
<table>${rows}</table>
|
|
<hr>
|
|
<table><tr><td class="total">TOTAL</td><td class="total" style="text-align:right">${cur}${total.toFixed(2)}</td></tr></table>
|
|
${d.paymentMethod ? `<hr><div>Paid via: ${esc(d.paymentMethod)}</div>` : ""}
|
|
<hr><div class="center">Thank you!</div>
|
|
</body></html>`;
|
|
}
|
|
export function renderTemplate(id, data) {
|
|
const template = templates[id];
|
|
if (!template)
|
|
throw new Error(`Template '${id}' not found`);
|
|
return template.render(data);
|
|
}
|