@narrow-os/document-templates 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/dist/index.js ADDED
@@ -0,0 +1,2482 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ A4_TEMPLATES: () => A4_TEMPLATES,
34
+ A4_TEMPLATE_IDS: () => A4_TEMPLATE_IDS,
35
+ DEFAULT_A4_ID: () => DEFAULT_A4_ID,
36
+ DEFAULT_COMPANY_INFO: () => DEFAULT_COMPANY_INFO,
37
+ DEFAULT_DOCUMENT_TYPE_SETTINGS: () => DEFAULT_DOCUMENT_TYPE_SETTINGS,
38
+ DEFAULT_RECEIPT_COMPANY_INFO: () => DEFAULT_RECEIPT_COMPANY_INFO,
39
+ DEFAULT_TAX_RATE: () => DEFAULT_TAX_RATE,
40
+ DEFAULT_THERMAL_ID: () => DEFAULT_THERMAL_ID,
41
+ DeliveryNoteGenerator: () => DeliveryNoteGenerator,
42
+ InvoiceReportGenerator: () => InvoiceReportGenerator,
43
+ QuotationReportGenerator: () => QuotationReportGenerator,
44
+ ReceiptGenerator: () => ReceiptGenerator,
45
+ THERMAL_TEMPLATES: () => THERMAL_TEMPLATES,
46
+ THERMAL_TEMPLATE_IDS: () => THERMAL_TEMPLATE_IDS,
47
+ escapeHtml: () => escapeHtml,
48
+ formatCompanyAddress: () => formatCompanyAddress,
49
+ formatCurrency: () => formatCurrency,
50
+ formatDate: () => formatDate,
51
+ generateBarcode: () => generateBarcode,
52
+ generateCode128Svg: () => generateCode128Svg,
53
+ generateDeliveryNoteHTML: () => generateDeliveryNoteHTML,
54
+ generateInvoiceHTML: () => generateInvoiceHTML,
55
+ generateQRCode: () => generateQRCode,
56
+ generateQuotationHTML: () => generateQuotationHTML,
57
+ getA4Template: () => getA4Template,
58
+ getClientDisplayName: () => getClientDisplayName,
59
+ getDocumentTypeSettings: () => getDocumentTypeSettings,
60
+ getThermalTemplate: () => getThermalTemplate,
61
+ hasVisibleText: () => hasVisibleText,
62
+ isA4TemplateId: () => isA4TemplateId,
63
+ isClassicTemplate: () => isClassicTemplate,
64
+ isThermalTemplateId: () => isThermalTemplateId,
65
+ receiptGenerator: () => receiptGenerator,
66
+ renderClassicHeader: () => renderClassicHeader,
67
+ renderContactBox: () => renderContactBox,
68
+ renderHeaderSection: () => renderHeaderSection
69
+ });
70
+ module.exports = __toCommonJS(index_exports);
71
+
72
+ // src/templates/a4.ts
73
+ var A4_TEMPLATES = [
74
+ {
75
+ id: "classic",
76
+ name: "Classic",
77
+ description: "Original Bizflow layout \u2014 green header, gold accent, Arial.",
78
+ primary: "#065F46",
79
+ accent: "#C5A005",
80
+ brand: "#990000",
81
+ headerBg: "#D1FAE5",
82
+ headerText: "#065F46",
83
+ totalsBg: "#ffffff",
84
+ totalsBorder: "#065F46",
85
+ rowAltBg: "#f9fafb",
86
+ border: "#e0e0e0",
87
+ borderFaint: "#f0f0f0",
88
+ surface: "#fdfdfd",
89
+ text: "#000000",
90
+ textMuted: "#555555",
91
+ fontFamily: "Arial, sans-serif",
92
+ fontFamilyHeading: "Arial, sans-serif",
93
+ fontSize: "12px",
94
+ borderRadius: "4px",
95
+ dividerStyle: "solid",
96
+ headerLayout: "split",
97
+ titleAlign: "right"
98
+ },
99
+ {
100
+ id: "modern",
101
+ name: "Modern",
102
+ description: "Sidebar header layout, sans-serif, large logo left.",
103
+ primary: "#0F172A",
104
+ accent: "#6366F1",
105
+ brand: "#4F46E5",
106
+ headerBg: "#F8FAFC",
107
+ headerText: "#0F172A",
108
+ totalsBg: "#F1F5F9",
109
+ totalsBorder: "#6366F1",
110
+ rowAltBg: "#F8FAFC",
111
+ border: "#E2E8F0",
112
+ borderFaint: "#F1F5F9",
113
+ surface: "#FFFFFF",
114
+ text: "#0F172A",
115
+ textMuted: "#475569",
116
+ fontFamily: "'Inter', 'Segoe UI', sans-serif",
117
+ fontFamilyHeading: "'Inter', 'Segoe UI', sans-serif",
118
+ fontSize: "12px",
119
+ borderRadius: "10px",
120
+ dividerStyle: "solid",
121
+ headerLayout: "sidebar",
122
+ titleAlign: "left"
123
+ },
124
+ {
125
+ id: "minimal",
126
+ name: "Minimal",
127
+ description: "Lots of whitespace, thin underlines only, monochrome.",
128
+ primary: "#111111",
129
+ accent: "#666666",
130
+ brand: "#111111",
131
+ headerBg: "#ffffff",
132
+ headerText: "#111111",
133
+ totalsBg: "#ffffff",
134
+ totalsBorder: "#111111",
135
+ rowAltBg: "#ffffff",
136
+ border: "#ffffff",
137
+ borderFaint: "#e5e5e5",
138
+ surface: "#ffffff",
139
+ text: "#111111",
140
+ textMuted: "#666666",
141
+ fontFamily: "'Helvetica Neue', Helvetica, Arial, sans-serif",
142
+ fontFamilyHeading: "'Helvetica Neue', Helvetica, Arial, sans-serif",
143
+ fontSize: "12px",
144
+ borderRadius: "0px",
145
+ dividerStyle: "solid",
146
+ headerLayout: "split",
147
+ titleAlign: "right"
148
+ },
149
+ {
150
+ id: "corporate",
151
+ name: "Corporate",
152
+ description: "Two-column symmetrical, navy tabular data, formal.",
153
+ primary: "#1E3A8A",
154
+ accent: "#1E40AF",
155
+ brand: "#0F172A",
156
+ headerBg: "#DBEAFE",
157
+ headerText: "#1E3A8A",
158
+ totalsBg: "#EFF6FF",
159
+ totalsBorder: "#1E3A8A",
160
+ rowAltBg: "#F8FAFC",
161
+ border: "#CBD5E1",
162
+ borderFaint: "#E2E8F0",
163
+ surface: "#FFFFFF",
164
+ text: "#0F172A",
165
+ textMuted: "#334155",
166
+ fontFamily: "'Segoe UI', Tahoma, Geneva, sans-serif",
167
+ fontFamilyHeading: "'Segoe UI', Tahoma, Geneva, sans-serif",
168
+ fontSize: "12px",
169
+ borderRadius: "4px",
170
+ dividerStyle: "solid",
171
+ headerLayout: "split",
172
+ titleAlign: "right"
173
+ },
174
+ {
175
+ id: "bold",
176
+ name: "Bold",
177
+ description: "Full-width header band, uppercase, strong colors.",
178
+ primary: "#0F172A",
179
+ accent: "#F59E0B",
180
+ brand: "#0F172A",
181
+ headerBg: "#0F172A",
182
+ headerText: "#FFFFFF",
183
+ totalsBg: "#FEF3C7",
184
+ totalsBorder: "#F59E0B",
185
+ rowAltBg: "#FFFBEB",
186
+ border: "#0F172A",
187
+ borderFaint: "#FDE68A",
188
+ surface: "#FFFFFF",
189
+ text: "#0F172A",
190
+ textMuted: "#52525B",
191
+ fontFamily: "'Arial Black', Arial, sans-serif",
192
+ fontFamilyHeading: "'Arial Black', Arial, sans-serif",
193
+ fontSize: "13px",
194
+ borderRadius: "0px",
195
+ dividerStyle: "solid",
196
+ headerLayout: "banded",
197
+ titleAlign: "left"
198
+ },
199
+ {
200
+ id: "elegant",
201
+ name: "Elegant",
202
+ description: "Serif headings, centered logo, gold accents.",
203
+ primary: "#7C2D12",
204
+ accent: "#B45309",
205
+ brand: "#78350F",
206
+ headerBg: "#FEF3C7",
207
+ headerText: "#7C2D12",
208
+ totalsBg: "#FFFBEB",
209
+ totalsBorder: "#B45309",
210
+ rowAltBg: "#FFFBEB",
211
+ border: "#FCD34D",
212
+ borderFaint: "#FDE68A",
213
+ surface: "#FFFFFF",
214
+ text: "#1C1917",
215
+ textMuted: "#57534E",
216
+ fontFamily: "Georgia, 'Times New Roman', serif",
217
+ fontFamilyHeading: "Georgia, 'Times New Roman', serif",
218
+ fontSize: "12px",
219
+ borderRadius: "6px",
220
+ dividerStyle: "solid",
221
+ headerLayout: "stacked",
222
+ titleAlign: "center"
223
+ },
224
+ {
225
+ id: "tech",
226
+ name: "Tech",
227
+ description: "Cool grays, monospace numbers, card-style totals.",
228
+ primary: "#0EA5E9",
229
+ accent: "#06B6D4",
230
+ brand: "#0F172A",
231
+ headerBg: "#F0F9FF",
232
+ headerText: "#0C4A6E",
233
+ totalsBg: "#F0F9FF",
234
+ totalsBorder: "#0EA5E9",
235
+ rowAltBg: "#F8FAFC",
236
+ border: "#BAE6FD",
237
+ borderFaint: "#E0F2FE",
238
+ surface: "#FFFFFF",
239
+ text: "#0F172A",
240
+ textMuted: "#475569",
241
+ fontFamily: "'Segoe UI', 'Roboto', sans-serif",
242
+ fontFamilyHeading: "'JetBrains Mono', 'Courier New', monospace",
243
+ fontSize: "12px",
244
+ borderRadius: "8px",
245
+ dividerStyle: "dashed",
246
+ headerLayout: "split",
247
+ titleAlign: "right"
248
+ },
249
+ {
250
+ id: "vibrant",
251
+ name: "Vibrant",
252
+ description: "Gradient header, magenta/teal palette.",
253
+ primary: "#DB2777",
254
+ accent: "#0D9488",
255
+ brand: "#831843",
256
+ headerBg: "#FCE7F3",
257
+ headerText: "#831843",
258
+ totalsBg: "#CCFBF1",
259
+ totalsBorder: "#0D9488",
260
+ rowAltBg: "#FDF2F8",
261
+ border: "#F9A8D4",
262
+ borderFaint: "#FBCFE8",
263
+ surface: "#FFFFFF",
264
+ text: "#500724",
265
+ textMuted: "#9D174D",
266
+ fontFamily: "'Trebuchet MS', 'Lucida Sans Unicode', sans-serif",
267
+ fontFamilyHeading: "'Trebuchet MS', 'Lucida Sans Unicode', sans-serif",
268
+ fontSize: "12px",
269
+ borderRadius: "12px",
270
+ dividerStyle: "solid",
271
+ headerLayout: "banded",
272
+ titleAlign: "center"
273
+ },
274
+ {
275
+ id: "compact",
276
+ name: "Compact",
277
+ description: "High info density, smaller fonts, optimized multi-page.",
278
+ primary: "#0F766E",
279
+ accent: "#0EA5E9",
280
+ brand: "#134E4A",
281
+ headerBg: "#CCFBF1",
282
+ headerText: "#134E4A",
283
+ totalsBg: "#F0FDFA",
284
+ totalsBorder: "#0F766E",
285
+ rowAltBg: "#F0FDFA",
286
+ border: "#5EEAD4",
287
+ borderFaint: "#99F6E4",
288
+ surface: "#FFFFFF",
289
+ text: "#134E4A",
290
+ textMuted: "#475569",
291
+ fontFamily: "'Inter', 'Helvetica', sans-serif",
292
+ fontFamilyHeading: "'Inter', 'Helvetica', sans-serif",
293
+ fontSize: "10.5px",
294
+ borderRadius: "3px",
295
+ dividerStyle: "dotted",
296
+ headerLayout: "split",
297
+ titleAlign: "right"
298
+ },
299
+ {
300
+ id: "mono",
301
+ name: "Mono",
302
+ description: "Pure black/white, ASCII-rules, terminal-style.",
303
+ primary: "#000000",
304
+ accent: "#000000",
305
+ brand: "#000000",
306
+ headerBg: "#FFFFFF",
307
+ headerText: "#000000",
308
+ totalsBg: "#FFFFFF",
309
+ totalsBorder: "#000000",
310
+ rowAltBg: "#FFFFFF",
311
+ border: "#000000",
312
+ borderFaint: "#000000",
313
+ surface: "#FFFFFF",
314
+ text: "#000000",
315
+ textMuted: "#000000",
316
+ fontFamily: "'Courier New', Courier, monospace",
317
+ fontFamilyHeading: "'Courier New', Courier, monospace",
318
+ fontSize: "12px",
319
+ borderRadius: "0px",
320
+ dividerStyle: "solid",
321
+ headerLayout: "stacked",
322
+ titleAlign: "left"
323
+ }
324
+ ];
325
+
326
+ // src/templates/thermal.ts
327
+ var THERMAL_TEMPLATES = [
328
+ {
329
+ id: "classic",
330
+ name: "Classic",
331
+ description: "Original 80mm thermal \u2014 centered courier.",
332
+ fontFamily: "'Courier New', monospace",
333
+ fontSize: "16px",
334
+ fontWeightBase: 600,
335
+ dividerStyle: "solid",
336
+ headerLayout: "centered",
337
+ itemSpacing: "normal",
338
+ totalsStyle: "bordered",
339
+ inkColor: "#000000",
340
+ accentColor: "#000000",
341
+ backgroundColor: "#FFFFFF"
342
+ },
343
+ {
344
+ id: "compact",
345
+ name: "Compact",
346
+ description: "Tighter spacing, smaller font \u2014 fit more on one roll.",
347
+ fontFamily: "'Courier New', monospace",
348
+ fontSize: "13px",
349
+ fontWeightBase: 500,
350
+ dividerStyle: "dashed",
351
+ headerLayout: "centered",
352
+ itemSpacing: "tight",
353
+ totalsStyle: "plain",
354
+ inkColor: "#000000",
355
+ accentColor: "#000000",
356
+ backgroundColor: "#FFFFFF"
357
+ },
358
+ {
359
+ id: "wide",
360
+ name: "Wide",
361
+ description: "Larger font, generous spacing \u2014 premium feel.",
362
+ fontFamily: "'Courier New', monospace",
363
+ fontSize: "18px",
364
+ fontWeightBase: 700,
365
+ dividerStyle: "double",
366
+ headerLayout: "centered",
367
+ itemSpacing: "loose",
368
+ totalsStyle: "accent",
369
+ inkColor: "#000000",
370
+ accentColor: "#000000",
371
+ backgroundColor: "#FFFFFF"
372
+ },
373
+ {
374
+ id: "dense",
375
+ name: "Dense",
376
+ description: "Dense info packing, minimal padding.",
377
+ fontFamily: "'Courier New', monospace",
378
+ fontSize: "14px",
379
+ fontWeightBase: 600,
380
+ dividerStyle: "solid",
381
+ headerLayout: "left-aligned",
382
+ itemSpacing: "tight",
383
+ totalsStyle: "bordered",
384
+ inkColor: "#000000",
385
+ accentColor: "#000000",
386
+ backgroundColor: "#FFFFFF"
387
+ },
388
+ {
389
+ id: "sparse",
390
+ name: "Sparse",
391
+ description: "Airy layout, dot dividers, easy to scan.",
392
+ fontFamily: "'Courier New', monospace",
393
+ fontSize: "15px",
394
+ fontWeightBase: 500,
395
+ dividerStyle: "dotted",
396
+ headerLayout: "centered",
397
+ itemSpacing: "loose",
398
+ totalsStyle: "plain",
399
+ inkColor: "#000000",
400
+ accentColor: "#000000",
401
+ backgroundColor: "#FFFFFF"
402
+ },
403
+ {
404
+ id: "boxed",
405
+ name: "Boxed",
406
+ description: "Header inside a heavy box, uppercase banner.",
407
+ fontFamily: "'Courier New', monospace",
408
+ fontSize: "16px",
409
+ fontWeightBase: 800,
410
+ dividerStyle: "solid",
411
+ headerLayout: "boxed",
412
+ itemSpacing: "normal",
413
+ totalsStyle: "bordered",
414
+ inkColor: "#000000",
415
+ accentColor: "#000000",
416
+ backgroundColor: "#FFFFFF"
417
+ },
418
+ {
419
+ id: "lined",
420
+ name: "Lined",
421
+ description: "Dashed dividers between every section.",
422
+ fontFamily: "'Courier New', monospace",
423
+ fontSize: "16px",
424
+ fontWeightBase: 600,
425
+ dividerStyle: "dashed",
426
+ headerLayout: "centered",
427
+ itemSpacing: "normal",
428
+ totalsStyle: "accent",
429
+ inkColor: "#000000",
430
+ accentColor: "#000000",
431
+ backgroundColor: "#FFFFFF"
432
+ },
433
+ {
434
+ id: "split",
435
+ name: "Split",
436
+ description: "Left-aligned header, two-column totals.",
437
+ fontFamily: "'Courier New', monospace",
438
+ fontSize: "16px",
439
+ fontWeightBase: 600,
440
+ dividerStyle: "solid",
441
+ headerLayout: "left-aligned",
442
+ itemSpacing: "normal",
443
+ totalsStyle: "bordered",
444
+ inkColor: "#000000",
445
+ accentColor: "#000000",
446
+ backgroundColor: "#FFFFFF"
447
+ },
448
+ {
449
+ id: "monobold",
450
+ name: "Mono Bold",
451
+ description: "Heaviest font weights, strong strike-through totals.",
452
+ fontFamily: "'Courier New', monospace",
453
+ fontSize: "16px",
454
+ fontWeightBase: 900,
455
+ dividerStyle: "double",
456
+ headerLayout: "centered",
457
+ itemSpacing: "normal",
458
+ totalsStyle: "accent",
459
+ inkColor: "#000000",
460
+ accentColor: "#000000",
461
+ backgroundColor: "#FFFFFF"
462
+ },
463
+ {
464
+ id: "elegant",
465
+ name: "Elegant",
466
+ description: "Serif accents via mixed font, soft dots.",
467
+ fontFamily: "Georgia, 'Times New Roman', serif",
468
+ fontSize: "15px",
469
+ fontWeightBase: 500,
470
+ dividerStyle: "dotted",
471
+ headerLayout: "centered",
472
+ itemSpacing: "loose",
473
+ totalsStyle: "plain",
474
+ inkColor: "#1C1917",
475
+ accentColor: "#7C2D12",
476
+ backgroundColor: "#FFFFFF"
477
+ }
478
+ ];
479
+
480
+ // src/templates/registry.ts
481
+ var DEFAULT_A4_ID = "classic";
482
+ var DEFAULT_THERMAL_ID = "classic";
483
+ var A4_TEMPLATE_IDS = A4_TEMPLATES.map((t) => t.id);
484
+ var THERMAL_TEMPLATE_IDS = THERMAL_TEMPLATES.map((t) => t.id);
485
+ function isA4TemplateId(value) {
486
+ return typeof value === "string" && A4_TEMPLATE_IDS.includes(value);
487
+ }
488
+ function isThermalTemplateId(value) {
489
+ return typeof value === "string" && THERMAL_TEMPLATE_IDS.includes(value);
490
+ }
491
+ function getA4Template(id) {
492
+ if (id && isA4TemplateId(id)) {
493
+ const found = A4_TEMPLATES.find((t) => t.id === id);
494
+ if (found) return found;
495
+ }
496
+ return A4_TEMPLATES.find((t) => t.id === DEFAULT_A4_ID) ?? A4_TEMPLATES[0];
497
+ }
498
+ function getThermalTemplate(id) {
499
+ if (id && isThermalTemplateId(id)) {
500
+ const found = THERMAL_TEMPLATES.find((t) => t.id === id);
501
+ if (found) return found;
502
+ }
503
+ return THERMAL_TEMPLATES.find((t) => t.id === DEFAULT_THERMAL_ID) ?? THERMAL_TEMPLATES[0];
504
+ }
505
+ function isClassicTemplate(id) {
506
+ return !id || id === DEFAULT_A4_ID || id === DEFAULT_THERMAL_ID;
507
+ }
508
+
509
+ // src/templates/contact-box.ts
510
+ function esc(value) {
511
+ if (value === null || value === void 0) return "";
512
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
513
+ }
514
+ function trimAll(s) {
515
+ return s.replace(/\s+/g, " ").trim();
516
+ }
517
+ function renderContactBox(input) {
518
+ const escape = esc;
519
+ const name = trimAll(input.name || "");
520
+ const fullName = input.fullName && trimAll(input.fullName) && trimAll(input.fullName) !== name ? trimAll(input.fullName || "") : "";
521
+ const taxNumber = trimAll(input.taxNumber || "");
522
+ const regNumber = trimAll(input.registrationNumber || "");
523
+ const address = trimAll(input.address || "");
524
+ const city = trimAll(input.city || "");
525
+ const postalCode = trimAll(input.postalCode || "");
526
+ const province = trimAll(input.province || "");
527
+ const phone = trimAll(input.phone || "");
528
+ const phone2 = trimAll(input.phone2 || "");
529
+ const email = trimAll(input.email || "");
530
+ const website = trimAll(input.website || "");
531
+ return `
532
+ <div class="client-box-label">${escape(input.label)}</div>
533
+ <div class="client-box">
534
+ <div class="client-name">${escape(name)}</div>
535
+ ${fullName ? `${escape(fullName)}<br>` : ""}
536
+ ${taxNumber ? `VAT/Tax No: ${escape(taxNumber)}<br>` : ""}
537
+ ${regNumber ? `Reg No: ${escape(regNumber)}<br>` : ""}
538
+ ${address ? `${escape(address)}<br>` : ""}
539
+ ${city || postalCode ? `${escape(city)} ${escape(postalCode)}<br>` : ""}
540
+ ${province ? `${escape(province)}<br>` : ""}
541
+ ${phone ? `${escape(phone)}<br>` : ""}
542
+ ${phone2 ? `${escape(phone2)}<br>` : ""}
543
+ ${email ? `${escape(email)}` : ""}
544
+ ${website ? `<br>Website: ${escape(website)}` : ""}
545
+ </div>
546
+ `;
547
+ }
548
+
549
+ // src/templates/sections.ts
550
+ function barcodeQrBlock(ctx, extraClass = "") {
551
+ const hasCode = ctx.settings.showBarcode && ctx.barcodeDataUrl || ctx.settings.showQRCode && ctx.qrDataUrl;
552
+ if (!hasCode) return "";
553
+ const t = ctx.template;
554
+ switch (ctx.templateId) {
555
+ // ---- modern: side-by-side flex card ----
556
+ case "modern":
557
+ return `<div class="barcode-qr-modern ${extraClass}" style="display:flex;gap:14px;align-items:flex-start;margin-top:14px;padding:10px 12px;background:${t.surface};border:1px solid ${t.borderFaint};border-radius:${t.borderRadius};">
558
+ ${ctx.settings.showBarcode && ctx.barcodeDataUrl ? `<img src="${ctx.barcodeDataUrl}" style="height:46px;flex-shrink:0;" alt="Barcode"/>` : ""}
559
+ ${ctx.settings.showQRCode && ctx.qrDataUrl ? `<img src="${ctx.qrDataUrl}" style="width:82px;height:82px;flex-shrink:0;" alt="QR Code"/>` : ""}
560
+ </div>`;
561
+ // ---- minimal: underline separator, compact ----
562
+ case "minimal":
563
+ return `<div class="barcode-qr-minimal ${extraClass}" style="margin-top:14px;padding-top:10px;border-top:1px solid ${t.borderFaint};">
564
+ ${ctx.settings.showBarcode && ctx.barcodeDataUrl ? `<img src="${ctx.barcodeDataUrl}" style="height:40px;display:block;margin-bottom:6px;" alt="Barcode"/>` : ""}
565
+ ${ctx.settings.showQRCode && ctx.qrDataUrl ? `<img src="${ctx.qrDataUrl}" style="width:70px;height:70px;" alt="QR Code"/>` : ""}
566
+ </div>`;
567
+ // ---- corporate: boxed with headerBg ----
568
+ case "corporate":
569
+ return `<div class="barcode-qr-corporate ${extraClass}" style="margin-top:14px;padding:12px 14px;background:${t.headerBg};border:1.5px solid ${t.border};border-radius:${t.borderRadius};">
570
+ ${ctx.settings.showBarcode && ctx.barcodeDataUrl ? `<img src="${ctx.barcodeDataUrl}" style="height:46px;display:block;margin-bottom:8px;" alt="Barcode"/>` : ""}
571
+ ${ctx.settings.showQRCode && ctx.qrDataUrl ? `<img src="${ctx.qrDataUrl}" style="width:88px;height:88px;" alt="QR Code"/>` : ""}
572
+ </div>`;
573
+ // ---- bold: thick accent border, large codes ----
574
+ case "bold":
575
+ return `<div class="barcode-qr-bold ${extraClass}" style="margin-top:14px;padding:14px;border:3px solid ${t.accent};border-radius:${t.borderRadius};background:${t.surface};">
576
+ ${ctx.settings.showBarcode && ctx.barcodeDataUrl ? `<img src="${ctx.barcodeDataUrl}" style="height:58px;display:block;margin-bottom:10px;" alt="Barcode"/>` : ""}
577
+ ${ctx.settings.showQRCode && ctx.qrDataUrl ? `<img src="${ctx.qrDataUrl}" style="width:105px;height:105px;" alt="QR Code"/>` : ""}
578
+ </div>`;
579
+ // ---- elegant: italic 'Verification' label, side-by-side ----
580
+ case "elegant":
581
+ return `<div class="barcode-qr-elegant ${extraClass}" style="margin-top:16px;padding-top:10px;border-top:1px solid ${t.borderFaint};">
582
+ <div style="font-style:italic;font-size:9px;color:${t.textMuted};margin-bottom:6px;letter-spacing:0.5px;">Verification</div>
583
+ <div style="display:flex;gap:14px;align-items:flex-start;">
584
+ ${ctx.settings.showBarcode && ctx.barcodeDataUrl ? `<img src="${ctx.barcodeDataUrl}" style="height:44px;" alt="Barcode"/>` : ""}
585
+ ${ctx.settings.showQRCode && ctx.qrDataUrl ? `<img src="${ctx.qrDataUrl}" style="width:78px;height:78px;" alt="QR Code"/>` : ""}
586
+ </div>
587
+ </div>`;
588
+ // ---- tech: dark card with inverted barcode ----
589
+ case "tech":
590
+ return `<div class="barcode-qr-tech ${extraClass}" style="margin-top:14px;padding:12px;background:#1a1a2e;border-radius:${t.borderRadius};display:inline-block;">
591
+ ${ctx.settings.showBarcode && ctx.barcodeDataUrl ? `<img src="${ctx.barcodeDataUrl}" style="height:44px;display:block;margin-bottom:6px;filter:invert(1) hue-rotate(180deg);" alt="Barcode"/>` : ""}
592
+ ${ctx.settings.showQRCode && ctx.qrDataUrl ? `<img src="${ctx.qrDataUrl}" style="width:85px;height:85px;border-radius:6px;" alt="QR Code"/>` : ""}
593
+ </div>`;
594
+ // ---- vibrant: dashed colored border box ----
595
+ case "vibrant":
596
+ return `<div class="barcode-qr-vibrant ${extraClass}" style="margin-top:14px;padding:12px 14px;border:2.5px dashed ${t.accent};border-radius:${t.borderRadius};background:${t.headerBg}22;">
597
+ ${ctx.settings.showBarcode && ctx.barcodeDataUrl ? `<img src="${ctx.barcodeDataUrl}" style="height:50px;display:block;margin-bottom:6px;" alt="Barcode"/>` : ""}
598
+ ${ctx.settings.showQRCode && ctx.qrDataUrl ? `<img src="${ctx.qrDataUrl}" style="width:95px;height:95px;" alt="QR Code"/>` : ""}
599
+ </div>`;
600
+ // ---- compact: tiny inline ----
601
+ case "compact":
602
+ return `<div class="barcode-qr-compact ${extraClass}" style="margin-top:10px;display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
603
+ ${ctx.settings.showBarcode && ctx.barcodeDataUrl ? `<img src="${ctx.barcodeDataUrl}" style="height:34px;" alt="Barcode"/>` : ""}
604
+ ${ctx.settings.showQRCode && ctx.qrDataUrl ? `<img src="${ctx.qrDataUrl}" style="width:56px;height:56px;" alt="QR Code"/>` : ""}
605
+ </div>`;
606
+ // ---- mono: grayscale, strict double border ----
607
+ case "mono":
608
+ return `<div class="barcode-qr-mono ${extraClass}" style="margin-top:14px;padding:12px;border:2px solid #333;background:#fafafa;border-radius:0;">
609
+ ${ctx.settings.showBarcode && ctx.barcodeDataUrl ? `<img src="${ctx.barcodeDataUrl}" style="height:44px;display:block;margin-bottom:6px;filter:grayscale(100%);" alt="Barcode"/>` : ""}
610
+ ${ctx.settings.showQRCode && ctx.qrDataUrl ? `<img src="${ctx.qrDataUrl}" style="width:82px;height:82px;filter:grayscale(100%);" alt="QR Code"/>` : ""}
611
+ </div>`;
612
+ // ---- classic: stacked vertically, standard ----
613
+ default:
614
+ return `<div class="barcode-qr-classic ${extraClass}" style="margin-top:10px;">
615
+ ${ctx.settings.showBarcode && ctx.barcodeDataUrl ? `<img src="${ctx.barcodeDataUrl}" style="max-width:200px;height:auto;display:block;margin-bottom:8px;" alt="Barcode"/>` : ""}
616
+ ${ctx.settings.showQRCode && ctx.qrDataUrl ? `<img src="${ctx.qrDataUrl}" style="max-width:120px;height:auto;" alt="QR Code"/>` : ""}
617
+ </div>`;
618
+ }
619
+ }
620
+ var renderModernHeader = (ctx) => {
621
+ const t = ctx.template;
622
+ const contactLines = [
623
+ ctx.settings.showPhone && ctx.cPhone ? ctx.cPhone : "",
624
+ ctx.settings.showPhone && ctx.cPhone2 ? ctx.cPhone2 : "",
625
+ ctx.settings.showEmail && ctx.cEmail ? ctx.cEmail : "",
626
+ ctx.settings.showWebsite && ctx.cWeb ? ctx.cWeb : ""
627
+ ].filter(Boolean).join("<br>");
628
+ return `<div class="row invoice-${ctx.templateId}">
629
+ <!-- full-width header band with left accent stripe -->
630
+ <div style="display:flex;width:100%;">
631
+ <div style="width:6px;background:${t.accent};flex-shrink:0;border-radius:${t.borderRadius} 0 0 ${t.borderRadius};"></div>
632
+ <div style="flex:1;padding:18px 22px;background:${t.headerBg};">
633
+ <div style="display:flex;align-items:center;gap:16px;">
634
+ ${ctx.logo ? `<img src="${ctx.logo}" style="height:52px;max-width:120px;object-fit:contain;"/>` : `<div style="font-family:${t.fontFamilyHeading};font-size:18px;font-weight:700;color:${t.text};letter-spacing:-0.3px;">${ctx.cName}</div>`}
635
+ <div style="flex:1;"></div>
636
+ <div style="font-family:${t.fontFamilyHeading};font-size:34px;font-weight:800;color:${t.headerText || t.text};letter-spacing:-1px;text-transform:uppercase;">${ctx.docTitle}</div>
637
+ </div>
638
+ <div style="display:flex;gap:32px;margin-top:12px;font-size:10.5px;color:${t.textMuted};">
639
+ <div>
640
+ <div style="font-weight:600;color:${t.text};margin-bottom:2px;">${ctx.cName}</div>
641
+ ${ctx.cAddress ? `<div>${ctx.cAddress}</div>` : ""}
642
+ ${ctx.cCity || ctx.cCode ? `<div>${ctx.cCity} ${ctx.cCode}</div>` : ""}
643
+ ${ctx.cProv ? `<div>${ctx.cProv}</div>` : ""}
644
+ ${ctx.settings.showTaxNumber && ctx.cVat ? `<div style="margin-top:3px;">VAT No.: ${ctx.cVat}</div>` : ""}
645
+ </div>
646
+ <div>
647
+ ${contactLines ? `<div style="white-space:pre-line;">${contactLines}</div>` : ""}
648
+ ${barcodeQrBlock(ctx)}
649
+ </div>
650
+ </div>
651
+ </div>
652
+ </div>
653
+ </div>`;
654
+ };
655
+ var renderMinimalHeader = (ctx) => {
656
+ const t = ctx.template;
657
+ return `<div class="row invoice-${ctx.templateId}">
658
+ <div style="text-align:center;padding:28px 0 10px;">
659
+ ${ctx.logo ? `<img src="${ctx.logo}" style="height:44px;margin:0 auto 10px;display:block;"/>` : ""}
660
+ ${!ctx.logo ? `<div style="font-family:${t.fontFamilyHeading};font-size:13px;font-weight:600;color:${t.text};letter-spacing:2px;text-transform:uppercase;margin-bottom:12px;">${ctx.cName}</div>` : ""}
661
+ <div style="font-family:${t.fontFamilyHeading};font-size:24px;letter-spacing:3px;color:${t.text};text-transform:uppercase;font-weight:300;">${ctx.docTitle}</div>
662
+ <div style="font-size:10px;color:${t.textMuted};margin-top:4px;">${ctx.docNumber}</div>
663
+ </div>
664
+ <div style="display:flex;justify-content:space-between;align-items:flex-start;border-top:1px solid ${t.borderFaint};border-bottom:1px solid ${t.borderFaint};padding:10px 0;margin-top:8px;">
665
+ <div style="font-size:9.5px;color:${t.textMuted};line-height:1.6;text-transform:uppercase;letter-spacing:0.4px;">
666
+ <div style="font-weight:700;color:${t.text};margin-bottom:2px;">From:</div>
667
+ <strong>${ctx.cName}</strong><br/>
668
+ ${ctx.cAddress ? `${ctx.cAddress}<br/>` : ""}
669
+ ${ctx.cCity} ${ctx.cCode}<br/>
670
+ ${ctx.cProv}
671
+ ${ctx.settings.showTaxNumber && ctx.cVat ? `<br/>VAT No.: ${ctx.cVat}` : ""}
672
+ </div>
673
+ <div style="text-align:right;font-size:9.5px;color:${t.textMuted};line-height:1.6;text-transform:uppercase;letter-spacing:0.4px;">
674
+ ${ctx.settings.showPhone && ctx.cPhone ? `<div>T: ${ctx.cPhone}</div>` : ""}
675
+ ${ctx.settings.showEmail && ctx.cEmail ? `<div>E: ${ctx.cEmail}</div>` : ""}
676
+ ${ctx.settings.showWebsite && ctx.cWeb ? `<div>W: ${ctx.cWeb}</div>` : ""}
677
+ ${barcodeQrBlock(ctx)}
678
+ </div>
679
+ </div>
680
+ </div>`;
681
+ };
682
+ var renderCorporateHeader = (ctx) => {
683
+ const t = ctx.template;
684
+ const contactDetailBlock = [
685
+ ctx.settings.showPhone && ctx.cPhone ? `<div style="display:flex;gap:6px;align-items:baseline;"><span style="font-weight:600;">Tel:</span><span>${ctx.cPhone}</span></div>` : "",
686
+ ctx.settings.showPhone && ctx.cPhone2 ? `<div style="display:flex;gap:6px;align-items:baseline;"><span style="font-weight:600;">Tel:</span><span>${ctx.cPhone2}</span></div>` : "",
687
+ ctx.settings.showEmail && ctx.cEmail ? `<div style="display:flex;gap:6px;align-items:baseline;"><span style="font-weight:600;">Email:</span><span>${ctx.cEmail}</span></div>` : "",
688
+ ctx.settings.showWebsite && ctx.cWeb ? `<div style="display:flex;gap:6px;align-items:baseline;"><span style="font-weight:600;">Web:</span><span>${ctx.cWeb}</span></div>` : ""
689
+ ].filter(Boolean).join("");
690
+ return `<div class="row invoice-${ctx.templateId}">
691
+ <div style="border:2px solid ${t.border};border-radius:${t.borderRadius};overflow:hidden;">
692
+ <!-- top boxed header -->
693
+ <div style="background:${t.headerBg};padding:18px 22px;display:flex;align-items:center;gap:16px;border-bottom:2px solid ${t.border};">
694
+ ${ctx.logo ? `<img src="${ctx.logo}" style="height:56px;max-width:130px;object-fit:contain;"/>` : `<div style="font-family:${t.fontFamilyHeading};font-size:17px;font-weight:700;color:${t.text};">${ctx.cName}</div>`}
695
+ <div style="flex:1;"></div>
696
+ <div style="font-family:${t.fontFamilyHeading};font-size:30px;font-weight:700;color:${t.headerText || t.text};letter-spacing:0.5px;">${ctx.docTitle}</div>
697
+ </div>
698
+ <div style="display:flex;gap:0;padding:14px 22px;background:${t.surface};">
699
+ <div style="flex:1;padding-right:20px;border-right:1px solid ${t.borderFaint};">
700
+ <div style="font-size:9px;text-transform:uppercase;letter-spacing:0.8px;color:${t.textMuted};margin-bottom:4px;font-weight:600;">Registered Address</div>
701
+ <div style="font-size:11px;color:${t.text};line-height:1.6;">
702
+ <strong>${ctx.cName}</strong><br/>
703
+ ${ctx.cAddress}<br/>
704
+ ${ctx.cCity} ${ctx.cCode}<br/>
705
+ ${ctx.cProv}
706
+ ${ctx.settings.showTaxNumber && ctx.cVat ? `<br/><span style="font-weight:600;">VAT No.: ${ctx.cVat}</span>` : ""}
707
+ </div>
708
+ </div>
709
+ <div style="flex:1;padding-left:20px;">
710
+ <div style="font-size:9px;text-transform:uppercase;letter-spacing:0.8px;color:${t.textMuted};margin-bottom:6px;font-weight:600;">Contact</div>
711
+ <div style="font-size:11px;color:${t.text};line-height:1.8;">${contactDetailBlock}</div>
712
+ ${barcodeQrBlock(ctx)}
713
+ </div>
714
+ </div>
715
+ </div>
716
+ </div>`;
717
+ };
718
+ var renderBoldHeader = (ctx) => {
719
+ const t = ctx.template;
720
+ return `<div class="row invoice-${ctx.templateId}">
721
+ <!-- huge banner header -->
722
+ <div style="background:linear-gradient(135deg, ${t.headerBg} 0%, ${t.surface} 100%);padding:22px 26px;border-bottom:4px solid ${t.accent};">
723
+ <div style="font-family:${t.fontFamilyHeading};font-size:38px;font-weight:900;color:${t.headerText || t.text};letter-spacing:-1px;line-height:1;">
724
+ ${ctx.cName.toUpperCase()}
725
+ </div>
726
+ ${ctx.cAddress || ctx.cCity ? `<div style="font-size:11px;color:${t.textMuted};margin-top:4px;">${ctx.cAddress} \xB7 ${ctx.cCity} ${ctx.cCode} \xB7 ${ctx.cProv}</div>` : ""}
727
+ </div>
728
+ <div style="display:flex;gap:0;padding:16px 0;align-items:center;">
729
+ <div style="flex:1;">
730
+ ${ctx.settings.showTaxNumber && ctx.cVat ? `<div style="font-size:10px;color:${t.textMuted};"><strong style="color:${t.text};">VAT No.:</strong> ${ctx.cVat}</div>` : ""}
731
+ ${ctx.settings.showPhone && ctx.cPhone ? `<div style="font-size:10px;color:${t.textMuted};margin-top:2px;"><strong style="color:${t.text};">T:</strong> ${ctx.cPhone}${ctx.cPhone2 ? ` / ${ctx.cPhone2}` : ""}</div>` : ""}
732
+ ${ctx.settings.showEmail && ctx.cEmail ? `<div style="font-size:10px;color:${t.textMuted};margin-top:2px;"><strong style="color:${t.text};">E:</strong> ${ctx.cEmail}</div>` : ""}
733
+ ${ctx.settings.showWebsite && ctx.cWeb ? `<div style="font-size:10px;color:${t.textMuted};margin-top:2px;"><strong style="color:${t.text};">W:</strong> ${ctx.cWeb}</div>` : ""}
734
+ </div>
735
+ <div style="font-family:${t.fontFamilyHeading};font-size:44px;font-weight:900;color:${t.headerText || t.text};background:${t.accent};color:#fff;padding:8px 28px;border-radius:4px;letter-spacing:1px;">${ctx.docTitle}</div>
736
+ </div>
737
+ <div style="border-top:3px solid ${t.accent};padding-top:10px;margin-top:2px;">
738
+ <div style="font-size:11px;color:${t.textMuted};">No: <strong style="color:${t.text};">${ctx.docNumber}</strong></div>
739
+ </div>
740
+ </div>`;
741
+ };
742
+ var renderElegantHeader = (ctx) => {
743
+ const t = ctx.template;
744
+ return `<div class="row invoice-${ctx.templateId}">
745
+ <div style="text-align:center;padding:24px 0 8px;">
746
+ ${ctx.logo ? `<img src="${ctx.logo}" style="height:50px;margin:0 auto 10px;display:block;"/>` : ""}
747
+ ${!ctx.logo ? `<div style="font-family:${t.fontFamilyHeading};font-size:20px;font-style:italic;font-weight:500;color:${t.text};margin-bottom:4px;">${ctx.cName}</div>` : ""}
748
+ <div style="font-family:${t.fontFamilyHeading};font-size:28px;font-style:italic;color:${t.text};font-weight:400;letter-spacing:1px;">${ctx.docTitle}</div>
749
+ </div>
750
+ <div style="display:flex;justify-content:center;gap:40px;padding:12px 0;border-top:1px solid ${t.borderFaint};border-bottom:1px solid ${t.borderFaint};margin-top:6px;font-size:10.5px;color:${t.textMuted};">
751
+ <div style="text-align:center;line-height:1.7;">
752
+ <div style="font-family:${t.fontFamilyHeading};">${ctx.cName}<br/>${ctx.cAddress}<br/>${ctx.cCity} ${ctx.cCode}<br/>${ctx.cProv}</div>
753
+ ${ctx.settings.showTaxNumber && ctx.cVat ? `<div style="margin-top:3px;">VAT Reg. No. ${ctx.cVat}</div>` : ""}
754
+ </div>
755
+ <div style="text-align:center;line-height:1.7;">
756
+ ${ctx.settings.showPhone && ctx.cPhone ? `${ctx.cPhone}<br/>` : ""}
757
+ ${ctx.settings.showEmail && ctx.cEmail ? ctx.cEmail : ""}
758
+ </div>
759
+ <div style="text-align:center;">
760
+ ${ctx.cName ? `<div style="font-weight:600;color:${t.text};">${ctx.docNumber}</div>` : ""}
761
+ </div>
762
+ </div>
763
+ ${barcodeQrBlock(ctx, "margin:0 auto;display:table;")}
764
+ </div>`;
765
+ };
766
+ var renderTechHeader = (ctx) => {
767
+ const t = ctx.template;
768
+ return `<div class="row invoice-${ctx.templateId}">
769
+ <div style="background:#1a1a2e;padding:14px 22px;display:flex;align-items:center;gap:16px;border-radius:${t.borderRadius} ${t.borderRadius} 0 0;">
770
+ <div style="flex:1;">
771
+ <div style="font-family:monospace;font-size:13px;font-weight:700;color:#e2e8f0;letter-spacing:0.5px;">${ctx.cName}</div>
772
+ <div style="font-family:monospace;font-size:9.5px;color:#94a3b8;margin-top:2px;">${ctx.cAddress} \xB7 ${ctx.cCity} ${ctx.cCode} \xB7 ${ctx.cProv}</div>
773
+ </div>
774
+ <div style="font-family:monospace;font-size:22px;font-weight:700;color:#fff;border-bottom:2px solid ${t.accent};letter-spacing:1px;">// ${ctx.docTitle}</div>
775
+ </div>
776
+ <div style="display:flex;border:1px solid ${t.borderFaint};border-top:none;border-radius:0 0 ${t.borderRadius} ${t.borderRadius};overflow:hidden;">
777
+ <div style="flex:1;padding:14px 18px;background:${t.surface};border-right:1px solid ${t.borderFaint};">
778
+ <div style="font-family:monospace;font-size:9px;color:${t.textMuted};text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">ORG_DETAILS</div>
779
+ ${ctx.settings.showTaxNumber && ctx.cVat ? `<div style="font-family:monospace;font-size:11px;color:${t.text};">VAT No.: <span style="color:${t.accent};">${ctx.cVat}</span></div>` : ""}
780
+ ${ctx.settings.showPhone && ctx.cPhone ? `<div style="font-family:monospace;font-size:11px;color:${t.text};">TEL: ${ctx.cPhone}</div>` : ""}
781
+ ${ctx.settings.showEmail && ctx.cEmail ? `<div style="font-family:monospace;font-size:11px;color:${t.text};">EMAIL: ${ctx.cEmail}</div>` : ""}
782
+ ${ctx.settings.showWebsite && ctx.cWeb ? `<div style="font-family:monospace;font-size:11px;color:${t.text};">WEB: ${ctx.cWeb}</div>` : ""}
783
+ ${barcodeQrBlock(ctx)}
784
+ </div>
785
+ <div style="width:160px;padding:14px 16px;background:${t.headerBg};">
786
+ <div style="font-family:monospace;font-size:9px;color:${t.textMuted};text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">REF</div>
787
+ <div style="font-family:monospace;font-size:13px;font-weight:700;color:${t.text};">${ctx.docNumber}</div>
788
+ </div>
789
+ </div>
790
+ </div>`;
791
+ };
792
+ var renderVibrantHeader = (ctx) => {
793
+ const t = ctx.template;
794
+ return `<div class="row invoice-${ctx.templateId}">
795
+ <div style="background:linear-gradient(135deg,${t.primary} 0%,${t.accent} 100%);padding:20px 26px;border-radius:${t.borderRadius};">
796
+ <div style="display:flex;align-items:center;gap:16px;margin-bottom:14px;">
797
+ ${ctx.logo ? `<img src="${ctx.logo}" style="height:52px;max-width:110px;object-fit:contain;"/>` : ""}
798
+ ${!ctx.logo ? `<div style="font-family:${t.fontFamilyHeading};font-size:21px;font-weight:800;color:#fff;letter-spacing:-0.3px;text-shadow:0 1px 4px rgba(0,0,0,0.15);">${ctx.cName}</div>` : ""}
799
+ <div style="flex:1;"></div>
800
+ <div style="background:#fff;color:${t.primary};font-family:${t.fontFamilyHeading};font-size:28px;font-weight:900;padding:6px 26px;border-radius:50px;letter-spacing:1px;box-shadow:0 2px 8px rgba(0,0,0,0.1);">${ctx.docTitle}</div>
801
+ </div>
802
+ <div style="display:flex;gap:24px;color:#fff;font-size:10.5px;opacity:0.95;">
803
+ <div style="line-height:1.6;">
804
+ <strong>${ctx.cName}</strong><br/>
805
+ ${ctx.cAddress || ""}${ctx.cAddress ? "<br/>" : ""}
806
+ ${ctx.cCity} ${ctx.cCode}<br/>
807
+ ${ctx.cProv}
808
+ ${ctx.settings.showTaxNumber && ctx.cVat ? `<br/><strong>VAT No.:</strong> ${ctx.cVat}` : ""}
809
+ </div>
810
+ <div style="line-height:1.8;">
811
+ ${ctx.settings.showPhone && ctx.cPhone ? `<div>${ctx.cPhone}</div>` : ""}
812
+ ${ctx.settings.showEmail && ctx.cEmail ? `<div>${ctx.cEmail}</div>` : ""}
813
+ ${ctx.settings.showWebsite && ctx.cWeb ? `<div>${ctx.cWeb}</div>` : ""}
814
+ </div>
815
+ <div style="margin-left:auto;">${barcodeQrBlock(ctx)}</div>
816
+ </div>
817
+ </div>
818
+ </div>`;
819
+ };
820
+ var renderCompactHeader = (ctx) => {
821
+ const t = ctx.template;
822
+ return `<div class="row invoice-${ctx.templateId}">
823
+ <div style="display:flex;gap:14px;align-items:flex-start;padding:10px 0;">
824
+ <div style="flex-shrink:0;">
825
+ ${ctx.logo ? `<img src="${ctx.logo}" style="height:38px;"/>` : ""}
826
+ </div>
827
+ <div style="flex:1;line-height:1.5;">
828
+ <div style="font-family:${t.fontFamilyHeading};font-size:14px;font-weight:700;color:${t.text};">${ctx.cName}</div>
829
+ <div style="font-size:9.5px;color:${t.textMuted};">${ctx.cAddress} \xB7 ${ctx.cCity} ${ctx.cCode} \xB7 ${ctx.cProv}</div>
830
+ <div style="font-size:9px;color:${t.textMuted};margin-top:2px;">
831
+ ${ctx.settings.showTaxNumber && ctx.cVat ? `VAT: ${ctx.cVat} \xB7 ` : ""}
832
+ ${ctx.settings.showPhone && ctx.cPhone ? `T: ${ctx.cPhone}` : ""}
833
+ </div>
834
+ </div>
835
+ <div style="font-family:${t.fontFamilyHeading};font-size:24px;font-weight:800;color:${t.text};white-space:nowrap;">${ctx.docTitle}</div>
836
+ </div>
837
+ ${barcodeQrBlock(ctx, "margin-top:6px;")}
838
+ </div>`;
839
+ };
840
+ var renderMonoHeader = (ctx) => {
841
+ const t = ctx.template;
842
+ return `<div class="row invoice-${ctx.templateId}">
843
+ <div style="border:3px double ${t.text};padding:0;border-radius:0;">
844
+ <div style="display:flex;align-items:center;gap:14px;padding:14px 18px;">
845
+ <div style="flex:1;">
846
+ ${ctx.logo ? `<img src="${ctx.logo}" style="height:44px;filter:grayscale(100%);"/>` : ""}
847
+ ${!ctx.logo ? `<div style="font-family:monospace;font-size:14px;font-weight:700;color:${t.text};text-transform:uppercase;letter-spacing:1px;">${ctx.cName}</div>` : ""}
848
+ <div style="font-family:monospace;font-size:9px;color:${t.textMuted};margin-top:3px;text-transform:uppercase;letter-spacing:0.5px;">${ctx.cAddress} \xB7 ${ctx.cCity} ${ctx.cCode} \xB7 ${ctx.cProv}</div>
849
+ </div>
850
+ <div style="font-family:monospace;font-size:20px;font-weight:700;color:${t.text};letter-spacing:3px;border:2px solid ${t.text};padding:6px 18px;">${ctx.docTitle}</div>
851
+ </div>
852
+ <div style="border-top:2px solid ${t.text};display:flex;gap:0;font-family:monospace;font-size:9.5px;text-transform:uppercase;letter-spacing:0.5px;">
853
+ ${ctx.settings.showTaxNumber && ctx.cVat ? `<div style="flex:1;padding:8px 18px;border-right:1px solid ${t.text};">VAT No.: ${ctx.cVat}</div>` : `<div style="flex:1;"></div>`}
854
+ ${ctx.settings.showPhone && ctx.cPhone ? `<div style="flex:1;padding:8px 14px;border-right:1px solid ${t.text};">T: ${ctx.cPhone}</div>` : `<div style="flex:1;"></div>`}
855
+ ${ctx.settings.showEmail && ctx.cEmail ? `<div style="flex:1;padding:8px 14px;">E: ${ctx.cEmail}</div>` : `<div style="flex:1;"></div>`}
856
+ </div>
857
+ </div>
858
+ <div style="display:flex;justify-content:flex-end;margin-top:10px;">${barcodeQrBlock(ctx)}</div>
859
+ </div>`;
860
+ };
861
+ function renderHeaderSection(ctx) {
862
+ switch (ctx.templateId) {
863
+ case "modern":
864
+ return renderModernHeader(ctx);
865
+ case "minimal":
866
+ return renderMinimalHeader(ctx);
867
+ case "corporate":
868
+ return renderCorporateHeader(ctx);
869
+ case "bold":
870
+ return renderBoldHeader(ctx);
871
+ case "elegant":
872
+ return renderElegantHeader(ctx);
873
+ case "tech":
874
+ return renderTechHeader(ctx);
875
+ case "vibrant":
876
+ return renderVibrantHeader(ctx);
877
+ case "compact":
878
+ return renderCompactHeader(ctx);
879
+ case "mono":
880
+ return renderMonoHeader(ctx);
881
+ case "classic":
882
+ default:
883
+ return renderClassicHeader(ctx);
884
+ }
885
+ }
886
+ function renderClassicHeader(ctx) {
887
+ const leftColExtra = (() => {
888
+ if (ctx.settings.showBarcode && ctx.barcodeDataUrl) {
889
+ return `<div style="margin-top: 10px;"><img src="${ctx.barcodeDataUrl}" alt="Barcode" style="max-width:200px;height:auto;" /></div>`;
890
+ }
891
+ if (ctx.settings.showQRCode && ctx.qrDataUrl) {
892
+ return `<div style="margin-top: 10px;"><img src="${ctx.qrDataUrl}" alt="QR Code" style="max-width:120px;height:auto;" /></div>`;
893
+ }
894
+ return "";
895
+ })();
896
+ return `<div class="row">
897
+ <div class="col-left">
898
+ ${ctx.logo ? `<img src="${ctx.logo}" class="logo-img" />` : ""}
899
+ ${!ctx.logo ? `<div class="company-header">${ctx.cName}</div>` : ""}
900
+ <div class="address-block">
901
+ <strong>${ctx.cName}</strong><br>
902
+ ${ctx.cAddress}<br>
903
+ ${ctx.cCity} ${ctx.cCode}<br>
904
+ ${ctx.cProv}
905
+ </div>
906
+ ${ctx.settings.showTaxNumber && ctx.cVat ? `<div class="reg-info">VAT No.: ${ctx.cVat}</div>` : ""}
907
+ <div class="contact-info">
908
+ ${ctx.settings.showPhone && ctx.cPhone ? `${ctx.cPhone}<br>` : ""}
909
+ ${ctx.settings.showPhone && ctx.cPhone2 ? `${ctx.cPhone2}<br>` : ""}
910
+ ${ctx.settings.showEmail && ctx.cEmail ? `${ctx.cEmail}<br>` : ""}
911
+ ${ctx.settings.showWebsite ? ctx.cWeb : ""}
912
+ </div>
913
+ ${leftColExtra}
914
+ </div>
915
+ <div class="col-right">
916
+ <div class="quote-title-main">${ctx.docTitle}</div>
917
+ <table class="details-table">
918
+ <tr><td class="label-cell">${ctx.docTitle.slice(0, -1)} No.:</td><td class="value-cell">${ctx.docNumber}</td></tr>
919
+ <tr><td class="label-cell">Date:</td><td class="value-cell"></td></tr>
920
+ <tr><td class="label-cell">Due date:</td><td class="value-cell"></td></tr>
921
+ </table>
922
+ </div>
923
+ </div>`;
924
+ }
925
+
926
+ // src/utils/formatting.ts
927
+ function escapeHtml(value) {
928
+ if (value === null || value === void 0) return "";
929
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
930
+ }
931
+ function hasVisibleText(str) {
932
+ if (!str) return false;
933
+ const stripped = str.replace(/<[^>]+>/g, "").trim();
934
+ return stripped.length > 0;
935
+ }
936
+ function getClientDisplayName(client) {
937
+ if (!client) return "";
938
+ if (client.company && client.company.trim()) return client.company.trim();
939
+ return client.name || "";
940
+ }
941
+ function formatCurrency(amount, currency = "ZAR") {
942
+ const num = typeof amount === "number" ? amount : parseFloat(String(amount || 0));
943
+ if (isNaN(num)) return currency === "ZAR" ? "R0.00" : "0.00";
944
+ const formatted = num.toLocaleString("en-ZA", {
945
+ minimumFractionDigits: 2,
946
+ maximumFractionDigits: 2
947
+ });
948
+ if (currency === "ZAR") return `R${formatted}`;
949
+ return `${currency} ${formatted}`;
950
+ }
951
+ function formatDate(date) {
952
+ if (!date) return "";
953
+ const d = typeof date === "string" ? new Date(date) : date;
954
+ if (isNaN(d.getTime())) return String(date);
955
+ return d.toLocaleDateString("en-ZA", {
956
+ year: "numeric",
957
+ month: "short",
958
+ day: "2-digit"
959
+ });
960
+ }
961
+ function formatCompanyAddress(companyInfo) {
962
+ if (!companyInfo) return "";
963
+ const parts = [
964
+ companyInfo.address || companyInfo.Address,
965
+ companyInfo.city,
966
+ companyInfo.province,
967
+ companyInfo.postCode
968
+ ].filter(Boolean);
969
+ return parts.join(", ");
970
+ }
971
+
972
+ // src/utils/barcode.ts
973
+ var import_qrcode = __toESM(require("qrcode"));
974
+ var CODE128_PATTERNS = [
975
+ "212222",
976
+ "222122",
977
+ "222221",
978
+ "121223",
979
+ "121322",
980
+ "131222",
981
+ "122213",
982
+ "122312",
983
+ "132212",
984
+ "221213",
985
+ "221312",
986
+ "231212",
987
+ "112232",
988
+ "122132",
989
+ "122231",
990
+ "113222",
991
+ "123122",
992
+ "123221",
993
+ "223211",
994
+ "221132",
995
+ "221231",
996
+ "213212",
997
+ "223112",
998
+ "312131",
999
+ "311222",
1000
+ "321122",
1001
+ "321221",
1002
+ "312212",
1003
+ "322112",
1004
+ "322211",
1005
+ "212123",
1006
+ "212321",
1007
+ "232121",
1008
+ "111323",
1009
+ "131123",
1010
+ "131321",
1011
+ "112313",
1012
+ "132113",
1013
+ "132311",
1014
+ "211313",
1015
+ "231113",
1016
+ "231311",
1017
+ "112133",
1018
+ "112331",
1019
+ "132131",
1020
+ "113123",
1021
+ "113321",
1022
+ "133121",
1023
+ "313121",
1024
+ "211331",
1025
+ "231131",
1026
+ "213113",
1027
+ "213311",
1028
+ "213131",
1029
+ "311123",
1030
+ "311321",
1031
+ "331121",
1032
+ "312113",
1033
+ "312311",
1034
+ "332111",
1035
+ "314111",
1036
+ "221411",
1037
+ "431111",
1038
+ "111224",
1039
+ "111422",
1040
+ "121124",
1041
+ "121421",
1042
+ "141122",
1043
+ "141221",
1044
+ "112214",
1045
+ "112412",
1046
+ "122114",
1047
+ "122411",
1048
+ "142112",
1049
+ "142211",
1050
+ "241211",
1051
+ "221114",
1052
+ "413111",
1053
+ "241112",
1054
+ "134111",
1055
+ "111242",
1056
+ "121142",
1057
+ "121241",
1058
+ "114212",
1059
+ "124112",
1060
+ "124211",
1061
+ "411212",
1062
+ "421112",
1063
+ "421211",
1064
+ "212141",
1065
+ "214121",
1066
+ "412121",
1067
+ "111143",
1068
+ "111341",
1069
+ "131141",
1070
+ "114113",
1071
+ "114311",
1072
+ "411113",
1073
+ "411311",
1074
+ "113141",
1075
+ "114131",
1076
+ "311141",
1077
+ "411131",
1078
+ "211412",
1079
+ "211214",
1080
+ "211232",
1081
+ "2331112"
1082
+ ];
1083
+ var START_CODE_B = 104;
1084
+ var STOP_CODE = 106;
1085
+ function generateCode128Svg(text, height = 40) {
1086
+ if (!text) text = "000000";
1087
+ let checksum = START_CODE_B;
1088
+ const codes = [START_CODE_B];
1089
+ for (let i = 0; i < text.length; i++) {
1090
+ const charCode = text.charCodeAt(i);
1091
+ const codeVal = charCode >= 32 && charCode <= 126 ? charCode - 32 : 0;
1092
+ codes.push(codeVal);
1093
+ checksum += codeVal * (i + 1);
1094
+ }
1095
+ codes.push(checksum % 103);
1096
+ codes.push(STOP_CODE);
1097
+ let pattern = "";
1098
+ for (const code of codes) {
1099
+ pattern += CODE128_PATTERNS[code] || "212222";
1100
+ }
1101
+ const moduleWidth = 1.5;
1102
+ let currentX = 10;
1103
+ const rects = [];
1104
+ for (let i = 0; i < pattern.length; i++) {
1105
+ const width = parseInt(pattern[i], 10) * moduleWidth;
1106
+ const isBar = i % 2 === 0;
1107
+ if (isBar) {
1108
+ rects.push(`<rect x="${currentX}" y="0" width="${width}" height="${height}" fill="#000000"/>`);
1109
+ }
1110
+ currentX += width;
1111
+ }
1112
+ const totalWidth = currentX + 10;
1113
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${height + 16}" viewBox="0 0 ${totalWidth} ${height + 16}">
1114
+ <rect width="100%" height="100%" fill="#ffffff"/>
1115
+ ${rects.join("")}
1116
+ <text x="${totalWidth / 2}" y="${height + 12}" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#000000">${escapeXml(text)}</text>
1117
+ </svg>`;
1118
+ return svg;
1119
+ }
1120
+ function escapeXml(unsafe) {
1121
+ return unsafe.replace(/[<>&'"]/g, (c) => {
1122
+ switch (c) {
1123
+ case "<":
1124
+ return "&lt;";
1125
+ case ">":
1126
+ return "&gt;";
1127
+ case "&":
1128
+ return "&amp;";
1129
+ case "'":
1130
+ return "&apos;";
1131
+ case '"':
1132
+ return "&quot;";
1133
+ default:
1134
+ return c;
1135
+ }
1136
+ });
1137
+ }
1138
+ async function generateBarcode(value) {
1139
+ const cleanVal = String(value || "").trim();
1140
+ const svg = generateCode128Svg(cleanVal);
1141
+ return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
1142
+ }
1143
+ async function generateQRCode(value) {
1144
+ const cleanVal = String(value || "").trim();
1145
+ const svg = await import_qrcode.default.toString(cleanVal, {
1146
+ type: "svg",
1147
+ width: 120,
1148
+ margin: 2,
1149
+ color: {
1150
+ dark: "#000000",
1151
+ light: "#ffffff"
1152
+ }
1153
+ });
1154
+ return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
1155
+ }
1156
+
1157
+ // src/utils/defaults.ts
1158
+ var DEFAULT_TAX_RATE = 0.15;
1159
+ var DEFAULT_DOCUMENT_TYPE_SETTINGS = {
1160
+ showSignature: true,
1161
+ showWatermark: true,
1162
+ showBarcode: false,
1163
+ showQRCode: false,
1164
+ showPreparedBy: true,
1165
+ showLastUpdatedBy: true,
1166
+ showDescription: true,
1167
+ showPaymentTerms: true,
1168
+ showAdditionalNotes: true,
1169
+ showTaxNumber: true,
1170
+ showEmail: true,
1171
+ showPhone: true,
1172
+ showWebsite: true,
1173
+ showBankDetails: true
1174
+ };
1175
+ function getDocumentTypeSettings(settings, docType) {
1176
+ if (!settings || !settings[docType]) {
1177
+ return { ...DEFAULT_DOCUMENT_TYPE_SETTINGS };
1178
+ }
1179
+ return { ...DEFAULT_DOCUMENT_TYPE_SETTINGS, ...settings[docType] };
1180
+ }
1181
+ var DEFAULT_COMPANY_INFO = {
1182
+ companyName: "Your Company",
1183
+ address: "123 Business St",
1184
+ city: "Johannesburg",
1185
+ province: "Gauteng",
1186
+ postCode: "2000",
1187
+ phone: "+27 11 000 0000",
1188
+ email: "info@company.com",
1189
+ taxId: "4000000000",
1190
+ paymentTerms: "Due within 30 days of issue date.",
1191
+ note: "Thank you for your business!"
1192
+ };
1193
+ var DEFAULT_RECEIPT_COMPANY_INFO = {
1194
+ name: "Your Company",
1195
+ address: "123 Business St, Johannesburg",
1196
+ phone: "+27 11 000 0000",
1197
+ email: "info@company.com",
1198
+ taxNumber: "4000000000"
1199
+ };
1200
+
1201
+ // src/generators/invoice-generator.ts
1202
+ var InvoiceReportGenerator = class {
1203
+ static decimalToNumber(decimalValue) {
1204
+ if (decimalValue === null || decimalValue === void 0) return 0;
1205
+ if (typeof decimalValue === "number") return decimalValue;
1206
+ if (typeof decimalValue === "string") return parseFloat(decimalValue) || 0;
1207
+ if (typeof decimalValue === "object")
1208
+ return parseFloat(decimalValue.toString()) || 0;
1209
+ return 0;
1210
+ }
1211
+ static formatMoney(amount) {
1212
+ return amount.toLocaleString("en-ZA", {
1213
+ minimumFractionDigits: 2,
1214
+ maximumFractionDigits: 2
1215
+ });
1216
+ }
1217
+ static async generateInvoiceReportHTML(invoice, companyInfo, combineServices = true, templateId, docSettings) {
1218
+ if (isClassicTemplate(templateId)) {
1219
+ templateId = "classic";
1220
+ }
1221
+ const template = getA4Template(templateId);
1222
+ const settings = docSettings ?? DEFAULT_DOCUMENT_TYPE_SETTINGS;
1223
+ const clientInfo = invoice.client || {};
1224
+ let barcodeDataUrl = null;
1225
+ let qrDataUrl = null;
1226
+ try {
1227
+ if (settings.showBarcode && invoice.invoiceNumber)
1228
+ barcodeDataUrl = await generateBarcode(invoice.invoiceNumber);
1229
+ } catch {
1230
+ }
1231
+ try {
1232
+ if (settings.showQRCode && invoice.invoiceNumber)
1233
+ qrDataUrl = await generateQRCode(invoice.invoiceNumber);
1234
+ } catch {
1235
+ }
1236
+ const classicMode = template.id === "classic";
1237
+ const colorRed = classicMode ? "#990000" : template.brand;
1238
+ const colorGold = classicMode ? "#C5A005" : template.accent;
1239
+ const headerGreenBg = classicMode ? "#D1FAE5" : template.headerBg;
1240
+ const headerGreenText = classicMode ? "#065F46" : template.headerText;
1241
+ const issueDate = invoice.issueDate ? new Date(invoice.issueDate).toLocaleDateString("en-GB") : "";
1242
+ const dueDate = invoice.dueDate ? new Date(invoice.dueDate).toLocaleDateString("en-GB") : "";
1243
+ const cName = companyInfo?.companyName || "";
1244
+ const cAddress = companyInfo?.address || companyInfo?.Address || "";
1245
+ const cCity = companyInfo?.city || "";
1246
+ const cCode = companyInfo?.postCode || "";
1247
+ const cProv = companyInfo?.province || "";
1248
+ const cVat = companyInfo?.taxId || "";
1249
+ const cPhone = companyInfo?.phone || "";
1250
+ const cPhone2 = companyInfo?.phone2 || "";
1251
+ const cEmail = companyInfo?.email || "";
1252
+ const cWeb = companyInfo?.website || "";
1253
+ const logo = companyInfo?.logo || "";
1254
+ const showWatermark = companyInfo?.showWatermark !== false;
1255
+ const showSignature = companyInfo?.showSignature !== false;
1256
+ const signature = companyInfo?.signature || "";
1257
+ const individualItems = [];
1258
+ const serviceItemsForCombining = [];
1259
+ let subtotalNet = 0;
1260
+ let totalVat = 0;
1261
+ let totalDiscountMoney = 0;
1262
+ (invoice.items || []).forEach((item) => {
1263
+ const qty = this.decimalToNumber(item.quantity);
1264
+ const unitPrice = this.decimalToNumber(item.unitPrice);
1265
+ const taxRate = this.decimalToNumber(item.taxRate);
1266
+ const linePrice = qty * unitPrice;
1267
+ let discountVal = 0;
1268
+ const discountInput = this.decimalToNumber(item.itemDiscountAmount);
1269
+ if (item.itemDiscountType === "PERCENTAGE") {
1270
+ discountVal = linePrice * (discountInput / 100);
1271
+ } else if (item.itemDiscountType === "AMOUNT") {
1272
+ discountVal = discountInput;
1273
+ }
1274
+ const net = linePrice - discountVal;
1275
+ const vat = net * (taxRate / 100);
1276
+ const total = net + vat;
1277
+ subtotalNet += net;
1278
+ totalVat += vat;
1279
+ totalDiscountMoney += discountVal;
1280
+ const itemData = {
1281
+ description: item.description || "",
1282
+ qty,
1283
+ unitPrice,
1284
+ linePrice,
1285
+ discountInput: `${discountInput}`,
1286
+ itemDiscountType: item.itemDiscountType,
1287
+ vat,
1288
+ total,
1289
+ details: item.details || null
1290
+ };
1291
+ const isService = item.type?.toLowerCase() === "service" || !!item.serviceId;
1292
+ const isProduct = !isService && (!!item.shopProductId || item.type?.toLowerCase() === "product");
1293
+ if (isService) {
1294
+ if (combineServices) {
1295
+ serviceItemsForCombining.push(itemData);
1296
+ } else {
1297
+ individualItems.push({
1298
+ ...itemData,
1299
+ itemCode: "SVC",
1300
+ itemType: "service"
1301
+ });
1302
+ }
1303
+ } else if (isProduct) {
1304
+ individualItems.push({
1305
+ ...itemData,
1306
+ itemCode: "PRD",
1307
+ itemType: "product"
1308
+ });
1309
+ } else {
1310
+ individualItems.push({
1311
+ ...itemData,
1312
+ itemCode: "CST",
1313
+ itemType: "custom"
1314
+ });
1315
+ }
1316
+ });
1317
+ let combinedServiceData = null;
1318
+ if (combineServices && serviceItemsForCombining.length > 0) {
1319
+ let totalQuantity = 0;
1320
+ let totalLinePrice = 0;
1321
+ let totalDiscount = 0;
1322
+ let totalNet = 0;
1323
+ let totalVatServices = 0;
1324
+ let totalAmountServices = 0;
1325
+ serviceItemsForCombining.forEach((service) => {
1326
+ totalQuantity += service.qty;
1327
+ totalLinePrice += service.linePrice;
1328
+ totalDiscount += service.discountInput !== "0.00" && service.discountInput !== "0" ? service.linePrice * parseFloat(service.discountInput) / 100 : 0;
1329
+ totalNet += service.total - service.vat;
1330
+ totalVatServices += service.vat;
1331
+ totalAmountServices += service.total;
1332
+ });
1333
+ const averageUnitPrice = totalQuantity > 0 ? totalLinePrice / totalQuantity : 0;
1334
+ const combinedDiscountPercent = totalLinePrice > 0 ? (totalDiscount / totalLinePrice * 100).toFixed(1) : "0.0";
1335
+ combinedServiceData = {
1336
+ description: `Services Package (${serviceItemsForCombining.length} services)`,
1337
+ quantity: totalQuantity,
1338
+ unitPrice: averageUnitPrice,
1339
+ linePrice: totalLinePrice,
1340
+ discountInput: serviceItemsForCombining.some(
1341
+ (s) => s.discountInput !== "0.00" && s.discountInput !== "0"
1342
+ ) ? combinedDiscountPercent : "0.00",
1343
+ vat: totalVatServices,
1344
+ total: totalAmountServices,
1345
+ individualServices: serviceItemsForCombining,
1346
+ displayType: "combined-service"
1347
+ };
1348
+ }
1349
+ let globalDiscVal = 0;
1350
+ const globalDiscInput = this.decimalToNumber(invoice.discountAmount);
1351
+ if (invoice.discountType === "PERCENTAGE") {
1352
+ globalDiscVal = subtotalNet * (globalDiscInput / 100);
1353
+ } else if (invoice.discountType === "AMOUNT") {
1354
+ globalDiscVal = globalDiscInput;
1355
+ }
1356
+ const finalSubtotalExVat = subtotalNet - globalDiscVal;
1357
+ const vatRatio = subtotalNet > 0 ? finalSubtotalExVat / subtotalNet : 1;
1358
+ const finalVat = totalVat * vatRatio;
1359
+ const totalBeforeInterest = finalSubtotalExVat + finalVat;
1360
+ const interestAmount = this.decimalToNumber(invoice.interestAmount);
1361
+ const finalTotal = totalBeforeInterest + interestAmount;
1362
+ const totalDiscountDisplay = totalDiscountMoney + globalDiscVal;
1363
+ const getInstallmentMonths = (period) => {
1364
+ if (!period) return 0;
1365
+ if (typeof period === "number") return period;
1366
+ switch (period) {
1367
+ case "30days":
1368
+ return 1;
1369
+ case "1to3months":
1370
+ return 3;
1371
+ case "3to6months":
1372
+ return 6;
1373
+ case "6to9months":
1374
+ return 9;
1375
+ case "9to12months":
1376
+ return 12;
1377
+ default:
1378
+ return 0;
1379
+ }
1380
+ };
1381
+ const months = getInstallmentMonths(invoice.installmentPeriod);
1382
+ const balanceAfterDeposit = finalTotal - this.decimalToNumber(invoice.depositAmount);
1383
+ const estMonthlyPayment = months > 0 ? balanceAfterDeposit / months : 0;
1384
+ const renderItemDetails = (details) => {
1385
+ if (!details) return "";
1386
+ return `
1387
+ <div class="rich-text-content" style="font-size: 10px; color: #666; margin-top: 2px; margin-left: 3px;">
1388
+ ${details}
1389
+ </div>
1390
+ `;
1391
+ };
1392
+ const renderIndividualService = (service) => {
1393
+ return `
1394
+ <li style="list-style-type: disc; margin-left: 8px; margin-bottom: 4px;">
1395
+ ${service.description} \xD7 ${service.qty}
1396
+ </li>
1397
+ `;
1398
+ };
1399
+ const tableRows = [];
1400
+ individualItems.forEach((item) => {
1401
+ const detailsHtml = renderItemDetails(item.details);
1402
+ const rowStyle = item.itemType === "custom" ? `background-color: ${template.rowAltBg};` : "";
1403
+ tableRows.push(`
1404
+ <tr style="${rowStyle}">
1405
+ <td class="col-desc">
1406
+ <strong>${item.description}</strong>
1407
+ ${detailsHtml}
1408
+ </td>
1409
+ <td class="col-qty">${item.qty}</td>
1410
+ <td class="col-unit-price">${this.formatMoney(item.unitPrice)}</td>
1411
+ <td class="col-line-price">${this.formatMoney(item.linePrice)}</td>
1412
+ <td class="col-disc">${item.discountInput === "0.00" || item.discountInput === "0" ? "-" : item.itemDiscountType === "PERCENTAGE" ? `${item.discountInput}%` : `R${item.discountInput}`}</td>
1413
+ <td class="col-vat">${this.formatMoney(item.vat)}</td>
1414
+ <td class="col-total">${this.formatMoney(item.total)}</td>
1415
+ </tr>
1416
+ `);
1417
+ });
1418
+ if (combineServices && combinedServiceData) {
1419
+ const serviceListHtml = serviceItemsForCombining.map(renderIndividualService).join("");
1420
+ tableRows.push(`
1421
+ <tr style="background-color: ${template.rowAltBg};">
1422
+ <td class="col-desc">
1423
+ <strong>${combinedServiceData.description}</strong>
1424
+ <div style="font-size: 10px; color: #666; margin-top: 2px;">
1425
+ <ul style="margin: 2px 0 0 12px; padding: 0;">
1426
+ ${serviceListHtml}
1427
+ </ul>
1428
+ </div>
1429
+ </td>
1430
+ <td class="col-qty">${combinedServiceData.quantity}</td>
1431
+ <td class="col-unit-price">-</td>
1432
+ <td class="col-line-price">${this.formatMoney(combinedServiceData.linePrice)}</td>
1433
+ <td class="col-disc">${combinedServiceData.discountInput === "0.00" || combinedServiceData.discountInput === "0" ? "-" : `${combinedServiceData.discountInput}%`}</td>
1434
+ <td class="col-vat">${this.formatMoney(combinedServiceData.vat)}</td>
1435
+ <td class="col-total"><strong>${this.formatMoney(combinedServiceData.total)}</strong></td>
1436
+ </tr>
1437
+ `);
1438
+ }
1439
+ const totalPaid = (invoice.payments || []).filter((p) => !p.isDeleted).reduce((sum, p) => sum + this.decimalToNumber(p.amount), 0);
1440
+ const balanceDue = finalTotal - totalPaid;
1441
+ return `
1442
+ <!DOCTYPE html>
1443
+ <html>
1444
+ <head>
1445
+ <title>Invoice ${invoice.invoiceNumber}</title>
1446
+ <style>
1447
+ @import url('https://fonts.googleapis.com/css2?family=Arial:wght@400;700&display=swap');
1448
+ body { font-family: ${template.fontFamily}; margin: 0; padding: 40px; font-size: ${template.fontSize}; color: ${template.text}; line-height: 1.3; }
1449
+ .row { display: flex; width: 100%; gap: 20px; }
1450
+ .col-left { width: 60%; }
1451
+ .col-right { width: 40%; }
1452
+ .logo-img { width: 180px; margin-bottom: 15px; }
1453
+ .company-header { font-family: ${template.fontFamilyHeading}; font-size: 24px; font-weight: bold; color: ${classicMode ? "#d4af37" : template.accent}; margin-bottom: 5px; }
1454
+ .company-header span { color: ${colorRed}; }
1455
+ .address-block, .reg-info, .contact-info { margin-bottom: 8px; }
1456
+ .quote-title-main { font-family: ${template.fontFamilyHeading}; font-size: 26px; color: ${colorRed}; font-weight: bold; text-align: ${template.titleAlign}; margin-bottom: 15px; }
1457
+
1458
+ .details-table { width: 100%; border-collapse: collapse; margin-bottom: 15px; }
1459
+ .details-table td { border: none; padding: 4px 8px; font-weight: bold; }
1460
+ .label-cell { width: 40%; text-align: left; color: ${template.textMuted}; }
1461
+ .value-cell { width: 60%; text-align: right; }
1462
+
1463
+ .client-box-label { font-weight: bold; margin-bottom: 2px; color: ${template.textMuted}; }
1464
+ .client-box { border: 1px solid ${template.border}; border-radius: ${template.borderRadius}; padding: 10px; min-height: 80px; background-color: ${template.surface}; }
1465
+ .client-name { font-weight: bold; font-size: 13px; margin-bottom: 4px; }
1466
+
1467
+ .items-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
1468
+ .items-table thead { display: table-header-group; }
1469
+ .items-table tr { page-break-inside: avoid; page-break-after: auto; }
1470
+ .items-table th { border: none; padding: 8px 6px; background-color: ${headerGreenBg}; color: ${headerGreenText}; font-weight: bold; text-align: center; font-size: 11px; text-transform: uppercase; }
1471
+ .items-table td { border: none; border-bottom: 1px solid ${template.borderFaint}; padding: 8px 6px; vertical-align: top; font-size: 11px; word-wrap: break-word; overflow-wrap: anywhere; }
1472
+
1473
+ .rich-text-content {
1474
+ overflow-wrap: anywhere;
1475
+ word-break: break-word;
1476
+ color: ${template.text};
1477
+ font-size: 10px;
1478
+ line-height: 1.3;
1479
+ }
1480
+ .rich-text-content p { margin: 0 0 8px 0; padding: 0; }
1481
+ .rich-text-content ul, .rich-text-content ol { margin: 0 0 8px 0; padding-left: 1.5em; }
1482
+ .rich-text-content li { margin-bottom: 4px; }
1483
+ .rich-text-content h1, .rich-text-content h2, .rich-text-content h3,
1484
+ .rich-text-content h4, .rich-text-content h5, .rich-text-content h6 {
1485
+ margin: 12px 0 6px 0;
1486
+ color: ${template.text};
1487
+ line-height: 1.2;
1488
+ }
1489
+ .rich-text-content h1 { font-size: 16px; }
1490
+ .rich-text-content h2 { font-size: 14px; }
1491
+ .rich-text-content h3 { font-size: 12px; }
1492
+ .rich-text-content img { max-width: 100%; height: auto; display: block; margin: 8px 0; }
1493
+ .rich-text-content table { width: 100%; border-collapse: collapse; margin: 8px 0; font-size: 10px; }
1494
+ .rich-text-content td, .rich-text-content th { border: 1px solid ${template.border}; padding: 4px 6px; text-align: left; }
1495
+ .rich-text-content th { background-color: ${template.rowAltBg}; font-weight: bold; }
1496
+ .rich-text-content blockquote { margin: 8px 0; padding: 4px 12px; border-left: 3px solid ${template.border}; color: ${template.textMuted}; font-style: italic; }
1497
+
1498
+ .col-code { width: 6%; text-align: center; }
1499
+ .col-desc { width: 28%; text-align: left; }
1500
+ .col-qty { width: 8%; text-align: center; }
1501
+ .col-unit-price { width: 10%; text-align: right; }
1502
+ .col-line-price { width: 10%; text-align: right; }
1503
+ .col-disc { width: 8%; text-align: center; }
1504
+ .col-vat { width: 10%; text-align: right; }
1505
+ .col-total { width: 10%; text-align: right; }
1506
+
1507
+ .totals-wrapper { display: flex; justify-content: flex-end; margin-top: 20px; page-break-inside: avoid; }
1508
+ .totals-table { width: 300px; border-collapse: collapse; background-color: ${template.totalsBg}; border-radius: ${template.borderRadius}; overflow: hidden; }
1509
+ .totals-table td { border: none; padding: 6px 8px; font-weight: bold; font-size: 11px; }
1510
+ .text-right { text-align: right; }
1511
+
1512
+ .terms-section { margin-top: 60px; page-break-inside: avoid; }
1513
+ .footer-strip { margin-top: 40px; border-top: 1px solid ${template.borderFaint}; padding: 10px 5px; text-align: center; font-size: 11px; font-weight: bold; color: ${template.textMuted}; page-break-inside: avoid; }
1514
+
1515
+ /* Watermark */
1516
+ .watermark {
1517
+ position: fixed;
1518
+ top: 50%;
1519
+ left: 50%;
1520
+ transform: translate(-50%, -50%) rotate(-30deg);
1521
+ width: 600px;
1522
+ height: 600px;
1523
+ opacity: 0.05;
1524
+ z-index: -100;
1525
+ display: flex;
1526
+ align-items: center;
1527
+ justify-content: center;
1528
+ pointer-events: none;
1529
+ }
1530
+ .watermark img { max-width: 100%; max-height: 100%; object-fit: contain; }
1531
+
1532
+ @page { size: A4; margin: 1.5cm 1cm 2cm 1cm; }
1533
+ @media print {
1534
+ body { -webkit-print-color-adjust: exact; }
1535
+ }
1536
+ </style>
1537
+ </head>
1538
+ <body>
1539
+ ${settings.showWatermark && logo && showWatermark ? `<div class="watermark"><img src="${logo}" /></div>` : ""}
1540
+
1541
+ <div class="row">
1542
+ <div class="col-left">
1543
+ ${logo ? `<img src="${logo}" class="logo-img" />` : ""}
1544
+ ${!logo ? `<div class="company-header">${cName}</div>` : ""}
1545
+ <div class="address-block">
1546
+ <strong>${cName}</strong><br>
1547
+ ${cAddress}<br>
1548
+ ${cCity} ${cCode}<br>
1549
+ ${cProv}
1550
+ </div>
1551
+ ${settings.showTaxNumber && cVat ? `<div class="reg-info">VAT No.: ${cVat}</div>` : ""}
1552
+ <div class="contact-info">
1553
+ ${settings.showPhone && cPhone ? `${cPhone}<br>` : ""}
1554
+ ${settings.showPhone && cPhone2 ? `${cPhone2}<br>` : ""}
1555
+ ${settings.showEmail && cEmail ? `${cEmail}<br>` : ""}
1556
+ ${settings.showWebsite && cWeb ? `${cWeb}` : ""}
1557
+ </div>
1558
+ ${settings.showBarcode && barcodeDataUrl || settings.showQRCode && qrDataUrl ? `
1559
+ <div style="display: flex; gap: 12px; align-items: flex-start; margin-top: 10px;">
1560
+ ${settings.showBarcode && barcodeDataUrl ? `<img src="${barcodeDataUrl}" style="height: 42px; flex-shrink: 0;" />` : ""}
1561
+ ${settings.showQRCode && qrDataUrl ? `<img src="${qrDataUrl}" style="width: 75px; height: 75px; flex-shrink: 0;" />` : ""}
1562
+ </div>` : ""}
1563
+ </div>
1564
+ <div class="col-right">
1565
+ <div class="quote-title-main">INVOICE</div>
1566
+ <table class="details-table">
1567
+ <tr><td class="label-cell">Invoice No.:</td><td class="value-cell">${invoice.invoiceNumber}</td></tr>
1568
+ <tr><td class="label-cell">Date:</td><td class="value-cell">${issueDate}</td></tr>
1569
+ <tr><td class="label-cell">Due Date:</td><td class="value-cell">${dueDate}</td></tr>
1570
+ ${settings.showPreparedBy && (invoice.creator?.name || invoice.createdBy) ? `<tr><td class="label-cell">Prepared by:</td><td class="value-cell">${invoice.creator?.name || invoice.createdBy}</td></tr>` : ""}
1571
+ ${settings.showLastUpdatedBy && invoice.lastUpdatedBy?.name ? `<tr><td class="label-cell">Last updated by:</td><td class="value-cell">${invoice.lastUpdatedBy.name}</td></tr>` : ""}
1572
+ </table>
1573
+ ${renderContactBox({
1574
+ label: "BILL TO",
1575
+ name: getClientDisplayName(invoice.client),
1576
+ fullName: clientInfo.companyFullName || "",
1577
+ taxNumber: clientInfo.vatNumber || clientInfo.taxNumber || "",
1578
+ registrationNumber: clientInfo.registrationNumber || "",
1579
+ address: clientInfo.address || "",
1580
+ city: clientInfo.town ? clientInfo.town + (clientInfo.village ? " " + clientInfo.village : "") : clientInfo.village || "",
1581
+ postalCode: clientInfo.postalCode || "",
1582
+ province: clientInfo.province || "South Africa",
1583
+ phone: clientInfo.phone || "",
1584
+ email: clientInfo.email || "",
1585
+ website: clientInfo.website || ""
1586
+ })}
1587
+ </div>
1588
+ </div>
1589
+
1590
+ <!-- Salutation -->
1591
+ ${invoice.salutation ? `<div style="margin-top: 20px; margin-bottom: 20px; font-size: 13px; white-space: pre-wrap; color: ${template.textMuted};">${invoice.salutation}</div>` : ""}
1592
+
1593
+ <table class="items-table">
1594
+ <thead>
1595
+ <tr>
1596
+ <th class="col-desc">DESCRIPTION</th>
1597
+ <th class="col-qty">QTY</th>
1598
+ <th class="col-unit-price">UNIT PRICE (R)</th>
1599
+ <th class="col-line-price">PRICE (R)</th>
1600
+ <th class="col-disc">DISC</th>
1601
+ <th class="col-vat">VAT (R)</th>
1602
+ <th class="col-total">TOTAL (R)</th>
1603
+ </tr>
1604
+ </thead>
1605
+ <tbody>
1606
+ ${tableRows.join("")}
1607
+ </tbody>
1608
+ </table>
1609
+
1610
+ <div class="totals-wrapper">
1611
+ <table class="totals-table">
1612
+ ${totalDiscountDisplay > 0 ? `<tr><td class="label-cell" style="color: ${colorRed}">TOTAL DISCOUNT ${invoice.discountType === "PERCENTAGE" ? `(${this.decimalToNumber(invoice.discountAmount)}%)` : ""}:</td><td class="text-right" style="color: ${colorRed}">-R${this.formatMoney(totalDiscountDisplay)}</td></tr>` : ""}
1613
+ <tr><td class="label-cell">SUBTOTAL (NET):</td><td class="text-right">R${this.formatMoney(finalSubtotalExVat)}</td></tr>
1614
+ <tr><td class="label-cell" style="color: #7c3aed;">VAT 15%:</td><td class="text-right" style="color: #7c3aed;">R${this.formatMoney(finalVat)}</td></tr>
1615
+ ${interestAmount > 0 ? `<tr><td class="label-cell" style="color: #ea580c">INTEREST (${this.decimalToNumber(invoice.interestRate)}%):</td><td class="text-right" style="color: #ea580c">+ R${this.formatMoney(interestAmount)}</td></tr>` : ""}
1616
+ <tr style="border-top: 2px solid ${template.totalsBorder}; color: ${template.totalsBorder};">
1617
+ <td class="label-cell" style="font-size: 11px;">GRAND TOTAL:</td>
1618
+ <td class="text-right" style="font-size: 11px;">R${this.formatMoney(finalTotal)}</td>
1619
+ </tr>
1620
+ ${totalPaid > 0 ? `
1621
+ <tr><td class="label-cell" style="color: ${colorGold}">PAID:</td><td class="text-right" style="color: ${colorGold}">R${this.formatMoney(totalPaid)}</td></tr>
1622
+ <tr><td class="label-cell" style="color: ${colorRed}">BALANCE DUE:</td><td class="text-right" style="color: ${colorRed}">R${this.formatMoney(balanceDue)}</td></tr>` : `
1623
+ <tr><td class="label-cell" style="color: ${colorRed}">BALANCE DUE:</td><td class="text-right" style="color: ${colorRed}">R${this.formatMoney(balanceDue)}</td></tr>`}
1624
+ ${estMonthlyPayment > 0 ? `
1625
+ <tr style="background-color: #fff7ed;">
1626
+ <td class="label-cell" style="color: #c2410c; font-size: 11px;">EST. MONTHLY PMT (${months} ${months === 1 ? "MONTH" : "MONTHS"}):</td>
1627
+ <td class="text-right" style="color: #c2410c; font-size: 11px;">R${this.formatMoney(estMonthlyPayment)}</td>
1628
+ </tr>` : ""}
1629
+ </table>
1630
+ </div>
1631
+
1632
+ ${settings.showDescription && hasVisibleText(invoice.description) || settings.showPaymentTerms && hasVisibleText(invoice.paymentTerms) || settings.showAdditionalNotes && hasVisibleText(invoice.notes) ? `
1633
+ <div class="terms-section" style="margin-top: 30px; border-top: 1px ${template.dividerStyle} ${template.borderFaint}; padding-top: 20px;">
1634
+ ${settings.showDescription && hasVisibleText(invoice.description) ? `
1635
+ <div style="margin-bottom: 15px;">
1636
+ <div style="font-weight: 800; font-size: 11px; text-transform: uppercase; color: ${template.text}; margin-bottom: 4px; border-bottom: 1px ${template.dividerStyle} ${template.border}; width: fit-content; padding-right: 10px;">Description</div>
1637
+ <div class="rich-text-content">${invoice.description}</div>
1638
+ </div>` : ""}
1639
+ ${settings.showPaymentTerms && hasVisibleText(invoice.paymentTerms) ? `
1640
+ <div style="margin-bottom: 15px;">
1641
+ <div style="font-weight: 800; font-size: 11px; text-transform: uppercase; color: ${template.text}; margin-bottom: 4px; border-bottom: 1px ${template.dividerStyle} ${template.border}; width: fit-content; padding-right: 10px;">Payment Terms</div>
1642
+ <div class="rich-text-content">${invoice.paymentTerms}</div>
1643
+ </div>` : ""}
1644
+ ${settings.showAdditionalNotes && hasVisibleText(invoice.notes) ? `
1645
+ <div style="margin-bottom: 15px;">
1646
+ <div style="font-weight: 800; font-size: 11px; text-transform: uppercase; color: ${template.text}; margin-bottom: 4px; border-bottom: 1px ${template.dividerStyle} ${template.border}; width: fit-content; padding-right: 10px;">Additional Notes</div>
1647
+ <div class="rich-text-content">${invoice.notes}</div>
1648
+ </div>` : ""}
1649
+ </div>` : ""}
1650
+
1651
+ ${settings.showBankDetails ? `
1652
+ <div class="footer-strip">
1653
+ Account holder: ${companyInfo?.companyName || ""} &nbsp;&nbsp;
1654
+ ${companyInfo?.bankName ? `Bank: ${companyInfo.bankName} Account No.: ${companyInfo.bankAccount || ""} &nbsp;&nbsp;` : ""}
1655
+ ${companyInfo?.bankName2 ? `Bank: ${companyInfo.bankName2} Account No.: ${companyInfo.bankAccount2 || ""}` : ""}
1656
+ </div>` : ""}
1657
+
1658
+ ${settings.showSignature && signature && showSignature ? `
1659
+ <div style="margin-top: 50px; page-break-inside: avoid; width: fit-content; margin-left: auto;">
1660
+ <div style="text-align: center;">
1661
+ <div style="font-weight: bold; font-size: 11px; margin-bottom: 5px; color: ${template.textMuted}; text-transform: uppercase;">AUTHORIZED SIGNATURE</div>
1662
+ <img src="${signature}" style="max-height: 70px; max-width: 200px; display: block; margin: 0 auto;" />
1663
+ <div style="border-top: 1px solid ${template.text}; width: 220px; margin-top: 5px; margin-bottom: 3px;"></div>
1664
+ <div style="font-size: 12px; font-weight: bold;">FOR ${(companyInfo?.companyName || "Company").toUpperCase()}</div>
1665
+ </div>
1666
+ </div>` : ""}
1667
+ </body>
1668
+ </html>
1669
+ `;
1670
+ }
1671
+ };
1672
+ async function generateInvoiceHTML(invoice, options) {
1673
+ return InvoiceReportGenerator.generateInvoiceReportHTML(
1674
+ invoice,
1675
+ options?.companyInfo,
1676
+ options?.combineServices ?? true,
1677
+ options?.templateId,
1678
+ options?.docSettings
1679
+ );
1680
+ }
1681
+
1682
+ // src/generators/quotation-generator.ts
1683
+ var QuotationReportGenerator = class {
1684
+ static decimalToNumber(decimalValue) {
1685
+ if (decimalValue === null || decimalValue === void 0) return 0;
1686
+ if (typeof decimalValue === "number") return decimalValue;
1687
+ if (typeof decimalValue === "string") return parseFloat(decimalValue) || 0;
1688
+ if (decimalValue && typeof decimalValue === "object") {
1689
+ return parseFloat(decimalValue.toString()) || 0;
1690
+ }
1691
+ return 0;
1692
+ }
1693
+ static formatMoney(amount) {
1694
+ return amount.toLocaleString("en-ZA", {
1695
+ minimumFractionDigits: 2,
1696
+ maximumFractionDigits: 2
1697
+ });
1698
+ }
1699
+ static async generateQuotationReportHTML(quotation, companyInfo, optionsOrCombineServices = true, legacyTemplateId, docSettings) {
1700
+ let combineServices = true;
1701
+ let hideItemPrices = false;
1702
+ let templateId;
1703
+ if (typeof optionsOrCombineServices === "boolean") {
1704
+ combineServices = optionsOrCombineServices;
1705
+ templateId = legacyTemplateId;
1706
+ } else {
1707
+ combineServices = optionsOrCombineServices.combineServices ?? true;
1708
+ hideItemPrices = optionsOrCombineServices.hideItemPrices ?? false;
1709
+ templateId = optionsOrCombineServices.templateId ?? legacyTemplateId;
1710
+ docSettings = optionsOrCombineServices.docSettings ?? docSettings;
1711
+ }
1712
+ if (isClassicTemplate(templateId)) {
1713
+ templateId = "classic";
1714
+ }
1715
+ const template = getA4Template(templateId);
1716
+ const settings = docSettings ?? DEFAULT_DOCUMENT_TYPE_SETTINGS;
1717
+ let barcodeDataUrl = null;
1718
+ let qrDataUrl = null;
1719
+ try {
1720
+ if (settings.showBarcode && quotation.quotationNumber)
1721
+ barcodeDataUrl = await generateBarcode(quotation.quotationNumber);
1722
+ } catch {
1723
+ }
1724
+ try {
1725
+ if (settings.showQRCode && quotation.quotationNumber)
1726
+ qrDataUrl = await generateQRCode(quotation.quotationNumber);
1727
+ } catch {
1728
+ }
1729
+ const classicMode = template.id === "classic";
1730
+ const colorRed = classicMode ? "#A00000" : template.brand;
1731
+ const colorGold = classicMode ? "#C5A005" : template.accent;
1732
+ const headerGreenBg = classicMode ? "#D1FAE5" : template.headerBg;
1733
+ const headerGreenText = classicMode ? "#065F46" : template.headerText;
1734
+ const issueDate = quotation.issueDate || quotation.date || quotation.createdAt ? new Date(quotation.issueDate || quotation.date || quotation.createdAt).toLocaleDateString("en-GB") : "";
1735
+ const validUntil = quotation.validUntil || quotation.dueDate ? new Date(quotation.validUntil || quotation.dueDate).toLocaleDateString("en-GB") : "";
1736
+ const cName = companyInfo?.companyName || "";
1737
+ const cAddress = companyInfo?.address || companyInfo?.Address || "";
1738
+ const cCity = companyInfo?.city || "";
1739
+ const cCode = companyInfo?.postCode || "";
1740
+ const cProv = companyInfo?.province || "";
1741
+ const cVat = companyInfo?.taxId || "";
1742
+ const cPhone = companyInfo?.phone || "";
1743
+ const cPhone2 = companyInfo?.phone2 || "";
1744
+ const cEmail = companyInfo?.email || "";
1745
+ const cWeb = companyInfo?.website || "";
1746
+ const logo = companyInfo?.logo || "";
1747
+ const showWatermark = companyInfo?.showWatermark !== false;
1748
+ const showSignature = companyInfo?.showSignature !== false;
1749
+ const signature = companyInfo?.signature || "";
1750
+ const individualItems = [];
1751
+ const serviceItemsForCombining = [];
1752
+ let subtotalNet = 0;
1753
+ let totalVat = 0;
1754
+ (quotation.items || []).forEach((item) => {
1755
+ const qty = this.decimalToNumber(item.quantity);
1756
+ const unitPrice = this.decimalToNumber(item.unitPrice);
1757
+ const taxRate = this.decimalToNumber(item.taxRate ?? 15);
1758
+ const linePrice = qty * unitPrice;
1759
+ const net = linePrice;
1760
+ const vat = net * (taxRate / 100);
1761
+ const total = net + vat;
1762
+ subtotalNet += net;
1763
+ totalVat += vat;
1764
+ const itemData = {
1765
+ description: item.description || "",
1766
+ qty,
1767
+ unitPrice,
1768
+ linePrice,
1769
+ discountInput: "0.00",
1770
+ vat,
1771
+ total,
1772
+ details: item.details || null
1773
+ };
1774
+ const isService = item.type?.toLowerCase() === "service" || !!item.serviceId;
1775
+ const isProduct = !isService && (!!item.shopProductId || item.type?.toLowerCase() === "product");
1776
+ if (isService) {
1777
+ if (combineServices) {
1778
+ serviceItemsForCombining.push(itemData);
1779
+ } else {
1780
+ individualItems.push({
1781
+ ...itemData,
1782
+ itemCode: "SVC",
1783
+ itemType: "service"
1784
+ });
1785
+ }
1786
+ } else if (isProduct) {
1787
+ individualItems.push({
1788
+ ...itemData,
1789
+ itemCode: "PRD",
1790
+ itemType: "product"
1791
+ });
1792
+ } else {
1793
+ individualItems.push({
1794
+ ...itemData,
1795
+ itemCode: "CST",
1796
+ itemType: "custom"
1797
+ });
1798
+ }
1799
+ });
1800
+ let combinedServiceData = null;
1801
+ if (combineServices && serviceItemsForCombining.length > 0) {
1802
+ let totalQuantity = 0;
1803
+ let totalLinePrice = 0;
1804
+ let totalVatServices = 0;
1805
+ let totalAmountServices = 0;
1806
+ serviceItemsForCombining.forEach((service) => {
1807
+ totalQuantity += service.qty;
1808
+ totalLinePrice += service.linePrice;
1809
+ totalVatServices += service.vat;
1810
+ totalAmountServices += service.total;
1811
+ });
1812
+ const averageUnitPrice = totalQuantity > 0 ? totalLinePrice / totalQuantity : 0;
1813
+ combinedServiceData = {
1814
+ description: `Services Package (${serviceItemsForCombining.length} services)`,
1815
+ quantity: totalQuantity,
1816
+ unitPrice: averageUnitPrice,
1817
+ linePrice: totalLinePrice,
1818
+ vat: totalVatServices,
1819
+ total: totalAmountServices,
1820
+ individualServices: serviceItemsForCombining
1821
+ };
1822
+ }
1823
+ let globalDiscVal = 0;
1824
+ const globalDiscInput = this.decimalToNumber(quotation.discountAmount);
1825
+ if (quotation.discountType === "PERCENTAGE") {
1826
+ globalDiscVal = subtotalNet * (globalDiscInput / 100);
1827
+ } else if (quotation.discountType === "AMOUNT") {
1828
+ globalDiscVal = globalDiscInput;
1829
+ }
1830
+ const finalSubtotalExVat = subtotalNet - globalDiscVal;
1831
+ const vatRatio = subtotalNet > 0 ? finalSubtotalExVat / subtotalNet : 1;
1832
+ const finalVat = totalVat * vatRatio;
1833
+ const totalBeforeInterest = finalSubtotalExVat + finalVat;
1834
+ const interestAmount = this.decimalToNumber(quotation.interestAmount);
1835
+ const finalTotal = totalBeforeInterest + interestAmount;
1836
+ const renderItemDetails = (details) => {
1837
+ if (!details) return "";
1838
+ return `
1839
+ <div class="rich-text-content" style="font-size: 10px; color: #666; margin-top: 2px; margin-left: 3px;">
1840
+ ${details}
1841
+ </div>
1842
+ `;
1843
+ };
1844
+ const tableRows = [];
1845
+ individualItems.forEach((item) => {
1846
+ const detailsHtml = renderItemDetails(item.details);
1847
+ const rowStyle = item.itemType === "custom" ? `background-color: ${template.rowAltBg};` : "";
1848
+ tableRows.push(`
1849
+ <tr style="${rowStyle}">
1850
+ <td class="col-desc">
1851
+ <strong>${item.description}</strong>
1852
+ ${detailsHtml}
1853
+ </td>
1854
+ <td class="col-qty">${item.qty}</td>
1855
+ ${hideItemPrices ? "" : `<td class="col-unit-price">${this.formatMoney(item.unitPrice)}</td>`}
1856
+ ${hideItemPrices ? "" : `<td class="col-line-price">${this.formatMoney(item.linePrice)}</td>`}
1857
+ <td class="col-vat">${this.formatMoney(item.vat)}</td>
1858
+ <td class="col-total">${this.formatMoney(item.total)}</td>
1859
+ </tr>
1860
+ `);
1861
+ });
1862
+ if (combineServices && combinedServiceData) {
1863
+ tableRows.push(`
1864
+ <tr style="background-color: ${template.rowAltBg};">
1865
+ <td class="col-desc">
1866
+ <strong>${combinedServiceData.description}</strong>
1867
+ </td>
1868
+ <td class="col-qty">${combinedServiceData.quantity}</td>
1869
+ ${hideItemPrices ? "" : `<td class="col-unit-price">-</td>`}
1870
+ ${hideItemPrices ? "" : `<td class="col-line-price">${this.formatMoney(combinedServiceData.linePrice)}</td>`}
1871
+ <td class="col-vat">${this.formatMoney(combinedServiceData.vat)}</td>
1872
+ <td class="col-total"><strong>${this.formatMoney(combinedServiceData.total)}</strong></td>
1873
+ </tr>
1874
+ `);
1875
+ }
1876
+ const clientInfo = quotation.client || {};
1877
+ return `
1878
+ <!DOCTYPE html>
1879
+ <html>
1880
+ <head>
1881
+ <title>Quotation ${quotation.quotationNumber}</title>
1882
+ <style>
1883
+ @import url('https://fonts.googleapis.com/css2?family=Arial:wght@400;700&display=swap');
1884
+ body { font-family: ${template.fontFamily}; margin: 0; padding: 40px; font-size: ${template.fontSize}; color: ${template.text}; line-height: 1.3; }
1885
+ .row { display: flex; width: 100%; gap: 20px; }
1886
+ .col-left { width: 60%; }
1887
+ .col-right { width: 40%; }
1888
+ .logo-img { width: 180px; margin-bottom: 15px; }
1889
+ .company-header { font-family: ${template.fontFamilyHeading}; font-size: 24px; font-weight: bold; color: ${classicMode ? "#d4af37" : template.accent}; margin-bottom: 5px; }
1890
+ .quote-title-main { font-family: ${template.fontFamilyHeading}; font-size: 26px; color: ${colorRed}; font-weight: bold; text-align: ${template.titleAlign}; margin-bottom: 15px; }
1891
+
1892
+ .details-table { width: 100%; border-collapse: collapse; margin-bottom: 15px; }
1893
+ .details-table td { border: none; padding: 4px 8px; font-weight: bold; }
1894
+ .label-cell { width: 40%; text-align: left; color: ${template.textMuted}; }
1895
+ .value-cell { width: 60%; text-align: right; }
1896
+
1897
+ .client-box-label { font-weight: bold; margin-bottom: 2px; color: ${template.textMuted}; }
1898
+ .client-box { border: 1px solid ${template.border}; border-radius: ${template.borderRadius}; padding: 10px; min-height: 80px; background-color: ${template.surface}; }
1899
+ .client-name { font-weight: bold; font-size: 13px; margin-bottom: 4px; }
1900
+
1901
+ .items-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
1902
+ .items-table thead { display: table-header-group; }
1903
+ .items-table tr { page-break-inside: avoid; page-break-after: auto; }
1904
+ .items-table th { border: none; padding: 8px 6px; background-color: ${headerGreenBg}; color: ${headerGreenText}; font-weight: bold; text-align: center; font-size: 11px; text-transform: uppercase; }
1905
+ .items-table td { border: none; border-bottom: 1px solid ${template.borderFaint}; padding: 8px 6px; vertical-align: top; font-size: 11px; word-wrap: break-word; overflow-wrap: anywhere; }
1906
+
1907
+ .rich-text-content { overflow-wrap: anywhere; word-break: break-word; color: ${template.text}; font-size: 10px; line-height: 1.3; }
1908
+ .col-desc { width: ${hideItemPrices ? "50%" : "35%"}; text-align: left; }
1909
+ .col-qty { width: 10%; text-align: center; }
1910
+ .col-unit-price { width: 15%; text-align: right; }
1911
+ .col-line-price { width: 15%; text-align: right; }
1912
+ .col-vat { width: 12%; text-align: right; }
1913
+ .col-total { width: 13%; text-align: right; }
1914
+
1915
+ .totals-wrapper { display: flex; justify-content: flex-end; margin-top: 20px; page-break-inside: avoid; }
1916
+ .totals-table { width: 300px; border-collapse: collapse; background-color: ${template.totalsBg}; border-radius: ${template.borderRadius}; overflow: hidden; }
1917
+ .totals-table td { border: none; padding: 6px 8px; font-weight: bold; font-size: 11px; }
1918
+ .text-right { text-align: right; }
1919
+
1920
+ .terms-section { margin-top: 60px; page-break-inside: avoid; }
1921
+ .footer-strip { margin-top: 40px; border-top: 1px solid ${template.borderFaint}; padding: 10px 5px; text-align: center; font-size: 11px; font-weight: bold; color: ${template.textMuted}; page-break-inside: avoid; }
1922
+
1923
+ /* Watermark */
1924
+ .watermark {
1925
+ position: fixed;
1926
+ top: 50%;
1927
+ left: 50%;
1928
+ transform: translate(-50%, -50%) rotate(-30deg);
1929
+ width: 600px;
1930
+ height: 600px;
1931
+ opacity: 0.05;
1932
+ z-index: -100;
1933
+ display: flex;
1934
+ align-items: center;
1935
+ justify-content: center;
1936
+ pointer-events: none;
1937
+ }
1938
+ .watermark img { max-width: 100%; max-height: 100%; object-fit: contain; }
1939
+
1940
+ @page { size: A4; margin: 1.5cm 1cm 2cm 1cm; }
1941
+ @media print { body { -webkit-print-color-adjust: exact; } }
1942
+ </style>
1943
+ </head>
1944
+ <body>
1945
+ ${settings.showWatermark && logo && showWatermark ? `<div class="watermark"><img src="${logo}" /></div>` : ""}
1946
+
1947
+ <div class="row">
1948
+ <div class="col-left">
1949
+ ${logo ? `<img src="${logo}" class="logo-img" />` : ""}
1950
+ ${!logo ? `<div class="company-header">${cName}</div>` : ""}
1951
+ <div class="address-block">
1952
+ <strong>${cName}</strong><br>
1953
+ ${cAddress}<br>
1954
+ ${cCity} ${cCode}<br>
1955
+ ${cProv}
1956
+ </div>
1957
+ ${settings.showTaxNumber && cVat ? `<div class="reg-info">VAT No.: ${cVat}</div>` : ""}
1958
+ <div class="contact-info">
1959
+ ${settings.showPhone && cPhone ? `${cPhone}<br>` : ""}
1960
+ ${settings.showPhone && cPhone2 ? `${cPhone2}<br>` : ""}
1961
+ ${settings.showEmail && cEmail ? `${cEmail}<br>` : ""}
1962
+ ${settings.showWebsite && cWeb ? `${cWeb}` : ""}
1963
+ </div>
1964
+ ${settings.showBarcode && barcodeDataUrl || settings.showQRCode && qrDataUrl ? `
1965
+ <div style="display: flex; gap: 12px; align-items: flex-start; margin-top: 10px;">
1966
+ ${settings.showBarcode && barcodeDataUrl ? `<img src="${barcodeDataUrl}" style="height: 42px; flex-shrink: 0;" />` : ""}
1967
+ ${settings.showQRCode && qrDataUrl ? `<img src="${qrDataUrl}" style="width: 75px; height: 75px; flex-shrink: 0;" />` : ""}
1968
+ </div>` : ""}
1969
+ </div>
1970
+ <div class="col-right">
1971
+ <div class="quote-title-main">QUOTATION</div>
1972
+ <table class="details-table">
1973
+ <tr><td class="label-cell">Quote No.:</td><td class="value-cell">${quotation.quotationNumber}</td></tr>
1974
+ <tr><td class="label-cell">Date:</td><td class="value-cell">${issueDate}</td></tr>
1975
+ <tr><td class="label-cell">Valid Until:</td><td class="value-cell">${validUntil}</td></tr>
1976
+ ${settings.showPreparedBy && quotation.creator?.name ? `<tr><td class="label-cell">Prepared by:</td><td class="value-cell">${quotation.creator.name}</td></tr>` : ""}
1977
+ </table>
1978
+ ${renderContactBox({
1979
+ label: "FOR",
1980
+ name: getClientDisplayName(quotation.client),
1981
+ fullName: clientInfo.companyFullName || "",
1982
+ taxNumber: clientInfo.vatNumber || clientInfo.taxNumber || "",
1983
+ registrationNumber: clientInfo.registrationNumber || "",
1984
+ address: clientInfo.address || "",
1985
+ city: clientInfo.town ? clientInfo.town + (clientInfo.village ? " " + clientInfo.village : "") : clientInfo.village || "",
1986
+ postalCode: clientInfo.postalCode || "",
1987
+ province: clientInfo.province || "South Africa",
1988
+ phone: clientInfo.phone || "",
1989
+ email: clientInfo.email || "",
1990
+ website: clientInfo.website || ""
1991
+ })}
1992
+ </div>
1993
+ </div>
1994
+
1995
+ <table class="items-table" style="margin-top: 20px;">
1996
+ <thead>
1997
+ <tr>
1998
+ <th class="col-desc">DESCRIPTION</th>
1999
+ <th class="col-qty">QTY</th>
2000
+ ${hideItemPrices ? "" : `<th class="col-unit-price">UNIT PRICE (R)</th>`}
2001
+ ${hideItemPrices ? "" : `<th class="col-line-price">PRICE (R)</th>`}
2002
+ <th class="col-vat">VAT (R)</th>
2003
+ <th class="col-total">TOTAL (R)</th>
2004
+ </tr>
2005
+ </thead>
2006
+ <tbody>
2007
+ ${tableRows.join("")}
2008
+ </tbody>
2009
+ </table>
2010
+
2011
+ <div class="totals-wrapper">
2012
+ <table class="totals-table">
2013
+ <tr><td class="label-cell">SUBTOTAL (NET):</td><td class="text-right">R${this.formatMoney(finalSubtotalExVat)}</td></tr>
2014
+ <tr><td class="label-cell" style="color: #7c3aed;">VAT 15%:</td><td class="text-right" style="color: #7c3aed;">R${this.formatMoney(finalVat)}</td></tr>
2015
+ <tr style="border-top: 2px solid ${template.totalsBorder}; color: ${template.totalsBorder};">
2016
+ <td class="label-cell" style="font-size: 11px;">GRAND TOTAL:</td>
2017
+ <td class="text-right" style="font-size: 11px;">R${this.formatMoney(finalTotal)}</td>
2018
+ </tr>
2019
+ ${quotation.depositRequired ? `
2020
+ <tr>
2021
+ <td class="label-cell" style="color: ${colorRed}">DEPOSIT REQUIRED:</td>
2022
+ <td class="text-right" style="color: ${colorRed}">R${this.formatMoney(this.decimalToNumber(quotation.depositAmount))}</td>
2023
+ </tr>` : ""}
2024
+ </table>
2025
+ </div>
2026
+
2027
+ ${settings.showPaymentTerms && hasVisibleText(quotation.terms) || settings.showAdditionalNotes && hasVisibleText(quotation.notes) ? `
2028
+ <div class="terms-section" style="margin-top: 30px; border-top: 1px ${template.dividerStyle} ${template.borderFaint}; padding-top: 20px;">
2029
+ ${settings.showPaymentTerms && hasVisibleText(quotation.terms) ? `
2030
+ <div style="margin-bottom: 15px;">
2031
+ <div style="font-weight: 800; font-size: 11px; text-transform: uppercase; color: ${template.text}; margin-bottom: 4px; border-bottom: 1px ${template.dividerStyle} ${template.border}; width: fit-content; padding-right: 10px;">Terms & Conditions</div>
2032
+ <div class="rich-text-content">${quotation.terms}</div>
2033
+ </div>` : ""}
2034
+ ${settings.showAdditionalNotes && hasVisibleText(quotation.notes) ? `
2035
+ <div style="margin-bottom: 15px;">
2036
+ <div style="font-weight: 800; font-size: 11px; text-transform: uppercase; color: ${template.text}; margin-bottom: 4px; border-bottom: 1px ${template.dividerStyle} ${template.border}; width: fit-content; padding-right: 10px;">Notes</div>
2037
+ <div class="rich-text-content">${quotation.notes}</div>
2038
+ </div>` : ""}
2039
+ </div>` : ""}
2040
+
2041
+ ${settings.showBankDetails ? `
2042
+ <div class="footer-strip">
2043
+ Account holder: ${companyInfo?.companyName || ""} &nbsp;&nbsp;
2044
+ ${companyInfo?.bankName ? `Bank: ${companyInfo.bankName} Account No.: ${companyInfo.bankAccount || ""} &nbsp;&nbsp;` : ""}
2045
+ ${companyInfo?.bankName2 ? `Bank: ${companyInfo.bankName2} Account No.: ${companyInfo.bankAccount2 || ""}` : ""}
2046
+ </div>` : ""}
2047
+
2048
+ ${settings.showSignature && signature && showSignature ? `
2049
+ <div style="margin-top: 50px; page-break-inside: avoid; width: fit-content; margin-left: auto;">
2050
+ <div style="text-align: center;">
2051
+ <div style="font-weight: bold; font-size: 11px; margin-bottom: 5px; color: ${template.textMuted}; text-transform: uppercase;">AUTHORIZED SIGNATURE</div>
2052
+ <img src="${signature}" style="max-height: 70px; max-width: 200px; display: block; margin: 0 auto;" />
2053
+ <div style="border-top: 1px solid ${template.text}; width: 220px; margin-top: 5px; margin-bottom: 3px;"></div>
2054
+ <div style="font-size: 12px; font-weight: bold;">FOR ${(companyInfo?.companyName || "Company").toUpperCase()}</div>
2055
+ </div>
2056
+ </div>` : ""}
2057
+ </body>
2058
+ </html>
2059
+ `;
2060
+ }
2061
+ };
2062
+ async function generateQuotationHTML(quotation, options) {
2063
+ return QuotationReportGenerator.generateQuotationReportHTML(
2064
+ quotation,
2065
+ options?.companyInfo,
2066
+ options ?? true,
2067
+ options?.templateId,
2068
+ options?.docSettings
2069
+ );
2070
+ }
2071
+
2072
+ // src/generators/receipt-generator.ts
2073
+ var ReceiptGenerator = class {
2074
+ companyInfo = DEFAULT_RECEIPT_COMPANY_INFO;
2075
+ DEFAULT_TAX_RATE = DEFAULT_TAX_RATE;
2076
+ constructor(companyInfo) {
2077
+ if (companyInfo) {
2078
+ this.companyInfo = { ...DEFAULT_RECEIPT_COMPANY_INFO, ...companyInfo };
2079
+ }
2080
+ }
2081
+ setCompanyInfo(companyInfo) {
2082
+ if (!companyInfo) return;
2083
+ if ("companyName" in companyInfo) {
2084
+ this.companyInfo = {
2085
+ name: companyInfo.companyName || "Your Company",
2086
+ address: [companyInfo.address || companyInfo.Address, companyInfo.city, companyInfo.province, companyInfo.postCode].filter(Boolean).join(", "),
2087
+ phone: companyInfo.phone || "(123) 456-7890",
2088
+ email: companyInfo.email || "info@company.com",
2089
+ taxNumber: companyInfo.taxId,
2090
+ logo: companyInfo.logo,
2091
+ terminalLogo: companyInfo.terminalLogo,
2092
+ signature: companyInfo.signature,
2093
+ showWatermark: companyInfo.showWatermark !== false,
2094
+ showSignature: companyInfo.showSignature !== false
2095
+ };
2096
+ } else {
2097
+ this.companyInfo = { ...this.companyInfo, ...companyInfo };
2098
+ }
2099
+ }
2100
+ setTerminalLogo(logoUrl) {
2101
+ this.companyInfo.terminalLogo = logoUrl;
2102
+ }
2103
+ formatNumber(value, decimals = 2) {
2104
+ const num = typeof value === "string" ? parseFloat(value) : Number(value);
2105
+ return isNaN(num) ? "0.00" : num.toFixed(decimals);
2106
+ }
2107
+ formatDate(dateString) {
2108
+ try {
2109
+ const date = new Date(dateString);
2110
+ if (isNaN(date.getTime())) return (/* @__PURE__ */ new Date()).toLocaleString();
2111
+ return date.toLocaleString();
2112
+ } catch {
2113
+ return (/* @__PURE__ */ new Date()).toLocaleString();
2114
+ }
2115
+ }
2116
+ getItemName(item) {
2117
+ return item.product?.name || "Product";
2118
+ }
2119
+ getItemSKU(item) {
2120
+ return item.product?.sku || "N/A";
2121
+ }
2122
+ async generateReceiptHTML(saleData, size = "thermal", forPrint = false) {
2123
+ const processedSaleData = {
2124
+ ...saleData,
2125
+ company: {
2126
+ ...this.companyInfo,
2127
+ ...saleData.company || {}
2128
+ }
2129
+ };
2130
+ if (size === "A4") {
2131
+ return this.generateA4ReceiptHTML(processedSaleData, forPrint);
2132
+ } else {
2133
+ return this.generateThermalReceiptHTML(processedSaleData, forPrint);
2134
+ }
2135
+ }
2136
+ generateA4ReceiptHTML(saleData, forPrint = false) {
2137
+ const company = saleData.company || this.companyInfo;
2138
+ const subtotal = this.formatNumber(saleData.subtotal);
2139
+ const discount = this.formatNumber(saleData.discount);
2140
+ const tax = this.formatNumber(saleData.tax);
2141
+ const total = this.formatNumber(saleData.total);
2142
+ const amountReceived = saleData.amountReceived ? this.formatNumber(saleData.amountReceived) : null;
2143
+ const change = saleData.change ? this.formatNumber(saleData.change) : null;
2144
+ const itemsRows = saleData.items.map(
2145
+ (item) => `
2146
+ <tr>
2147
+ <td style="padding: 10px; border-bottom: 1px solid #e2e8f0;">
2148
+ <div style="font-weight: 600; color: #1e293b;">${this.getItemName(item)}</div>
2149
+ <div style="font-size: 11px; color: #64748b;">SKU: ${this.getItemSKU(item)}</div>
2150
+ </td>
2151
+ <td style="padding: 10px; border-bottom: 1px solid #e2e8f0; text-align: center;">${item.quantity}</td>
2152
+ <td style="padding: 10px; border-bottom: 1px solid #e2e8f0; text-align: right;">R${this.formatNumber(item.price)}</td>
2153
+ <td style="padding: 10px; border-bottom: 1px solid #e2e8f0; text-align: right; font-weight: 600;">R${this.formatNumber(item.total)}</td>
2154
+ </tr>`
2155
+ ).join("");
2156
+ return `
2157
+ <!DOCTYPE html>
2158
+ <html>
2159
+ <head>
2160
+ <meta charset="utf-8">
2161
+ <title>Receipt #${saleData.saleNumber}</title>
2162
+ <style>
2163
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 40px; color: #0f172a; font-size: 13px; line-height: 1.4; }
2164
+ .container { max-width: 800px; margin: 0 auto; }
2165
+ .header { display: flex; justify-content: space-between; border-bottom: 2px solid #0f172a; padding-bottom: 20px; margin-bottom: 24px; }
2166
+ .title { font-size: 28px; font-weight: 800; color: #0f172a; margin: 0; }
2167
+ .badge { display: inline-block; padding: 4px 10px; border-radius: 9999px; background: #dcfce7; color: #166534; font-size: 11px; font-weight: 700; margin-top: 6px; }
2168
+ table { width: 100%; border-collapse: collapse; margin-top: 16px; }
2169
+ th { background: #f8fafc; padding: 10px; text-align: left; font-size: 11px; text-transform: uppercase; color: #475569; border-bottom: 1px solid #cbd5e1; }
2170
+ .totals { width: 320px; margin-left: auto; margin-top: 24px; }
2171
+ .total-row { display: flex; justify-content: space-between; padding: 6px 0; }
2172
+ .grand-total { font-size: 16px; font-weight: 800; border-top: 2px solid #0f172a; padding-top: 8px; margin-top: 4px; }
2173
+ @media print { body { padding: 0; } }
2174
+ </style>
2175
+ ${forPrint ? `<script>window.onload = function() { window.print(); }</script>` : ""}
2176
+ </head>
2177
+ <body>
2178
+ <div class="container">
2179
+ <div class="header">
2180
+ <div>
2181
+ <h1 class="title">${company.name}</h1>
2182
+ <div style="color: #475569; margin-top: 6px;">${company.address}</div>
2183
+ <div style="color: #475569;">Tel: ${company.phone} \xB7 Email: ${company.email}</div>
2184
+ ${company.taxNumber ? `<div style="color: #475569;">VAT No: ${company.taxNumber}</div>` : ""}
2185
+ </div>
2186
+ <div style="text-align: right;">
2187
+ <div style="font-size: 22px; font-weight: 700;">TAX RECEIPT</div>
2188
+ <div style="color: #64748b; font-size: 12px; margin-top: 4px;">Receipt #${saleData.saleNumber}</div>
2189
+ <div style="color: #64748b; font-size: 12px;">Date: ${this.formatDate(saleData.date)}</div>
2190
+ <div class="badge">PAID</div>
2191
+ </div>
2192
+ </div>
2193
+
2194
+ ${saleData.customerName ? `
2195
+ <div style="background: #f8fafc; padding: 12px 16px; border-radius: 8px; margin-bottom: 20px;">
2196
+ <div style="font-size: 11px; font-weight: 700; color: #64748b; text-transform: uppercase;">Customer</div>
2197
+ <div style="font-weight: 600; margin-top: 2px;">${saleData.customerName}</div>
2198
+ ${saleData.customerPhone ? `<div style="font-size: 12px; color: #475569;">${saleData.customerPhone}</div>` : ""}
2199
+ ${saleData.customerEmail ? `<div style="font-size: 12px; color: #475569;">${saleData.customerEmail}</div>` : ""}
2200
+ </div>` : ""}
2201
+
2202
+ <table>
2203
+ <thead>
2204
+ <tr>
2205
+ <th>Item</th>
2206
+ <th style="text-align: center;">Qty</th>
2207
+ <th style="text-align: right;">Unit Price</th>
2208
+ <th style="text-align: right;">Total</th>
2209
+ </tr>
2210
+ </thead>
2211
+ <tbody>
2212
+ ${itemsRows}
2213
+ </tbody>
2214
+ </table>
2215
+
2216
+ <div class="totals">
2217
+ <div class="total-row"><span>Subtotal (Net)</span><span>R${subtotal}</span></div>
2218
+ ${saleData.discount > 0 ? `<div class="total-row" style="color: #dc2626;"><span>Discount</span><span>-R${discount}</span></div>` : ""}
2219
+ <div class="total-row"><span>VAT (15%)</span><span>R${tax}</span></div>
2220
+ <div class="total-row grand-total"><span>Total</span><span>R${total}</span></div>
2221
+ <div class="total-row" style="color: #64748b; font-size: 11px; margin-top: 8px;">
2222
+ <span>Payment Method</span><span>${saleData.paymentMethod.toUpperCase()}</span>
2223
+ </div>
2224
+ ${amountReceived ? `<div class="total-row" style="color: #64748b; font-size: 11px;"><span>Amount Tendered</span><span>R${amountReceived}</span></div>` : ""}
2225
+ ${change ? `<div class="total-row" style="color: #64748b; font-size: 11px;"><span>Change</span><span>R${change}</span></div>` : ""}
2226
+ </div>
2227
+
2228
+ <div style="text-align: center; margin-top: 60px; padding-top: 20px; border-top: 1px solid #e2e8f0; color: #64748b; font-size: 12px;">
2229
+ Thank you for your business!
2230
+ </div>
2231
+ </div>
2232
+ </body>
2233
+ </html>
2234
+ `;
2235
+ }
2236
+ generateThermalReceiptHTML(saleData, forPrint = false) {
2237
+ const company = saleData.company || this.companyInfo;
2238
+ const subtotal = this.formatNumber(saleData.subtotal);
2239
+ const discount = this.formatNumber(saleData.discount);
2240
+ const tax = this.formatNumber(saleData.tax);
2241
+ const total = this.formatNumber(saleData.total);
2242
+ const itemsRows = saleData.items.map(
2243
+ (item) => `
2244
+ <div style="display: flex; justify-content: space-between; margin-bottom: 4px; font-size: 12px;">
2245
+ <div style="flex: 1; padding-right: 8px;">
2246
+ <div>${this.getItemName(item)}</div>
2247
+ <div style="font-size: 10px; color: #555;">${item.quantity} x R${this.formatNumber(item.price)}</div>
2248
+ </div>
2249
+ <div style="font-weight: 700;">R${this.formatNumber(item.total)}</div>
2250
+ </div>`
2251
+ ).join("");
2252
+ return `
2253
+ <!DOCTYPE html>
2254
+ <html>
2255
+ <head>
2256
+ <meta charset="utf-8">
2257
+ <title>Receipt #${saleData.saleNumber}</title>
2258
+ <style>
2259
+ @page { size: 80mm auto; margin: 0; }
2260
+ body { font-family: 'Courier New', monospace; width: 72mm; margin: 0 auto; padding: 4mm 2mm; color: #000; font-size: 12px; line-height: 1.3; }
2261
+ .center { text-align: center; }
2262
+ .divider { border-top: 1px dashed #000; margin: 8px 0; }
2263
+ .double-divider { border-top: 2px solid #000; margin: 8px 0; }
2264
+ .row { display: flex; justify-content: space-between; margin: 3px 0; }
2265
+ .bold { font-weight: 700; }
2266
+ @media print { body { width: 72mm; margin: 0; } }
2267
+ </style>
2268
+ ${forPrint ? `<script>window.onload = function() { window.print(); }</script>` : ""}
2269
+ </head>
2270
+ <body>
2271
+ <div class="center">
2272
+ <div style="font-size: 15px; font-weight: 900; text-transform: uppercase;">${company.name}</div>
2273
+ <div style="font-size: 11px;">${company.address}</div>
2274
+ <div style="font-size: 11px;">Tel: ${company.phone}</div>
2275
+ ${company.taxNumber ? `<div style="font-size: 11px;">VAT No: ${company.taxNumber}</div>` : ""}
2276
+ </div>
2277
+
2278
+ <div class="divider"></div>
2279
+
2280
+ <div class="row"><span>Receipt #:</span><span class="bold">${saleData.saleNumber}</span></div>
2281
+ <div class="row"><span>Date:</span><span>${this.formatDate(saleData.date)}</span></div>
2282
+ ${saleData.customerName ? `<div class="row"><span>Customer:</span><span>${saleData.customerName}</span></div>` : ""}
2283
+
2284
+ <div class="double-divider"></div>
2285
+
2286
+ ${itemsRows}
2287
+
2288
+ <div class="double-divider"></div>
2289
+
2290
+ <div class="row"><span>Subtotal:</span><span>R${subtotal}</span></div>
2291
+ ${saleData.discount > 0 ? `<div class="row"><span>Discount:</span><span>-R${discount}</span></div>` : ""}
2292
+ <div class="row"><span>VAT (15%):</span><span>R${tax}</span></div>
2293
+ <div class="row bold" style="font-size: 14px; margin-top: 4px;"><span>TOTAL:</span><span>R${total}</span></div>
2294
+
2295
+ <div class="divider"></div>
2296
+ <div class="row"><span>Payment:</span><span>${saleData.paymentMethod.toUpperCase()}</span></div>
2297
+
2298
+ <div class="divider"></div>
2299
+ <div class="center" style="font-size: 11px; margin-top: 8px;">
2300
+ Thank you for your business!<br>
2301
+ Please keep receipt for returns.
2302
+ </div>
2303
+ </body>
2304
+ </html>
2305
+ `;
2306
+ }
2307
+ };
2308
+ var receiptGenerator = new ReceiptGenerator();
2309
+
2310
+ // src/generators/delivery-note-generator.ts
2311
+ var DeliveryNoteGenerator = class {
2312
+ static async generateDeliveryNoteHTML(deliveryNote, companyInfo, templateId, docSettings) {
2313
+ if (isClassicTemplate(templateId)) {
2314
+ templateId = "classic";
2315
+ }
2316
+ const template = getA4Template(templateId);
2317
+ const settings = docSettings ?? DEFAULT_DOCUMENT_TYPE_SETTINGS;
2318
+ const barcodeDataUrl = settings.showBarcode && deliveryNote.deliveryNoteNumber ? await generateBarcode(deliveryNote.deliveryNoteNumber) : null;
2319
+ const qrDataUrl = settings.showQRCode && deliveryNote.deliveryNoteNumber ? await generateQRCode(deliveryNote.deliveryNoteNumber) : null;
2320
+ const companyName = companyInfo?.companyName || "Your Company";
2321
+ const companyAddress = companyInfo?.address || companyInfo?.Address || "";
2322
+ const companyCity = companyInfo?.city || "";
2323
+ const companyProvince = companyInfo?.province || "";
2324
+ const companyPostCode = companyInfo?.postCode || "";
2325
+ const companyPhone = companyInfo?.phone || "";
2326
+ const companyEmail = companyInfo?.email || "";
2327
+ const logo = companyInfo?.logo || "";
2328
+ const client = deliveryNote.client || {};
2329
+ const itemsRows = (deliveryNote.items || []).map(
2330
+ (item) => `
2331
+ <tr>
2332
+ <td style="padding: 10px; border-bottom: 1px solid ${template.borderFaint};">
2333
+ <strong>${item.description}</strong>
2334
+ ${item.details ? `<div style="font-size: 10px; color: ${template.textMuted}; margin-top: 2px;">${item.details}</div>` : ""}
2335
+ </td>
2336
+ <td style="padding: 10px; border-bottom: 1px solid ${template.borderFaint}; text-align: center;">${item.quantity}</td>
2337
+ ${item.unitPrice !== void 0 ? `<td style="padding: 10px; border-bottom: 1px solid ${template.borderFaint}; text-align: right;">${formatCurrency(item.unitPrice)}</td>` : ""}
2338
+ ${item.amount !== void 0 ? `<td style="padding: 10px; border-bottom: 1px solid ${template.borderFaint}; text-align: right; font-weight: 600;">${formatCurrency(item.amount)}</td>` : ""}
2339
+ </tr>`
2340
+ ).join("");
2341
+ return `
2342
+ <!DOCTYPE html>
2343
+ <html>
2344
+ <head>
2345
+ <meta charset="utf-8">
2346
+ <title>Delivery Note #${deliveryNote.deliveryNoteNumber}</title>
2347
+ <style>
2348
+ body { font-family: ${template.fontFamily}; margin: 0; padding: 40px; color: ${template.text}; font-size: ${template.fontSize}; line-height: 1.4; }
2349
+ .header { display: flex; justify-content: space-between; border-bottom: 2px solid ${template.primary}; padding-bottom: 20px; margin-bottom: 24px; }
2350
+ .title { font-size: 26px; font-weight: 800; color: ${template.primary}; margin: 0; }
2351
+ table { width: 100%; border-collapse: collapse; margin-top: 20px; }
2352
+ th { background: ${template.headerBg}; color: ${template.headerText || template.primary}; padding: 10px; text-align: left; font-size: 11px; text-transform: uppercase; border-bottom: 1px solid ${template.border}; }
2353
+ .grid { display: flex; gap: 24px; margin-bottom: 24px; }
2354
+ .card { flex: 1; border: 1px solid ${template.border}; border-radius: ${template.borderRadius}; padding: 16px; background: ${template.surface}; }
2355
+ .card-title { font-size: 11px; font-weight: 700; color: ${template.textMuted}; text-transform: uppercase; margin-bottom: 8px; border-bottom: 1px solid ${template.borderFaint}; padding-bottom: 4px; }
2356
+ @page { size: A4; margin: 1.5cm 1cm 2cm 1cm; }
2357
+ @media print { body { padding: 0; } }
2358
+ </style>
2359
+ </head>
2360
+ <body>
2361
+ <div class="header">
2362
+ <div>
2363
+ ${logo ? `<img src="${logo}" style="height: 50px; margin-bottom: 10px; display: block;" />` : ""}
2364
+ <div style="font-size: 18px; font-weight: 700;">${companyName}</div>
2365
+ <div style="color: ${template.textMuted}; font-size: 11px;">
2366
+ ${companyAddress ? `${companyAddress}<br>` : ""}
2367
+ ${companyCity} ${companyProvince} ${companyPostCode}<br>
2368
+ ${companyPhone ? `Tel: ${companyPhone} \xB7 ` : ""}${companyEmail}
2369
+ </div>
2370
+ </div>
2371
+ <div style="text-align: right;">
2372
+ <h1 class="title">DELIVERY NOTE</h1>
2373
+ <div style="font-size: 12px; margin-top: 6px;">Note #: <strong>${deliveryNote.deliveryNoteNumber}</strong></div>
2374
+ <div style="font-size: 12px; color: ${template.textMuted};">Date: ${new Date(deliveryNote.date).toLocaleDateString("en-GB")}</div>
2375
+ ${deliveryNote.referenceNumber ? `<div style="font-size: 11px; color: ${template.textMuted};">Ref: ${deliveryNote.referenceNumber}</div>` : ""}
2376
+ ${barcodeDataUrl || qrDataUrl ? `
2377
+ <div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 10px;">
2378
+ ${barcodeDataUrl ? `<img src="${barcodeDataUrl}" style="height: 38px;" />` : ""}
2379
+ ${qrDataUrl ? `<img src="${qrDataUrl}" style="width: 60px; height: 60px;" />` : ""}
2380
+ </div>` : ""}
2381
+ </div>
2382
+ </div>
2383
+
2384
+ <div class="grid">
2385
+ <div class="card">
2386
+ <div class="card-title">Delivered To</div>
2387
+ <div style="font-weight: 700; font-size: 13px;">${getClientDisplayName(client)}</div>
2388
+ ${client.address ? `<div style="font-size: 11px; margin-top: 4px;">${client.address}</div>` : ""}
2389
+ ${client.city || client.town ? `<div style="font-size: 11px;">${client.city || client.town} ${client.postalCode || ""}</div>` : ""}
2390
+ ${client.phone ? `<div style="font-size: 11px; margin-top: 4px;">Tel: ${client.phone}</div>` : ""}
2391
+ ${client.email ? `<div style="font-size: 11px;">Email: ${client.email}</div>` : ""}
2392
+ </div>
2393
+ <div class="card">
2394
+ <div class="card-title">Delivery Details</div>
2395
+ <div style="font-size: 11px; line-height: 1.6;">
2396
+ <div><strong>Status:</strong> ${deliveryNote.status || "DISPATCHED"}</div>
2397
+ ${deliveryNote.referenceNumber ? `<div><strong>Reference:</strong> ${deliveryNote.referenceNumber}</div>` : ""}
2398
+ ${deliveryNote.creator?.name ? `<div><strong>Prepared By:</strong> ${deliveryNote.creator.name}</div>` : ""}
2399
+ </div>
2400
+ </div>
2401
+ </div>
2402
+
2403
+ <table>
2404
+ <thead>
2405
+ <tr>
2406
+ <th>Description</th>
2407
+ <th style="text-align: center; width: 12%;">Qty</th>
2408
+ ${deliveryNote.items?.some((it) => it.unitPrice !== void 0) ? `<th style="text-align: right; width: 18%;">Unit Price</th>` : ""}
2409
+ ${deliveryNote.items?.some((it) => it.amount !== void 0) ? `<th style="text-align: right; width: 18%;">Amount</th>` : ""}
2410
+ </tr>
2411
+ </thead>
2412
+ <tbody>
2413
+ ${itemsRows}
2414
+ </tbody>
2415
+ </table>
2416
+
2417
+ ${deliveryNote.notes ? `
2418
+ <div style="margin-top: 30px; border-top: 1px solid ${template.borderFaint}; padding-top: 12px; font-size: 11px;">
2419
+ <strong>Notes / Instructions:</strong>
2420
+ <p style="margin-top: 4px; color: ${template.textMuted};">${deliveryNote.notes}</p>
2421
+ </div>` : ""}
2422
+
2423
+ <div style="margin-top: 50px; display: flex; justify-content: space-between; page-break-inside: avoid;">
2424
+ <div style="width: 220px; text-align: center; border-top: 1px solid #000; padding-top: 6px; font-size: 11px;">
2425
+ Received in Good Order (Signature)
2426
+ </div>
2427
+ <div style="width: 220px; text-align: center; border-top: 1px solid #000; padding-top: 6px; font-size: 11px;">
2428
+ Date & Print Name
2429
+ </div>
2430
+ </div>
2431
+ </body>
2432
+ </html>
2433
+ `;
2434
+ }
2435
+ };
2436
+ async function generateDeliveryNoteHTML(deliveryNote, options) {
2437
+ return DeliveryNoteGenerator.generateDeliveryNoteHTML(
2438
+ deliveryNote,
2439
+ options?.companyInfo,
2440
+ options?.templateId,
2441
+ options?.docSettings
2442
+ );
2443
+ }
2444
+ // Annotate the CommonJS export names for ESM import in node:
2445
+ 0 && (module.exports = {
2446
+ A4_TEMPLATES,
2447
+ A4_TEMPLATE_IDS,
2448
+ DEFAULT_A4_ID,
2449
+ DEFAULT_COMPANY_INFO,
2450
+ DEFAULT_DOCUMENT_TYPE_SETTINGS,
2451
+ DEFAULT_RECEIPT_COMPANY_INFO,
2452
+ DEFAULT_TAX_RATE,
2453
+ DEFAULT_THERMAL_ID,
2454
+ DeliveryNoteGenerator,
2455
+ InvoiceReportGenerator,
2456
+ QuotationReportGenerator,
2457
+ ReceiptGenerator,
2458
+ THERMAL_TEMPLATES,
2459
+ THERMAL_TEMPLATE_IDS,
2460
+ escapeHtml,
2461
+ formatCompanyAddress,
2462
+ formatCurrency,
2463
+ formatDate,
2464
+ generateBarcode,
2465
+ generateCode128Svg,
2466
+ generateDeliveryNoteHTML,
2467
+ generateInvoiceHTML,
2468
+ generateQRCode,
2469
+ generateQuotationHTML,
2470
+ getA4Template,
2471
+ getClientDisplayName,
2472
+ getDocumentTypeSettings,
2473
+ getThermalTemplate,
2474
+ hasVisibleText,
2475
+ isA4TemplateId,
2476
+ isClassicTemplate,
2477
+ isThermalTemplateId,
2478
+ receiptGenerator,
2479
+ renderClassicHeader,
2480
+ renderContactBox,
2481
+ renderHeaderSection
2482
+ });