immortal-js 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +577 -0
- package/docs/CHANGELOG.md +21 -0
- package/docs/CONTRIBUTING.md +41 -0
- package/docs/api/chaos.md +179 -0
- package/docs/api/dashboard.md +109 -0
- package/docs/api/isolation.md +191 -0
- package/docs/api/lifecycle.md +187 -0
- package/docs/api/monitoring.md +313 -0
- package/docs/api/plugins.md +217 -0
- package/docs/api/recovery.md +267 -0
- package/docs/api/safe-zone.md +236 -0
- package/docs/api/supervision.md +285 -0
- package/docs/guides/configuration.md +168 -0
- package/docs/guides/express.md +171 -0
- package/docs/guides/fastify.md +188 -0
- package/docs/guides/koa.md +102 -0
- package/docs/guides/nestjs.md +182 -0
- package/docs/guides/testing.md +91 -0
- package/examples/express-basic/index.ts +462 -0
- package/examples/express-basic/package.json +21 -0
- package/examples/express-basic/tsconfig.json +24 -0
- package/examples/fastify-microservice/index.ts +342 -0
- package/examples/fastify-microservice/package.json +22 -0
- package/examples/fastify-microservice/tsconfig.json +24 -0
- package/examples/invoice-service/data/invoices.db +0 -0
- package/examples/invoice-service/data/invoices.db-shm +0 -0
- package/examples/invoice-service/data/invoices.db-wal +0 -0
- package/examples/invoice-service/package.json +25 -0
- package/examples/invoice-service/public/index.html +5025 -0
- package/examples/invoice-service/src/db.ts +608 -0
- package/examples/invoice-service/src/pdf.ts +358 -0
- package/examples/invoice-service/src/server.ts +527 -0
- package/examples/invoice-service/src/store.ts +159 -0
- package/examples/invoice-service/src/types.ts +193 -0
- package/examples/invoice-service/tsconfig.json +23 -0
- package/examples/nestjs-enterprise/app.module.ts +561 -0
- package/examples/nestjs-enterprise/main.ts +67 -0
- package/examples/nestjs-enterprise/package.json +26 -0
- package/examples/nestjs-enterprise/tsconfig.json +27 -0
- package/immortal-js-1.0.0.tgz +0 -0
- package/package.json +33 -0
- package/packages/adapter-express/package.json +34 -0
- package/packages/adapter-express/src/index.ts +349 -0
- package/packages/adapter-express/tsconfig.json +14 -0
- package/packages/adapter-fastify/package.json +56 -0
- package/packages/adapter-fastify/src/plugin.ts +226 -0
- package/packages/adapter-fastify/tsconfig.json +14 -0
- package/packages/adapter-koa/package.json +55 -0
- package/packages/adapter-koa/src/index.ts +207 -0
- package/packages/adapter-koa/tsconfig.json +14 -0
- package/packages/adapter-nestjs/package.json +61 -0
- package/packages/adapter-nestjs/src/immortal.module.ts +313 -0
- package/packages/adapter-nestjs/src/index.ts +14 -0
- package/packages/adapter-nestjs/tsconfig.json +16 -0
- package/packages/core/package.json +56 -0
- package/packages/core/src/chaos/ChaosEngine.ts +249 -0
- package/packages/core/src/config/defaults.ts +200 -0
- package/packages/core/src/config/schema.ts +199 -0
- package/packages/core/src/event-bus.ts +168 -0
- package/packages/core/src/index.ts +164 -0
- package/packages/core/src/isolation/BulkheadPool.ts +279 -0
- package/packages/core/src/isolation/WorkerSandbox.ts +306 -0
- package/packages/core/src/isolation/index.ts +8 -0
- package/packages/core/src/lifecycle/GracefulShutdown.ts +161 -0
- package/packages/core/src/logger.ts +104 -0
- package/packages/core/src/monitoring/DiagnosticsChannel.ts +248 -0
- package/packages/core/src/monitoring/HealthMonitor.ts +191 -0
- package/packages/core/src/monitoring/MemoryLeakGuard.ts +340 -0
- package/packages/core/src/monitoring/MetricsCollector.ts +219 -0
- package/packages/core/src/monitoring/index.ts +10 -0
- package/packages/core/src/plugins/BuiltinPlugins.ts +269 -0
- package/packages/core/src/recovery/CircuitBreaker.ts +334 -0
- package/packages/core/src/recovery/FallbackCache.ts +328 -0
- package/packages/core/src/recovery/RetryEngine.ts +225 -0
- package/packages/core/src/recovery/Timeout.ts +97 -0
- package/packages/core/src/recovery/index.ts +11 -0
- package/packages/core/src/runtime.ts +242 -0
- package/packages/core/src/safe-zone/AsyncBoundary.ts +114 -0
- package/packages/core/src/safe-zone/ErrorTrap.ts +347 -0
- package/packages/core/src/safe-zone/SafeWrapper.ts +317 -0
- package/packages/core/src/safe-zone/index.ts +23 -0
- package/packages/core/src/supervision/ClusterManager.ts +243 -0
- package/packages/core/src/supervision/RestartStrategy.ts +68 -0
- package/packages/core/src/supervision/Supervisor.ts +311 -0
- package/packages/core/src/supervision/index.ts +11 -0
- package/packages/core/src/types.ts +470 -0
- package/packages/core/test/bulkhead.test.ts +310 -0
- package/packages/core/test/circuit-breaker.test.ts +153 -0
- package/packages/core/test/memory-guard.test.ts +213 -0
- package/packages/core/test/retry.test.ts +110 -0
- package/packages/core/test/safe-zone.test.ts +271 -0
- package/packages/core/test/supervisor.test.ts +310 -0
- package/packages/core/tsconfig.json +13 -0
- package/packages/dashboard/package.json +56 -0
- package/packages/dashboard/server/DashboardServer.ts +454 -0
- package/packages/dashboard/tsconfig.json +14 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/pdf.ts
|
|
3
|
+
* @description Invoice Service Pro v3 — Ultra Premium PDF HTML Generator
|
|
4
|
+
*
|
|
5
|
+
* Strategy: Generate a complete, self-contained HTML document styled for
|
|
6
|
+
* professional print output. Browser Ctrl+P / window.print() → PDF.
|
|
7
|
+
* No binary dependencies. In production, pipe into Puppeteer for true PDF bytes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Invoice, InvoiceTotals, InvoiceCurrency } from "./types.js";
|
|
11
|
+
import { CURRENCY_META } from "./types.js";
|
|
12
|
+
|
|
13
|
+
// ─── Money ────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export function formatMoney(cents: number, currency: InvoiceCurrency): string {
|
|
16
|
+
const meta = CURRENCY_META[currency];
|
|
17
|
+
const formatted = (cents / 100).toLocaleString("en-US", {
|
|
18
|
+
minimumFractionDigits: currency === "JPY" ? 0 : 2,
|
|
19
|
+
maximumFractionDigits: currency === "JPY" ? 0 : 2,
|
|
20
|
+
});
|
|
21
|
+
return `${meta.symbol}${formatted}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── Totals ───────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export function calculateTotals(invoice: Invoice): InvoiceTotals {
|
|
27
|
+
const cur = invoice.currency;
|
|
28
|
+
const globalDiscount = invoice.discount ?? 0;
|
|
29
|
+
let subtotalCents = 0;
|
|
30
|
+
let discountCents = 0;
|
|
31
|
+
let taxCents = 0;
|
|
32
|
+
|
|
33
|
+
for (const item of invoice.lineItems) {
|
|
34
|
+
const lineSubtotal = Math.round(item.quantity * item.unitPrice);
|
|
35
|
+
const lineDiscount = Math.round(lineSubtotal * (item.discount ?? 0));
|
|
36
|
+
const lineAfterDiscount = lineSubtotal - lineDiscount;
|
|
37
|
+
const lineTax = Math.round(lineAfterDiscount * item.taxRate);
|
|
38
|
+
subtotalCents += lineSubtotal;
|
|
39
|
+
discountCents += lineDiscount;
|
|
40
|
+
taxCents += lineTax;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const afterLineDiscounts = subtotalCents - discountCents;
|
|
44
|
+
const globalDiscountAmt = Math.round(afterLineDiscounts * globalDiscount);
|
|
45
|
+
const totalDiscountCents = discountCents + globalDiscountAmt;
|
|
46
|
+
const afterAllDiscounts = subtotalCents - totalDiscountCents;
|
|
47
|
+
const totalCents = afterAllDiscounts + taxCents;
|
|
48
|
+
const effectiveTax = afterAllDiscounts > 0 ? (taxCents / afterAllDiscounts) * 100 : 0;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
subtotalCents,
|
|
52
|
+
discountCents: totalDiscountCents,
|
|
53
|
+
taxCents,
|
|
54
|
+
totalCents,
|
|
55
|
+
subtotal: formatMoney(subtotalCents, cur),
|
|
56
|
+
discount: formatMoney(totalDiscountCents, cur),
|
|
57
|
+
tax: formatMoney(taxCents, cur),
|
|
58
|
+
total: formatMoney(totalCents, cur),
|
|
59
|
+
effectiveTaxRate: effectiveTax.toFixed(1) + "%",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Status Badge ─────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
function statusStyle(s: string): { bg: string; color: string; border: string; label: string } {
|
|
66
|
+
const map: Record<string, { bg: string; color: string; border: string; label: string }> = {
|
|
67
|
+
draft: { bg: "#1e293b", color: "#94a3b8", border: "#334155", label: "DRAFT" },
|
|
68
|
+
sent: { bg: "#1e3a8a", color: "#93c5fd", border: "#1d4ed8", label: "SENT" },
|
|
69
|
+
paid: { bg: "#064e3b", color: "#6ee7b7", border: "#059669", label: "PAID" },
|
|
70
|
+
overdue: { bg: "#7f1d1d", color: "#fca5a5", border: "#dc2626", label: "OVERDUE" },
|
|
71
|
+
cancelled: { bg: "#1c1917", color: "#78716c", border: "#44403c", label: "CANCELLED" },
|
|
72
|
+
refunded: { bg: "#4a1d96", color: "#c4b5fd", border: "#7c3aed", label: "REFUNDED" },
|
|
73
|
+
};
|
|
74
|
+
return map[s] ?? map["draft"]!;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── HTML Escape ──────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
function esc(v: string | undefined | null): string {
|
|
80
|
+
if (!v) return "";
|
|
81
|
+
return String(v)
|
|
82
|
+
.replace(/&/g, "&").replace(/</g, "<")
|
|
83
|
+
.replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Main Generator ───────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
export function generatePdfHtml(invoice: Invoice): string {
|
|
89
|
+
const totals = calculateTotals(invoice);
|
|
90
|
+
const cur = invoice.currency;
|
|
91
|
+
const ss = statusStyle(invoice.status);
|
|
92
|
+
const meta = CURRENCY_META[cur];
|
|
93
|
+
|
|
94
|
+
const lineItemRows = invoice.lineItems.map((item, idx) => {
|
|
95
|
+
const lineSubtotal = Math.round(item.quantity * item.unitPrice);
|
|
96
|
+
const lineDiscount = Math.round(lineSubtotal * (item.discount ?? 0));
|
|
97
|
+
const lineAfterDisc = lineSubtotal - lineDiscount;
|
|
98
|
+
const lineTax = Math.round(lineAfterDisc * item.taxRate);
|
|
99
|
+
const lineTotal = lineAfterDisc + lineTax;
|
|
100
|
+
const rowBg = idx % 2 === 0 ? "#0f172a" : "#111827";
|
|
101
|
+
return `
|
|
102
|
+
<tr style="background:${rowBg};">
|
|
103
|
+
<td style="padding:12px 16px;border-bottom:1px solid #1e293b;">${esc(item.description)}${item.unit ? `<span style="font-size:11px;color:#475569;margin-left:6px;">/ ${esc(item.unit)}</span>` : ""}</td>
|
|
104
|
+
<td style="padding:12px 16px;border-bottom:1px solid #1e293b;text-align:center;font-family:monospace;font-size:13px;">${item.quantity % 1 === 0 ? item.quantity : item.quantity.toFixed(2)}</td>
|
|
105
|
+
<td style="padding:12px 16px;border-bottom:1px solid #1e293b;text-align:right;font-family:monospace;">${formatMoney(item.unitPrice, cur)}</td>
|
|
106
|
+
<td style="padding:12px 16px;border-bottom:1px solid #1e293b;text-align:center;color:#94a3b8;">${(item.taxRate * 100).toFixed(0)}%</td>
|
|
107
|
+
${item.discount ? `<td style="padding:12px 16px;border-bottom:1px solid #1e293b;text-align:center;color:#f59e0b;">-${(item.discount * 100).toFixed(0)}%</td>` : `<td style="padding:12px 16px;border-bottom:1px solid #1e293b;text-align:center;color:#475569;">—</td>`}
|
|
108
|
+
<td style="padding:12px 16px;border-bottom:1px solid #1e293b;text-align:right;font-family:monospace;font-weight:700;color:#e2e8f0;">${formatMoney(lineTotal, cur)}</td>
|
|
109
|
+
</tr>`;
|
|
110
|
+
}).join("");
|
|
111
|
+
|
|
112
|
+
return `<!DOCTYPE html>
|
|
113
|
+
<html lang="en">
|
|
114
|
+
<head>
|
|
115
|
+
<meta charset="UTF-8"/>
|
|
116
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
117
|
+
<title>Invoice ${esc(invoice.number)} — ${esc(invoice.to.name)}</title>
|
|
118
|
+
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
|
119
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap"/>
|
|
120
|
+
<style>
|
|
121
|
+
*{box-sizing:border-box;margin:0;padding:0;}
|
|
122
|
+
body{font-family:'Inter',system-ui,sans-serif;font-size:14px;color:#e2e8f0;background:#090e17;padding:0;margin:0;}
|
|
123
|
+
.page{max-width:900px;margin:40px auto;background:#0f172a;border-radius:20px;overflow:hidden;box-shadow:0 25px 80px rgba(0,0,0,.6),0 0 0 1px #1e293b;}
|
|
124
|
+
|
|
125
|
+
/* Header */
|
|
126
|
+
.header{
|
|
127
|
+
background:linear-gradient(135deg,#0f172a 0%,#1e3a5f 50%,#0f172a 100%);
|
|
128
|
+
padding:48px 56px;display:flex;justify-content:space-between;align-items:flex-start;
|
|
129
|
+
border-bottom:1px solid #1e3a5f;position:relative;overflow:hidden;
|
|
130
|
+
}
|
|
131
|
+
.header::before{
|
|
132
|
+
content:'';position:absolute;top:-80px;right:-80px;width:300px;height:300px;
|
|
133
|
+
background:radial-gradient(circle,rgba(56,189,248,.08) 0%,transparent 70%);
|
|
134
|
+
border-radius:50%;
|
|
135
|
+
}
|
|
136
|
+
.brand-name{font-size:30px;font-weight:900;letter-spacing:-1px;line-height:1;}
|
|
137
|
+
.brand-name span{color:#38bdf8;}
|
|
138
|
+
.brand-tagline{font-size:12px;color:#475569;margin-top:5px;letter-spacing:0.5px;}
|
|
139
|
+
.invoice-meta{text-align:right;}
|
|
140
|
+
.invoice-number{font-size:26px;font-weight:800;font-family:monospace;color:#38bdf8;letter-spacing:1px;}
|
|
141
|
+
.invoice-dates{margin-top:8px;font-size:12px;color:#64748b;line-height:1.8;}
|
|
142
|
+
.invoice-dates strong{color:#94a3b8;}
|
|
143
|
+
|
|
144
|
+
/* Status bar */
|
|
145
|
+
.status-bar{
|
|
146
|
+
background:#111827;padding:14px 56px;display:flex;align-items:center;
|
|
147
|
+
justify-content:space-between;border-bottom:1px solid #1e293b;
|
|
148
|
+
}
|
|
149
|
+
.status-badge{
|
|
150
|
+
display:inline-flex;align-items:center;gap:6px;
|
|
151
|
+
background:${ss.bg};color:${ss.color};border:1px solid ${ss.border};
|
|
152
|
+
padding:5px 14px;border-radius:20px;font-size:11px;font-weight:800;letter-spacing:1.2px;
|
|
153
|
+
}
|
|
154
|
+
.status-badge::before{content:'';width:6px;height:6px;border-radius:50%;background:${ss.color};}
|
|
155
|
+
.currency-info{font-size:12px;color:#64748b;}
|
|
156
|
+
.currency-info strong{color:#94a3b8;}
|
|
157
|
+
.invoice-ref{font-size:12px;color:#475569;}
|
|
158
|
+
.invoice-ref span{color:#64748b;}
|
|
159
|
+
|
|
160
|
+
/* Parties */
|
|
161
|
+
.parties{display:grid;grid-template-columns:1fr 1fr;border-bottom:1px solid #1e293b;}
|
|
162
|
+
.party{padding:36px 56px;}
|
|
163
|
+
.party:first-child{border-right:1px solid #1e293b;}
|
|
164
|
+
.party-label{font-size:10px;font-weight:800;color:#38bdf8;letter-spacing:2px;text-transform:uppercase;margin-bottom:14px;}
|
|
165
|
+
.party-name{font-size:18px;font-weight:700;color:#f1f5f9;margin-bottom:6px;}
|
|
166
|
+
.party-detail{font-size:13px;color:#64748b;line-height:1.8;}
|
|
167
|
+
.party-email{color:#38bdf8;font-size:13px;margin-top:4px;}
|
|
168
|
+
.party-tax{font-size:11px;color:#475569;margin-top:6px;font-family:monospace;}
|
|
169
|
+
|
|
170
|
+
/* Line items table */
|
|
171
|
+
.table-section{padding:0 40px 32px;}
|
|
172
|
+
.table-title{font-size:11px;font-weight:700;color:#475569;letter-spacing:1.5px;text-transform:uppercase;padding:24px 16px 12px;}
|
|
173
|
+
table{width:100%;border-collapse:collapse;border-radius:10px;overflow:hidden;border:1px solid #1e293b;}
|
|
174
|
+
thead tr{background:#1e293b;}
|
|
175
|
+
thead th{padding:11px 16px;text-align:left;font-size:10px;font-weight:700;color:#64748b;letter-spacing:0.8px;text-transform:uppercase;}
|
|
176
|
+
thead th:not(:first-child){text-align:center;}
|
|
177
|
+
thead th:last-child,thead th:nth-child(3){text-align:right;}
|
|
178
|
+
|
|
179
|
+
/* Totals */
|
|
180
|
+
.totals-wrap{margin:0 40px 40px;display:flex;justify-content:flex-end;}
|
|
181
|
+
.totals{background:#111827;border:1px solid #1e293b;border-radius:12px;padding:24px 32px;min-width:320px;}
|
|
182
|
+
.totals-row{display:flex;justify-content:space-between;padding:7px 0;font-size:14px;border-bottom:1px solid #1e293b;}
|
|
183
|
+
.totals-row:last-child{border-bottom:none;}
|
|
184
|
+
.totals-label{color:#64748b;}
|
|
185
|
+
.totals-value{font-family:monospace;font-weight:600;color:#94a3b8;}
|
|
186
|
+
.totals-row.grand{padding-top:14px;margin-top:4px;font-size:20px;font-weight:800;}
|
|
187
|
+
.totals-row.grand .totals-label{color:#e2e8f0;}
|
|
188
|
+
.totals-row.grand .totals-value{color:#38bdf8;}
|
|
189
|
+
|
|
190
|
+
/* Notes */
|
|
191
|
+
.notes-section{margin:0 40px 40px;display:grid;grid-template-columns:1fr 1fr;gap:20px;}
|
|
192
|
+
.notes-block{background:#111827;border:1px solid #1e293b;border-radius:10px;padding:20px;}
|
|
193
|
+
.notes-label{font-size:10px;font-weight:700;color:#38bdf8;letter-spacing:1.5px;text-transform:uppercase;margin-bottom:10px;}
|
|
194
|
+
.notes-text{font-size:13px;color:#64748b;line-height:1.7;}
|
|
195
|
+
|
|
196
|
+
/* Footer */
|
|
197
|
+
.footer{background:#0a0f1a;border-top:1px solid #1e293b;padding:20px 56px;display:flex;justify-content:space-between;align-items:center;}
|
|
198
|
+
.footer-left{font-size:12px;color:#334155;}
|
|
199
|
+
.footer-right{font-size:11px;color:#475569;text-align:right;}
|
|
200
|
+
|
|
201
|
+
/* Print button */
|
|
202
|
+
.print-btn{
|
|
203
|
+
position:fixed;bottom:32px;right:32px;z-index:999;
|
|
204
|
+
background:linear-gradient(135deg,#0284c7,#0ea5e9);color:#fff;border:none;
|
|
205
|
+
padding:14px 28px;border-radius:50px;font-size:13px;font-weight:700;
|
|
206
|
+
cursor:pointer;box-shadow:0 8px 32px rgba(14,165,233,.4);
|
|
207
|
+
display:flex;align-items:center;gap:10px;font-family:'Inter',sans-serif;
|
|
208
|
+
transition:all .2s;letter-spacing:0.3px;
|
|
209
|
+
}
|
|
210
|
+
.print-btn:hover{transform:translateY(-2px);box-shadow:0 12px 40px rgba(14,165,233,.5);}
|
|
211
|
+
.print-btn svg{width:18px;height:18px;}
|
|
212
|
+
|
|
213
|
+
@media print{
|
|
214
|
+
body{background:#fff;color:#0f172a;}
|
|
215
|
+
.page{box-shadow:none;border-radius:0;margin:0;max-width:100%;background:#fff;}
|
|
216
|
+
.header{background:#f8fafc !important;padding:32px 40px;border-bottom:2px solid #e2e8f0;}
|
|
217
|
+
.brand-name span{color:#0284c7;}
|
|
218
|
+
.status-bar{background:#f1f5f9;padding:10px 40px;}
|
|
219
|
+
.parties .party{padding:24px 40px;}
|
|
220
|
+
.party-detail,.invoice-dates,.currency-info,.invoice-ref,.totals-label,.notes-text{color:#475569;}
|
|
221
|
+
.brand-tagline,.party-tax,.footer-left,.footer-right{color:#94a3b8;}
|
|
222
|
+
.party-name{color:#0f172a;}
|
|
223
|
+
.invoice-number{color:#0284c7;}
|
|
224
|
+
.party-email{color:#0284c7;}
|
|
225
|
+
.party-label,.notes-label{color:#0284c7;}
|
|
226
|
+
table{border-color:#e2e8f0;}
|
|
227
|
+
thead tr{background:#f1f5f9;}
|
|
228
|
+
thead th{color:#64748b;}
|
|
229
|
+
tbody tr{background:#fff !important;}
|
|
230
|
+
tbody td{border-bottom-color:#f1f5f9;color:#0f172a;}
|
|
231
|
+
.totals{background:#f8fafc;border-color:#e2e8f0;}
|
|
232
|
+
.totals-row{border-bottom-color:#e2e8f0;}
|
|
233
|
+
.totals-value{color:#334155;}
|
|
234
|
+
.totals-row.grand .totals-value{color:#0284c7;}
|
|
235
|
+
.totals-row.grand .totals-label{color:#0f172a;}
|
|
236
|
+
.notes-block{background:#f8fafc;border-color:#e2e8f0;}
|
|
237
|
+
.notes-text{color:#475569;}
|
|
238
|
+
.footer{background:#f8fafc;border-top-color:#e2e8f0;}
|
|
239
|
+
.status-badge{border-color:#94a3b8 !important;color:#0f172a !important;background:#f1f5f9 !important;}
|
|
240
|
+
.print-btn{display:none !important;}
|
|
241
|
+
.status-bar{background:#f1f5f9 !important;}
|
|
242
|
+
}
|
|
243
|
+
</style>
|
|
244
|
+
</head>
|
|
245
|
+
<body>
|
|
246
|
+
|
|
247
|
+
<button class="print-btn no-print" onclick="window.print()">
|
|
248
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 01-2-2v-5a2 2 0 012-2h16a2 2 0 012 2v5a2 2 0 01-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
|
|
249
|
+
Print / Save PDF
|
|
250
|
+
</button>
|
|
251
|
+
|
|
252
|
+
<div class="page">
|
|
253
|
+
|
|
254
|
+
<!-- Header -->
|
|
255
|
+
<div class="header">
|
|
256
|
+
<div>
|
|
257
|
+
<div class="brand-name">immortal<span>.js</span></div>
|
|
258
|
+
<div class="brand-tagline">Invoice Service Pro v3 — Resilient Billing Platform</div>
|
|
259
|
+
</div>
|
|
260
|
+
<div class="invoice-meta">
|
|
261
|
+
<div class="invoice-number">${esc(invoice.number)}</div>
|
|
262
|
+
<div class="invoice-dates">
|
|
263
|
+
<strong>Issued:</strong> ${esc(invoice.issuedAt)}<br/>
|
|
264
|
+
<strong>Due:</strong> ${esc(invoice.dueAt)}
|
|
265
|
+
${invoice.paidAt ? `<br/><strong style="color:#10b981;">Paid:</strong> ${esc(invoice.paidAt)}` : ""}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<!-- Status Bar -->
|
|
271
|
+
<div class="status-bar">
|
|
272
|
+
<div style="display:flex;align-items:center;gap:20px;">
|
|
273
|
+
<span class="status-badge">${esc(ss.label)}</span>
|
|
274
|
+
<span class="currency-info">Currency: <strong>${esc(invoice.currency)}</strong> — ${esc(meta.name)}</span>
|
|
275
|
+
</div>
|
|
276
|
+
<div style="display:flex;align-items:center;gap:24px;">
|
|
277
|
+
${invoice.reference ? `<span class="invoice-ref">Ref: <span>${esc(invoice.reference)}</span></span>` : ""}
|
|
278
|
+
<code style="font-size:11px;color:#334155;background:#1e293b;padding:3px 10px;border-radius:6px;">${esc(invoice.id)}</code>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<!-- Parties -->
|
|
283
|
+
<div class="parties">
|
|
284
|
+
<div class="party">
|
|
285
|
+
<div class="party-label">Invoice From</div>
|
|
286
|
+
<div class="party-name">${esc(invoice.from.name)}</div>
|
|
287
|
+
<div class="party-detail">${esc(invoice.from.address)}<br/>${esc(invoice.from.city)}, ${esc(invoice.from.country)}</div>
|
|
288
|
+
${invoice.from.phone ? `<div class="party-detail">${esc(invoice.from.phone)}</div>` : ""}
|
|
289
|
+
<div class="party-email">${esc(invoice.from.email)}</div>
|
|
290
|
+
${invoice.from.taxId ? `<div class="party-tax">Tax ID: ${esc(invoice.from.taxId)}</div>` : ""}
|
|
291
|
+
</div>
|
|
292
|
+
<div class="party">
|
|
293
|
+
<div class="party-label">Bill To</div>
|
|
294
|
+
<div class="party-name">${esc(invoice.to.name)}</div>
|
|
295
|
+
<div class="party-detail">${esc(invoice.to.address)}<br/>${esc(invoice.to.city)}, ${esc(invoice.to.country)}</div>
|
|
296
|
+
${invoice.to.phone ? `<div class="party-detail">${esc(invoice.to.phone)}</div>` : ""}
|
|
297
|
+
<div class="party-email">${esc(invoice.to.email)}</div>
|
|
298
|
+
${invoice.to.taxId ? `<div class="party-tax">Tax ID: ${esc(invoice.to.taxId)}</div>` : ""}
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<!-- Line Items -->
|
|
303
|
+
<div class="table-section">
|
|
304
|
+
<div class="table-title">Line Items</div>
|
|
305
|
+
<table>
|
|
306
|
+
<thead>
|
|
307
|
+
<tr>
|
|
308
|
+
<th style="width:38%;">Description</th>
|
|
309
|
+
<th style="width:8%;text-align:center;">Qty</th>
|
|
310
|
+
<th style="width:14%;text-align:right;">Unit Price</th>
|
|
311
|
+
<th style="width:9%;text-align:center;">Tax</th>
|
|
312
|
+
<th style="width:9%;text-align:center;">Disc.</th>
|
|
313
|
+
<th style="width:16%;text-align:right;">Amount</th>
|
|
314
|
+
</tr>
|
|
315
|
+
</thead>
|
|
316
|
+
<tbody>${lineItemRows}</tbody>
|
|
317
|
+
</table>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<!-- Totals -->
|
|
321
|
+
<div class="totals-wrap">
|
|
322
|
+
<div class="totals">
|
|
323
|
+
<div class="totals-row">
|
|
324
|
+
<span class="totals-label">Subtotal</span>
|
|
325
|
+
<span class="totals-value">${totals.subtotal}</span>
|
|
326
|
+
</div>
|
|
327
|
+
${totals.discountCents > 0 ? `<div class="totals-row"><span class="totals-label" style="color:#f59e0b;">Discount</span><span class="totals-value" style="color:#f59e0b;">- ${totals.discount}</span></div>` : ""}
|
|
328
|
+
<div class="totals-row">
|
|
329
|
+
<span class="totals-label">Tax (${totals.effectiveTaxRate})</span>
|
|
330
|
+
<span class="totals-value">${totals.tax}</span>
|
|
331
|
+
</div>
|
|
332
|
+
<div class="totals-row grand">
|
|
333
|
+
<span class="totals-label">Total Due</span>
|
|
334
|
+
<span class="totals-value">${totals.total}</span>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
<!-- Notes & Terms -->
|
|
340
|
+
${(invoice.notes || invoice.terms) ? `
|
|
341
|
+
<div class="notes-section">
|
|
342
|
+
${invoice.notes ? `<div class="notes-block"><div class="notes-label">Notes</div><div class="notes-text">${esc(invoice.notes)}</div></div>` : "<div></div>"}
|
|
343
|
+
${invoice.terms ? `<div class="notes-block"><div class="notes-label">Terms & Conditions</div><div class="notes-text">${esc(invoice.terms)}</div></div>` : "<div></div>"}
|
|
344
|
+
</div>` : ""}
|
|
345
|
+
|
|
346
|
+
<!-- Footer -->
|
|
347
|
+
<div class="footer">
|
|
348
|
+
<div class="footer-left">Generated by <strong style="color:#38bdf8;">Immortal.js Invoice Service Pro v3</strong></div>
|
|
349
|
+
<div class="footer-right">
|
|
350
|
+
Generated: ${new Date().toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}<br/>
|
|
351
|
+
<span style="color:#1e293b;">Built with resilience. Zero downtime.</span>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
</div>
|
|
356
|
+
</body>
|
|
357
|
+
</html>`;
|
|
358
|
+
}
|