@flowselections/layout-maker 1.3.2

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.
Files changed (145) hide show
  1. package/README.md +24 -0
  2. package/dist-lib/_core-safelist.d.ts +2 -0
  3. package/dist-lib/_core-safelist.d.ts.map +1 -0
  4. package/dist-lib/_core-safelist.js +18 -0
  5. package/dist-lib/components/AfdrukKnop.d.ts +13 -0
  6. package/dist-lib/components/AfdrukKnop.d.ts.map +1 -0
  7. package/dist-lib/components/AfdrukKnop.js +38 -0
  8. package/dist-lib/components/AiLayoutImportDialog.d.ts +6 -0
  9. package/dist-lib/components/AiLayoutImportDialog.d.ts.map +1 -0
  10. package/dist-lib/components/AiLayoutImportDialog.js +133 -0
  11. package/dist-lib/components/BlokOntwerperPage.d.ts +5 -0
  12. package/dist-lib/components/BlokOntwerperPage.d.ts.map +1 -0
  13. package/dist-lib/components/BlokOntwerperPage.js +67 -0
  14. package/dist-lib/components/DocumentSettingsPanel.d.ts +13 -0
  15. package/dist-lib/components/DocumentSettingsPanel.d.ts.map +1 -0
  16. package/dist-lib/components/DocumentSettingsPanel.js +65 -0
  17. package/dist-lib/components/FormaatOrientatieBar.d.ts +10 -0
  18. package/dist-lib/components/FormaatOrientatieBar.d.ts.map +1 -0
  19. package/dist-lib/components/FormaatOrientatieBar.js +51 -0
  20. package/dist-lib/components/LayoutsPage.d.ts +2 -0
  21. package/dist-lib/components/LayoutsPage.d.ts.map +1 -0
  22. package/dist-lib/components/LayoutsPage.js +196 -0
  23. package/dist-lib/components/LogoBibliotheekDialog.d.ts +7 -0
  24. package/dist-lib/components/LogoBibliotheekDialog.d.ts.map +1 -0
  25. package/dist-lib/components/LogoBibliotheekDialog.js +27 -0
  26. package/dist-lib/components/LogosPage.d.ts +7 -0
  27. package/dist-lib/components/LogosPage.d.ts.map +1 -0
  28. package/dist-lib/components/LogosPage.js +131 -0
  29. package/dist-lib/components/PaginaDocument.d.ts +22 -0
  30. package/dist-lib/components/PaginaDocument.d.ts.map +1 -0
  31. package/dist-lib/components/PaginaDocument.js +80 -0
  32. package/dist-lib/components/PakbonBuilderPage.d.ts +5 -0
  33. package/dist-lib/components/PakbonBuilderPage.d.ts.map +1 -0
  34. package/dist-lib/components/PakbonBuilderPage.js +69 -0
  35. package/dist-lib/components/PakbonPreview.d.ts +13 -0
  36. package/dist-lib/components/PakbonPreview.d.ts.map +1 -0
  37. package/dist-lib/components/PakbonPreview.js +423 -0
  38. package/dist-lib/components/PakbonSectiesPanel.d.ts +18 -0
  39. package/dist-lib/components/PakbonSectiesPanel.d.ts.map +1 -0
  40. package/dist-lib/components/PakbonSectiesPanel.js +156 -0
  41. package/dist-lib/components/PreviewFit.d.ts +9 -0
  42. package/dist-lib/components/PreviewFit.d.ts.map +1 -0
  43. package/dist-lib/components/PreviewFit.js +38 -0
  44. package/dist-lib/components/PrintQueuePage.d.ts +2 -0
  45. package/dist-lib/components/PrintQueuePage.d.ts.map +1 -0
  46. package/dist-lib/components/PrintQueuePage.js +59 -0
  47. package/dist-lib/components/PrinterSetupWizard.d.ts +6 -0
  48. package/dist-lib/components/PrinterSetupWizard.d.ts.map +1 -0
  49. package/dist-lib/components/PrinterSetupWizard.js +119 -0
  50. package/dist-lib/components/PrintersPage.d.ts +2 -0
  51. package/dist-lib/components/PrintersPage.d.ts.map +1 -0
  52. package/dist-lib/components/PrintersPage.js +60 -0
  53. package/dist-lib/components/ProductiebonBlokkenPanel.d.ts +7 -0
  54. package/dist-lib/components/ProductiebonBlokkenPanel.d.ts.map +1 -0
  55. package/dist-lib/components/ProductiebonBlokkenPanel.js +85 -0
  56. package/dist-lib/components/ProductiebonBuilderPage.d.ts +5 -0
  57. package/dist-lib/components/ProductiebonBuilderPage.d.ts.map +1 -0
  58. package/dist-lib/components/ProductiebonBuilderPage.js +48 -0
  59. package/dist-lib/components/ProductiebonPreview.d.ts +12 -0
  60. package/dist-lib/components/ProductiebonPreview.d.ts.map +1 -0
  61. package/dist-lib/components/ProductiebonPreview.js +237 -0
  62. package/dist-lib/components/TemplateModuleLogo.d.ts +6 -0
  63. package/dist-lib/components/TemplateModuleLogo.d.ts.map +1 -0
  64. package/dist-lib/components/TemplateModuleLogo.js +4 -0
  65. package/dist-lib/components/settings/MijnModuleSettingsCard.d.ts +2 -0
  66. package/dist-lib/components/settings/MijnModuleSettingsCard.d.ts.map +1 -0
  67. package/dist-lib/components/settings/MijnModuleSettingsCard.js +42 -0
  68. package/dist-lib/hooks/useMijnModuleData.d.ts +13 -0
  69. package/dist-lib/hooks/useMijnModuleData.d.ts.map +1 -0
  70. package/dist-lib/hooks/useMijnModuleData.js +49 -0
  71. package/dist-lib/index.d.ts +38 -0
  72. package/dist-lib/index.d.ts.map +1 -0
  73. package/dist-lib/index.js +56 -0
  74. package/dist-lib/integrations/supabase/auth-attacher.d.ts +2 -0
  75. package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -0
  76. package/dist-lib/integrations/supabase/auth-attacher.js +12 -0
  77. package/dist-lib/integrations/supabase/auth-middleware.d.ts +19847 -0
  78. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -0
  79. package/dist-lib/integrations/supabase/auth-middleware.js +52 -0
  80. package/dist-lib/integrations/supabase/client.d.ts +2 -0
  81. package/dist-lib/integrations/supabase/client.d.ts.map +1 -0
  82. package/dist-lib/integrations/supabase/client.js +3 -0
  83. package/dist-lib/integrations/supabase/client.server.d.ts +19843 -0
  84. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -0
  85. package/dist-lib/integrations/supabase/client.server.js +30 -0
  86. package/dist-lib/integrations/supabase/types.d.ts +20423 -0
  87. package/dist-lib/integrations/supabase/types.d.ts.map +1 -0
  88. package/dist-lib/integrations/supabase/types.js +78 -0
  89. package/dist-lib/lib/aiLayout.functions.d.ts +19864 -0
  90. package/dist-lib/lib/aiLayout.functions.d.ts.map +1 -0
  91. package/dist-lib/lib/aiLayout.functions.js +28 -0
  92. package/dist-lib/lib/aiLayout.server.d.ts +14 -0
  93. package/dist-lib/lib/aiLayout.server.d.ts.map +1 -0
  94. package/dist-lib/lib/aiLayout.server.js +197 -0
  95. package/dist-lib/lib/browserPrint.d.ts +38 -0
  96. package/dist-lib/lib/browserPrint.d.ts.map +1 -0
  97. package/dist-lib/lib/browserPrint.js +104 -0
  98. package/dist-lib/lib/documentProfiles.d.ts +50 -0
  99. package/dist-lib/lib/documentProfiles.d.ts.map +1 -0
  100. package/dist-lib/lib/documentProfiles.js +458 -0
  101. package/dist-lib/lib/documentTypes.d.ts +123 -0
  102. package/dist-lib/lib/documentTypes.d.ts.map +1 -0
  103. package/dist-lib/lib/documentTypes.js +74 -0
  104. package/dist-lib/lib/fieldRegistry.d.ts +20 -0
  105. package/dist-lib/lib/fieldRegistry.d.ts.map +1 -0
  106. package/dist-lib/lib/fieldRegistry.js +123 -0
  107. package/dist-lib/lib/formaatPrinters.d.ts +14 -0
  108. package/dist-lib/lib/formaatPrinters.d.ts.map +1 -0
  109. package/dist-lib/lib/formaatPrinters.js +39 -0
  110. package/dist-lib/lib/layoutPreview.d.ts +18 -0
  111. package/dist-lib/lib/layoutPreview.d.ts.map +1 -0
  112. package/dist-lib/lib/layoutPreview.js +134 -0
  113. package/dist-lib/lib/logoLibrary.d.ts +80 -0
  114. package/dist-lib/lib/logoLibrary.d.ts.map +1 -0
  115. package/dist-lib/lib/logoLibrary.js +264 -0
  116. package/dist-lib/lib/pakbonBuilder.d.ts +210 -0
  117. package/dist-lib/lib/pakbonBuilder.d.ts.map +1 -0
  118. package/dist-lib/lib/pakbonBuilder.js +910 -0
  119. package/dist-lib/lib/pakbonHtml.d.ts +48 -0
  120. package/dist-lib/lib/pakbonHtml.d.ts.map +1 -0
  121. package/dist-lib/lib/pakbonHtml.js +754 -0
  122. package/dist-lib/lib/printApi.d.ts +45 -0
  123. package/dist-lib/lib/printApi.d.ts.map +1 -0
  124. package/dist-lib/lib/printApi.js +98 -0
  125. package/dist-lib/lib/productiebonBuilder.d.ts +137 -0
  126. package/dist-lib/lib/productiebonBuilder.d.ts.map +1 -0
  127. package/dist-lib/lib/productiebonBuilder.js +661 -0
  128. package/dist-lib/lib/supabaseClient.d.ts +4 -0
  129. package/dist-lib/lib/supabaseClient.d.ts.map +1 -0
  130. package/dist-lib/lib/supabaseClient.js +10 -0
  131. package/dist-lib/lib/useDocumentData.d.ts +97 -0
  132. package/dist-lib/lib/useDocumentData.d.ts.map +1 -0
  133. package/dist-lib/lib/useDocumentData.js +286 -0
  134. package/dist-lib/lib/useLayoutIdParam.d.ts +7 -0
  135. package/dist-lib/lib/useLayoutIdParam.d.ts.map +1 -0
  136. package/dist-lib/lib/useLayoutIdParam.js +10 -0
  137. package/dist-lib/lib/utils.d.ts +3 -0
  138. package/dist-lib/lib/utils.d.ts.map +1 -0
  139. package/dist-lib/lib/utils.js +5 -0
  140. package/dist-lib/lib/validationSchemas.d.ts +15 -0
  141. package/dist-lib/lib/validationSchemas.d.ts.map +1 -0
  142. package/dist-lib/lib/validationSchemas.js +25 -0
  143. package/dist-lib/styles.css +1 -0
  144. package/package.json +117 -0
  145. package/public/logo.png +0 -0
@@ -0,0 +1,754 @@
1
+ // ============================================================================
2
+ // src/lib/pakbonHtml.ts — canonieke print-HTML van een pakbon
3
+ // ----------------------------------------------------------------------------
4
+ // Dit is de ENIGE plek waar een pakbon naar printbare HTML wordt vertaald.
5
+ // Andere modules (bijv. het pickstation) mogen GEEN eigen pakbon-sjabloon meer
6
+ // bevatten: zij roepen `renderPakbonDocument()` of `printPakbon()` aan met de
7
+ // layoutconfiguratie en de echte ordergegevens. Zo tonen het live voorbeeld en
8
+ // de afdruk altijd exact dezelfde secties — inclusief de orderregels.
9
+ // ============================================================================
10
+ import { DEMO_ORDER, conditieContext, groepeerIdentiekeKarren, groepeerIdentiekeLagen, karBundelTekst, fustVerzamellijst, pickMethode, productVeldBreedtes, sectieOnderdrukt, laagBundelTekst, leesPakbonConfig, sectieZichtbaar, sorteerProducten, totalen, vulVeldcodes, } from "./pakbonBuilder";
11
+ import { haalPrintLogos } from "./logoLibrary";
12
+ // --- kleine hulpjes ---------------------------------------------------------
13
+ const arr = (v) => (Array.isArray(v) ? v : []);
14
+ const opt = (s, key, fallback) => s.opties?.[key] ?? fallback;
15
+ const optS = (s, key, fallback) => String(s.opties?.[key] ?? fallback);
16
+ const lijst = (s, key) => Array.isArray(s.opties?.[key]) ? s.opties[key].map(String) : [];
17
+ export function esc(v) {
18
+ return String(v ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
19
+ }
20
+ const cel = (p, veld) => {
21
+ if (veld === "foto")
22
+ return "";
23
+ if (veld === "qrcode")
24
+ return "";
25
+ return String(p[veld] ?? "");
26
+ };
27
+ /**
28
+ * Vult ontbrekende onderdelen aan met veilige lege waarden, zodat een module
29
+ * die maar een deel van de gegevens heeft nooit een lege of stukke pakbon
30
+ * afdrukt.
31
+ */
32
+ export function maakPakbonOrder(input) {
33
+ const bron = (input && typeof input === "object" ? input : {});
34
+ const groep = (naam, velden) => {
35
+ const b = (bron[naam] && typeof bron[naam] === "object" ? bron[naam] : {});
36
+ const out = {};
37
+ for (const v of velden)
38
+ out[v] = b[v] === undefined || b[v] === null ? "" : String(b[v]);
39
+ return out;
40
+ };
41
+ const karren = arr(bron["karren"]).map((k, i) => ({
42
+ karnummer: Number(k["karnummer"] ?? i + 1),
43
+ route: String(k["route"] ?? ""),
44
+ chauffeur: String(k["chauffeur"] ?? ""),
45
+ gewicht: String(k["gewicht"] ?? ""),
46
+ lagen: arr(k["lagen"]).map((l, j) => ({
47
+ laagnummer: Number(l["laagnummer"] ?? j + 1),
48
+ belading: String(l["belading"] ?? ""),
49
+ potmaat: String(l["potmaat"] ?? ""),
50
+ gewicht: String(l["gewicht"] ?? ""),
51
+ opmerking: String(l["opmerking"] ?? ""),
52
+ producten: arr(l["producten"]).map(normProduct),
53
+ fusten: arr(l["fusten"]).map((f) => ({
54
+ fusttype: String(f["fusttype"] ?? f["fustsoort"] ?? ""),
55
+ aantal: Number(f["aantal"] ?? 0) || 0,
56
+ hoogte: String(f["hoogte"] ?? ""),
57
+ belading: String(f["belading"] ?? ""),
58
+ barcode: String(f["barcode"] ?? ""),
59
+ })),
60
+ })),
61
+ }));
62
+ // Losse orderregels zonder karren: in één kar/laag zetten zodat elke
63
+ // sectie-instelling (orderregels, producten) gewoon werkt.
64
+ const losseRegels = arr(bron["producten"] ?? bron["regels"]).map(normProduct);
65
+ if (!karren.length && losseRegels.length) {
66
+ karren.push({
67
+ karnummer: 1,
68
+ route: "",
69
+ chauffeur: "",
70
+ gewicht: "",
71
+ lagen: [
72
+ {
73
+ laagnummer: 1,
74
+ belading: "",
75
+ potmaat: "",
76
+ gewicht: "",
77
+ opmerking: "",
78
+ producten: losseRegels,
79
+ fusten: [],
80
+ },
81
+ ],
82
+ });
83
+ }
84
+ return {
85
+ order: groep("order", ["nummer", "datum", "leverdatum", "referentie", "opmerking"]),
86
+ bedrijf: groep("bedrijf", ["naam", "adres", "plaats", "telefoon", "email", "kvk", "btw"]),
87
+ klant: groep("klant", ["naam", "nummer", "contact", "referentie"]),
88
+ afleveradres: groep("afleveradres", ["naam", "adres", "plaats", "land"]),
89
+ factuuradres: groep("factuuradres", ["naam", "adres", "plaats", "land"]),
90
+ transport: groep("transport", ["transporteur", "route", "chauffeur", "track"]),
91
+ karren,
92
+ };
93
+ }
94
+ function normProduct(p) {
95
+ const aantal = Number(p["aantal"] ?? 0) || 0;
96
+ return {
97
+ artikelnummer: String(p["artikelnummer"] ?? p["artikelcode"] ?? ""),
98
+ artikelnaam: String(p["artikelnaam"] ?? p["naam"] ?? p["product_name"] ?? ""),
99
+ potmaat: String(p["potmaat"] ?? ""),
100
+ aantal,
101
+ belading: String(p["belading"] ?? ""),
102
+ barcode: String(p["barcode"] ?? ""),
103
+ opmerking: String(p["opmerking"] ?? ""),
104
+ locatie: String(p["locatie"] ?? ""),
105
+ gewicht: String(p["gewicht"] ?? ""),
106
+ productgroep: String(p["productgroep"] ?? ""),
107
+ kleur: String(p["kleur"] ?? ""),
108
+ fust_aantal: Number(p["fust_aantal"] ?? 0) || 0,
109
+ fustsoort: String(p["fustsoort"] ?? ""),
110
+ planten_per_fust: Number(p["planten_per_fust"] ?? 0) || 0,
111
+ };
112
+ }
113
+ // --- secties ----------------------------------------------------------------
114
+ function blokFrame(s, inhoud) {
115
+ if (!inhoud)
116
+ return "";
117
+ const o = s.opties ?? {};
118
+ const breedte = Math.min(Math.max(Number(o["blok_breedte"] ?? 100), 5), 100);
119
+ const uitlijning = String(o["blok_uitlijning"] ?? "left");
120
+ const marge = Number(o["blok_marge"] ?? 0);
121
+ const padding = Number(o["blok_padding"] ?? 0);
122
+ const rand = !!o["blok_rand"];
123
+ const radius = Number(o["blok_radius"] ?? 0);
124
+ const schaal = Number(o["blok_tekstschaal"] ?? 100);
125
+ const stijl = [
126
+ "display:flow-root",
127
+ `width:${breedte}%`,
128
+ uitlijning === "right" || uitlijning === "center" ? "margin-left:auto" : "",
129
+ uitlijning === "left" || uitlijning === "center" ? "margin-right:auto" : "",
130
+ `margin-top:${marge}mm`,
131
+ `margin-bottom:${marge}mm`,
132
+ padding ? `padding:${padding}mm` : "",
133
+ o["blok_achtergrond"] ? `background:${String(o["blok_achtergrond"])}` : "",
134
+ rand ? `border:0.3mm solid ${String(o["blok_randkleur"] ?? "#e5e7eb")}` : "",
135
+ radius ? `border-radius:${radius}mm` : "",
136
+ schaal !== 100 ? `font-size:${schaal}%` : "",
137
+ ]
138
+ .filter(Boolean)
139
+ .join(";");
140
+ return `<div class="blok" style="${stijl}">${inhoud}</div>`;
141
+ }
142
+ function titelHtml(s) {
143
+ return s.titel ? `<div class="sectitel">${esc(s.titel)}</div>` : "";
144
+ }
145
+ // Vaste kolombreedtes: zorgt dat elke tabel (elke laag/kar) exact dezelfde
146
+ // kolomuitlijning heeft, ook als de koppen maar één keer getoond worden.
147
+ function kolBreedtes(kols) {
148
+ const opgegeven = kols.map((k) => (Number(k.breedte) > 0 ? Number(k.breedte) : 0));
149
+ const som = opgegeven.reduce((a, b) => a + b, 0);
150
+ const leeg = opgegeven.filter((b) => b === 0).length;
151
+ if (!leeg && som > 0)
152
+ return opgegeven.map((b) => (b / som) * 100);
153
+ const rest = Math.max(0, 100 - som);
154
+ const per = leeg ? rest / leeg : 0;
155
+ const ruw = opgegeven.map((b) => (b === 0 ? per : b));
156
+ const totaal = ruw.reduce((a, b) => a + b, 0) || 1;
157
+ return ruw.map((b) => (b / totaal) * 100);
158
+ }
159
+ function colgroupHtml(kols) {
160
+ const b = kolBreedtes(kols);
161
+ return `<colgroup>${b.map((w) => `<col style="width:${w.toFixed(4)}%">`).join("")}</colgroup>`;
162
+ }
163
+ function rij(label, waarde) {
164
+ if (waarde === null || waarde === undefined || waarde === "")
165
+ return "";
166
+ return `<div class="kv"><span class="k">${esc(label)}</span><span class="v">${esc(waarde)}</span></div>`;
167
+ }
168
+ function regelWeergave(producten, kolommen, weergave, toonKoppen, zebra, randen) {
169
+ const kols = kolommen.length ? kolommen : [];
170
+ if (!kols.length || !producten.length)
171
+ return "";
172
+ const kstijl = (k) => [
173
+ "",
174
+ `text-align:${k.uitlijning ?? "left"}`,
175
+ k.fontfamilie ? `font-family:${k.fontfamilie}` : "",
176
+ k.fontgrootte ? `font-size:${k.fontgrootte}pt` : "",
177
+ k.kleur ? `color:${k.kleur}` : "",
178
+ k.vet ? "font-weight:700" : "",
179
+ k.achtergrond ? `background:${k.achtergrond}` : "",
180
+ ]
181
+ .filter(Boolean)
182
+ .join(";");
183
+ if (weergave === "tabel" || weergave === "compact") {
184
+ const kop = toonKoppen
185
+ ? `<thead><tr>${kols
186
+ .map((k) => `<th style="${kstijl(k)}" class="${randen ? "rand" : "onder"}">${esc(k.label)}</th>`)
187
+ .join("")}</tr></thead>`
188
+ : "";
189
+ const body = producten
190
+ .map((p, i) => `<tr${zebra && i % 2 === 1 ? ' class="zebra"' : ""}>${kols
191
+ .map((k) => `<td style="${kstijl(k)}" class="${randen ? "rand" : "onder"}">${esc(cel(p, k.key))}</td>`)
192
+ .join("")}</tr>`)
193
+ .join("");
194
+ return `<table class="regels vast ${weergave === "compact" ? "compact" : ""}">${colgroupHtml(kols)}${kop}<tbody>${body}</tbody></table>`;
195
+ }
196
+ const items = producten
197
+ .map((p) => `<div class="regelrij">${kols
198
+ .map((k) => `<span style="${kstijl(k)}">${esc(cel(p, k.key))}</span>`)
199
+ .join("")}</div>`)
200
+ .join("");
201
+ return weergave === "naast" ? `<div class="tweekolom">${items}</div>` : `<div>${items}</div>`;
202
+ }
203
+ function groepeer(order, groepering, sortering) {
204
+ const alle = order.karren.flatMap((k) => k.lagen.flatMap((l) => l.producten.map((p) => ({ p, kar: k, laag: l }))));
205
+ const maak = (titel, items) => ({
206
+ titel,
207
+ producten: sorteerProducten(items, sortering),
208
+ });
209
+ if (groepering === "kar")
210
+ return order.karren.map((k) => maak(`Kar ${k.karnummer}`, k.lagen.flatMap((l) => l.producten)));
211
+ if (groepering === "laag") {
212
+ const map = new Map();
213
+ for (const r of alle) {
214
+ const t = `Kar ${r.kar.karnummer} · laag ${r.laag.laagnummer}`;
215
+ map.set(t, [...(map.get(t) ?? []), r.p]);
216
+ }
217
+ return [...map.entries()].map(([t, items]) => maak(t, items));
218
+ }
219
+ if (groepering === "productgroep") {
220
+ const map = new Map();
221
+ for (const r of alle)
222
+ map.set(r.p.productgroep, [...(map.get(r.p.productgroep) ?? []), r.p]);
223
+ return [...map.entries()].map(([t, items]) => maak(t, items));
224
+ }
225
+ if (groepering === "afleveradres")
226
+ return [maak(order.afleveradres.naam, alle.map((r) => r.p))];
227
+ if (groepering === "klant")
228
+ return [maak(order.klant.naam, alle.map((r) => r.p))];
229
+ return [maak("—", alle.map((r) => r.p))];
230
+ }
231
+ function fustTabel(velden, fusten, titel) {
232
+ if (!velden.length || !fusten.length)
233
+ return "";
234
+ const label = {
235
+ fusttype: "Fustsoort",
236
+ aantal: "Aantal",
237
+ hoogte: "Hoogte",
238
+ belading: "Belading",
239
+ barcode: "Barcode",
240
+ };
241
+ const kop = `<tr>${velden.map((v) => `<th class="onder">${esc(label[v] ?? v)}</th>`).join("")}</tr>`;
242
+ const rijen = fusten
243
+ .map((f) => `<tr>${velden
244
+ .map((v) => `<td class="onder">${esc(f[v] ?? "")}</td>`)
245
+ .join("")}</tr>`)
246
+ .join("");
247
+ return `${titel ? `<div class="sectitel">${esc(titel)}</div>` : ""}<table class="regels">${kop}${rijen}</table>`;
248
+ }
249
+ function productenHtml(sectie, laag, toonKop = true) {
250
+ const velden = lijst(sectie, "velden");
251
+ if (!velden.length)
252
+ return "";
253
+ let producten = sorteerProducten(laag.producten, optS(sectie, "sortering", "eigen"));
254
+ if (opt(sectie, "groeperen", false)) {
255
+ const map = new Map();
256
+ for (const p of producten) {
257
+ const b = map.get(p.artikelnummer);
258
+ if (b) {
259
+ b.aantal += p.aantal;
260
+ b.fust_aantal += p.fust_aantal;
261
+ }
262
+ else
263
+ map.set(p.artikelnummer, { ...p });
264
+ }
265
+ producten = [...map.values()];
266
+ }
267
+ if (!producten.length)
268
+ return "";
269
+ const label = (v) => v === "foto" ? "Foto" : v === "qrcode" ? "QR-code" : PRODUCT_LABELS[v] ?? v;
270
+ const weergave = optS(sectie, "weergave", "tabel");
271
+ if (weergave === "lijst") {
272
+ return `<ul class="plijst">${producten
273
+ .map((p) => `<li>${velden.map((v) => esc(cel(p, v))).filter(Boolean).join(" · ")}</li>`)
274
+ .join("")}</ul>`;
275
+ }
276
+ const kop = toonKop && opt(sectie, "toonKoppen", true) !== false
277
+ ? `<tr>${velden.map((v) => `<th class="onder">${esc(label(v))}</th>`).join("")}</tr>`
278
+ : "";
279
+ const breedtes = productVeldBreedtes(velden);
280
+ const colgroup = `<colgroup>${velden
281
+ .map((_, index) => `<col style="width:${(breedtes[index] ?? 0).toFixed(4)}%">`)
282
+ .join("")}</colgroup>`;
283
+ const rijen = producten
284
+ .map((p) => `<tr>${velden.map((v) => `<td class="onder">${esc(cel(p, v))}</td>`).join("")}</tr>`)
285
+ .join("");
286
+ return `<table class="regels vast">${colgroup}${kop}${rijen}</table>`;
287
+ }
288
+ const PRODUCT_LABELS = {
289
+ artikelnummer: "Artikelnr",
290
+ artikelnaam: "Artikel",
291
+ potmaat: "Potmaat",
292
+ aantal: "Aantal",
293
+ belading: "Belading",
294
+ barcode: "Barcode",
295
+ opmerking: "Opmerking",
296
+ locatie: "Locatie",
297
+ gewicht: "Gewicht",
298
+ productgroep: "Productgroep",
299
+ kleur: "Kleur",
300
+ fust_aantal: "Aantal fusten",
301
+ fustsoort: "Fustsoort",
302
+ planten_per_fust: "Planten per fust",
303
+ };
304
+ function karKop(s, kar, velden, bundelTekst = "") {
305
+ const scheiding = optS(s, "scheiding", "kop");
306
+ const tekst = optS(s, "koptekst", "KAR {kar}")
307
+ .replace(/\{kar\}/g, String(kar.karnummer))
308
+ .replace(/\{route\}/g, kar.route)
309
+ .replace(/\{chauffeur\}/g, kar.chauffeur)
310
+ .replace(/\{lagen\}/g, String(kar.lagen.length))
311
+ .replace(/\{producten\}/g, String(kar.lagen.reduce((n, l) => n + l.producten.length, 0)));
312
+ const info = [];
313
+ if (velden.includes("lagen"))
314
+ info.push(`${kar.lagen.length} lagen`);
315
+ if (velden.includes("producten"))
316
+ info.push(`${kar.lagen.reduce((n, l) => n + l.producten.reduce((m, p) => m + p.aantal, 0), 0)} producten`);
317
+ if (velden.includes("gewicht") && kar.gewicht)
318
+ info.push(kar.gewicht);
319
+ if (velden.includes("chauffeur") && kar.chauffeur)
320
+ info.push(kar.chauffeur);
321
+ if (velden.includes("route") && kar.route)
322
+ info.push(kar.route);
323
+ const toonNummer = velden.includes("karnummer");
324
+ if (!toonNummer && !info.length && !bundelTekst)
325
+ return "";
326
+ const groot = scheiding === "kop";
327
+ const gekleurd = scheiding === "kleur" || groot;
328
+ const stijl = [
329
+ gekleurd ? `background:${optS(s, "kleur", "#e8eef7")}` : "",
330
+ groot ? "font-size:117%;font-weight:700" : "font-weight:600",
331
+ ]
332
+ .filter(Boolean)
333
+ .join(";");
334
+ const links = [
335
+ toonNummer ? esc(tekst) : "",
336
+ bundelTekst ? `<span class="bundel">${esc(bundelTekst)}</span>` : "",
337
+ ]
338
+ .filter(Boolean)
339
+ .join(" ");
340
+ return `<div class="karkop" style="${stijl}"><span>${links}</span><span class="meta">${info
341
+ .map((t) => `<span>${esc(t)}</span>`)
342
+ .join("")}</span></div>`;
343
+ }
344
+ function laagKop(s, laag, velden, compact, bundelTekst = "") {
345
+ const tekst = optS(s, "koptekst", "Laag {laag}").replace(/\{laag\}/g, String(laag.laagnummer));
346
+ const info = [];
347
+ if (velden.includes("belading") && laag.belading)
348
+ info.push(`Belading ${laag.belading}`);
349
+ if (velden.includes("potmaat") && laag.potmaat)
350
+ info.push(laag.potmaat);
351
+ if (velden.includes("producten"))
352
+ info.push(`${laag.producten.reduce((n, p) => n + p.aantal, 0)} producten`);
353
+ if (velden.includes("fusten"))
354
+ info.push(`${laag.fusten.reduce((n, f) => n + f.aantal, 0)} fusten`);
355
+ if (velden.includes("gewicht") && laag.gewicht)
356
+ info.push(laag.gewicht);
357
+ if (velden.includes("opmerking") && laag.opmerking)
358
+ info.push(laag.opmerking);
359
+ if (!velden.includes("laagnummer") && !info.length && !bundelTekst)
360
+ return "";
361
+ return `<div class="laagkop${compact ? " compact" : ""}"><span>${velden.includes("laagnummer") ? esc(tekst) : ""}${bundelTekst ? ` <span class="bundel">${esc(bundelTekst)}</span>` : ""}</span><span class="meta">${info.map((t) => `<span>${esc(t)}</span>`).join("")}</span></div>`;
362
+ }
363
+ function bundelMelding(tekst) {
364
+ return `<div class="bundel">${esc(tekst)}</div>`;
365
+ }
366
+ function karBlok(s, kar, lagen, zoek, omkaderd, aantal = 1) {
367
+ // Bij samenvoegen van identieke karren vervalt het unieke karnummer.
368
+ const bundelt = opt(s, "bundel_identiek", true) !== false;
369
+ const velden = lijst(s, "velden").filter((v) => !(bundelt && (v === "karnummer" || v === "barcode" || v === "qrcode")));
370
+ const lagenSectie = zoek("lagen");
371
+ const productSectie = zoek("producten");
372
+ const fustSectie = zoek("fusten");
373
+ const lagenModus = lagenSectie ? optS(lagenSectie, "modus", "apart") : "geen";
374
+ let inhoud = karKop(s, kar, velden, karBundelTekst(aantal));
375
+ if (lagen.length && lagenSectie && lagenModus !== "geen") {
376
+ const lVelden = lijst(lagenSectie, "velden");
377
+ const toonProducten = lagenModus !== "nummer" && lagenModus !== "regel";
378
+ const bundelLagen = lagenModus === "gebundeld";
379
+ inhoud += groepeerIdentiekeLagen(lagen, bundelLagen)
380
+ .map(({ laag, aantal: laagAantal }, index) => {
381
+ let l = laagKop(lagenSectie, laag, lVelden, lagenModus === "regel", laagAantal > 1 ? laagBundelTekst(laagAantal) : "");
382
+ if (toonProducten && productSectie)
383
+ l += productenHtml(productSectie, laag, index === 0 || opt(productSectie, "kop_eenmalig", true) === false);
384
+ if (fustSectie && ["laag", "product"].includes(optS(fustSectie, "modus", "totaal")))
385
+ l += fustTabel(lijst(fustSectie, "velden"), laag.fusten, `Fusten laag ${laag.laagnummer}`);
386
+ return `<div class="laag">${l}</div>`;
387
+ })
388
+ .join("");
389
+ }
390
+ if (fustSectie && optS(fustSectie, "modus", "totaal") === "kar")
391
+ inhoud += fustTabel(lijst(fustSectie, "velden"), lagen.flatMap((l) => l.fusten), `Fusten kar ${kar.karnummer}`);
392
+ return `<div class="karblok${omkaderd ? " omkaderd" : ""}">${inhoud}</div>`;
393
+ }
394
+ const FUST_KOLOM_LABELS = {
395
+ artikelnummer: "Artikel",
396
+ artikelnaam: "Omschrijving",
397
+ potmaat: "Potmaat",
398
+ kleur: "Kleur",
399
+ productgroep: "Productgroep",
400
+ locatie: "Locatie",
401
+ fustsoort: "Tray/fust",
402
+ planten_per_fust: "Pl./fust",
403
+ aantal: "Planten",
404
+ };
405
+ const FUST_RECHTS = new Set(["planten_per_fust", "aantal"]);
406
+ /** Verzamellijst met alle te picken fusten per fusttype. */
407
+ function fustVerzamellijstHtml(order, s) {
408
+ const groepen = fustVerzamellijst(order?.karren);
409
+ if (!groepen.length)
410
+ return "";
411
+ const gekozen = s ? lijst(s, "velden") : [];
412
+ const kolommen = (gekozen.length ? gekozen : Object.keys(FUST_KOLOM_LABELS)).filter((k) => FUST_KOLOM_LABELS[k]);
413
+ const kopTekst = s
414
+ ? String(s.opties?.["kop"] ?? "Te verzamelen fusten — karindeling zelf bepalen")
415
+ : "Te verzamelen fusten — karindeling zelf bepalen";
416
+ const toonHoogte = s ? opt(s, "toonFusthoogte", true) !== false : true;
417
+ const toonTotaal = s ? opt(s, "toonTotaal", true) !== false : true;
418
+ const totaal = groepen.reduce((n, f) => n + f.aantal, 0);
419
+ const blokken = groepen
420
+ .map((f) => {
421
+ const kop = `<div class="sectitel" style="display:flex;justify-content:space-between"><span>${esc([f.fusttype, toonHoogte ? f.hoogte : "", toonHoogte ? f.belading : ""]
422
+ .filter(Boolean)
423
+ .join(" · "))}</span><span>${f.aantal}× fust</span></div>`;
424
+ if (!kolommen.length)
425
+ return kop;
426
+ const koppen = kolommen
427
+ .map((k) => `<th class="onder"${FUST_RECHTS.has(k) ? ' style="text-align:right"' : ""}>${esc(FUST_KOLOM_LABELS[k])}</th>`)
428
+ .join("");
429
+ const rijen = f.producten
430
+ .map((pr) => `<tr>${kolommen
431
+ .map((k) => `<td class="onder"${FUST_RECHTS.has(k) ? ' style="text-align:right"' : ""}>${esc(String(pr[k] ?? ""))}</td>`)
432
+ .join("")}</tr>`)
433
+ .join("");
434
+ return `${kop}<table class="regels"><tr>${koppen}</tr>${rijen}</table>`;
435
+ })
436
+ .join("");
437
+ const totaalHtml = toonTotaal
438
+ ? `<table class="regels"><tr><td style="font-weight:700">Totaal fusten</td><td style="font-weight:700;text-align:right">${totaal}</td></tr></table>`
439
+ : "";
440
+ return `${kopTekst ? `<div class="sectitel">${esc(kopTekst)}</div>` : ""}${blokken}${totaalHtml}`;
441
+ }
442
+ function karrenHtml(s, order, zoek) {
443
+ const modus = optS(s, "modus", "pagina");
444
+ if (modus === "geen")
445
+ return "";
446
+ const scheiding = optS(s, "scheiding", "kop");
447
+ const lagenSectie = zoek("lagen");
448
+ const lagenModus = lagenSectie ? optS(lagenSectie, "modus", "apart") : "geen";
449
+ const perPagina = modus === "pagina" || scheiding === "pagina";
450
+ const perLaagPagina = modus !== "nummer" && lagenModus === "pagina";
451
+ const bundelKarren = opt(s, "bundel_identiek", true) !== false;
452
+ const karGroepen = groepeerIdentiekeKarren(order.karren, bundelKarren);
453
+ if (modus === "naast") {
454
+ return blokFrame(s, `<div class="tweekolom">${karGroepen
455
+ .map((g) => karBlok(s, g.kar, g.kar.lagen, zoek, false, g.aantal))
456
+ .join("")}</div>`);
457
+ }
458
+ const delen = [];
459
+ karGroepen.forEach((groep, i) => {
460
+ const kar = groep.kar;
461
+ const lagen = modus === "nummer" ? [] : kar.lagen;
462
+ if (perLaagPagina && lagen.length) {
463
+ const laagGroepen = groepeerIdentiekeLagen(lagen, String(lagenModus) === "gebundeld");
464
+ laagGroepen.forEach((lg, j) => {
465
+ const breek = i > 0 || j > 0;
466
+ delen.push(`<div class="${breek ? "breekvoor" : ""}">${blokFrame(s, karBlok(s, kar, [lg.laag], zoek, modus === "blok" || modus === "passend", groep.aantal))}</div>`);
467
+ });
468
+ return;
469
+ }
470
+ const breek = i > 0 && perPagina;
471
+ const scheider = i > 0 && !perPagina ? scheidingHtml(scheiding) : "";
472
+ delen.push(`<div class="${breek ? "breekvoor" : ""} ${modus === "passend" ? "heelhouden" : ""}">${scheider}${blokFrame(s, karBlok(s, kar, lagen, zoek, modus === "blok" || modus === "passend", groep.aantal))}</div>`);
473
+ });
474
+ return delen.join("");
475
+ }
476
+ function scheidingHtml(modus) {
477
+ if (modus === "lijn")
478
+ return `<div class="lijn"></div>`;
479
+ if (modus === "balk")
480
+ return `<div class="balk"></div>`;
481
+ return "";
482
+ }
483
+ function sectieHtml(s, order, zoek, opties) {
484
+ switch (s.type) {
485
+ case "logo": {
486
+ const logoId = typeof s.opties?.["logoId"] === "string" ? String(s.opties["logoId"]) : null;
487
+ // Geen eigen logo gekozen? Dan niets tonen — het documentlogo staat al bovenaan.
488
+ if (!logoId)
489
+ return "";
490
+ const logos = arr(opties.logos);
491
+ const logo = logos.find((l) => l.id === logoId);
492
+ if (!logo?.viewUrl)
493
+ return "";
494
+ const align = optS(s, "uitlijning", "left");
495
+ return `<div style="text-align:${esc(align)}"><img src="${esc(logo.viewUrl)}" alt="${esc(logo.naam ?? "")}" style="height:${Number(opt(s, "hoogte", 18))}mm;object-fit:contain" /></div>`;
496
+ }
497
+ case "bedrijfsgegevens":
498
+ return (titelHtml(s) +
499
+ [
500
+ opt(s, "toonNaam", true) ? `<div class="vet">${esc(order.bedrijf.naam)}</div>` : "",
501
+ opt(s, "toonAdres", true)
502
+ ? `<div>${esc([order.bedrijf.adres, order.bedrijf.plaats].filter(Boolean).join(" · "))}</div>`
503
+ : "",
504
+ opt(s, "toonContact", true)
505
+ ? `<div>${esc([order.bedrijf.telefoon, order.bedrijf.email].filter(Boolean).join(" · "))}</div>`
506
+ : "",
507
+ opt(s, "toonKvk", false)
508
+ ? `<div>${esc([order.bedrijf.kvk, order.bedrijf.btw].filter(Boolean).join(" · "))}</div>`
509
+ : "",
510
+ ].join(""));
511
+ case "klantgegevens":
512
+ return (titelHtml(s) +
513
+ `<div class="vet">${esc(order.klant.naam)}</div>` +
514
+ (opt(s, "toonKlantnummer", true) ? rij("Klantnummer", order.klant.nummer) : "") +
515
+ (opt(s, "toonContact", true) ? rij("Contact", order.klant.contact) : "") +
516
+ (opt(s, "toonReferentie", true) ? rij("Referentie", order.klant.referentie) : ""));
517
+ case "afleveradres":
518
+ case "factuuradres": {
519
+ const a = s.type === "afleveradres" ? order.afleveradres : order.factuuradres;
520
+ return (titelHtml(s) +
521
+ `<div class="vet">${esc(a.naam)}</div><div>${esc(a.adres)}</div><div>${esc(a.plaats)}</div>` +
522
+ (opt(s, "toonLand", true) ? `<div>${esc(a.land)}</div>` : ""));
523
+ }
524
+ case "orderinformatie":
525
+ return (titelHtml(s) +
526
+ `<div class="grid2">${[
527
+ opt(s, "toonOrdernummer", true) ? rij("Ordernummer", order.order.nummer) : "",
528
+ opt(s, "toonDatum", true) ? rij("Orderdatum", order.order.datum) : "",
529
+ opt(s, "toonLeverdatum", true) ? rij("Leverdatum", order.order.leverdatum) : "",
530
+ opt(s, "toonReferentie", true) ? rij("Referentie", order.order.referentie) : "",
531
+ ].join("")}</div>`);
532
+ case "transportinformatie":
533
+ return (titelHtml(s) +
534
+ `<div class="grid2">${[
535
+ opt(s, "toonTransporteur", true) ? rij("Transporteur", order.transport.transporteur) : "",
536
+ opt(s, "toonRoute", true) ? rij("Route", order.transport.route) : "",
537
+ opt(s, "toonChauffeur", false) ? rij("Chauffeur", order.transport.chauffeur) : "",
538
+ opt(s, "toonTrack", false) ? rij("Track & trace", order.transport.track) : "",
539
+ ].join("")}</div>`);
540
+ case "opmerkingen":
541
+ if (!order.order.opmerking)
542
+ return "";
543
+ return `<div class="${opt(s, "kader", true) ? "kader" : ""}"><span class="vet">Opmerkingen: </span>${esc(order.order.opmerking)}</div>`;
544
+ case "orderregels": {
545
+ const kolommen = arr(s.kolommen);
546
+ const groepen = groepeer(order, optS(s, "groepering", "order"), optS(s, "sortering", "eigen"));
547
+ const body = groepen
548
+ .map((g) => {
549
+ const tabel = regelWeergave(g.producten, kolommen, optS(s, "weergave", "tabel"), opt(s, "toonKoppen", true) !== false, !!opt(s, "zebra", false), opt(s, "randen", true) !== false);
550
+ if (!tabel)
551
+ return "";
552
+ return `<div class="groep">${g.titel !== "—" ? `<div class="vet">${esc(g.titel)}</div>` : ""}${tabel}</div>`;
553
+ })
554
+ .join("");
555
+ return titelHtml(s) + body;
556
+ }
557
+ case "pickmethode":
558
+ if (optS(s, "methode", "kar") !== "fusten")
559
+ return "";
560
+ return fustVerzamellijstHtml(order, s);
561
+ case "karren":
562
+ case "lagen":
563
+ case "producten":
564
+ return ""; // apart afgehandeld
565
+ case "fusten": {
566
+ if (optS(s, "modus", "totaal") !== "totaal")
567
+ return "";
568
+ const alle = order.karren.flatMap((k) => k.lagen.flatMap((l) => l.fusten));
569
+ return fustTabel(lijst(s, "velden"), alle, s.titel);
570
+ }
571
+ case "totalen": {
572
+ const t = totalen(order);
573
+ const perFust = optS(zoek("pickmethode") ?? s, "methode", "kar") === "fusten";
574
+ const velden = lijst(s, "velden").filter((v) => !(perFust && (v === "karren" || v === "lagen")));
575
+ if (!velden.length)
576
+ return "";
577
+ const rechts = optS(s, "uitlijning", "right") === "right";
578
+ return `<div class="totalen" style="justify-content:${rechts ? "flex-end" : "flex-start"}">${velden
579
+ .map((v) => `<span>${esc(t[v] ?? v)}</span>`)
580
+ .join("")}</div>`;
581
+ }
582
+ case "handtekening":
583
+ return (`<div class="handtekening">${opt(s, "label", "") ? `<div class="sectitel">${esc(opt(s, "label", ""))}</div>` : ""}` +
584
+ (opt(s, "toonNaam", true) ? `<div class="lijnveld">Naam</div>` : "") +
585
+ (opt(s, "toonDatum", true) ? `<div class="lijnveld">Datum</div>` : "") +
586
+ `<div class="lijnveld hoog"></div></div>`);
587
+ case "tekstblok":
588
+ return `<div class="${opt(s, "kader", false) ? "kader" : ""}" style="font-size:${Number(opt(s, "fontgrootte", 9))}pt">${esc(vulVeldcodes(optS(s, "tekst", ""), order))}</div>`;
589
+ case "qrcode":
590
+ return `<div><div class="qr" style="width:${Number(opt(s, "grootte", 25))}mm;height:${Number(opt(s, "grootte", 25))}mm"></div><div class="klein">${esc(vulVeldcodes(optS(s, "waarde", ""), order))}</div></div>`;
591
+ case "barcode":
592
+ return `<div><div class="streepjes" style="height:${Number(opt(s, "hoogte", 15))}mm"></div><div>${esc(vulVeldcodes(optS(s, "waarde", ""), order))}</div></div>`;
593
+ case "afbeelding": {
594
+ const url = optS(s, "url", "");
595
+ return url
596
+ ? `<img src="${esc(url)}" alt="${esc(s.titel)}" style="height:${Number(opt(s, "hoogte", 25))}mm" />`
597
+ : "";
598
+ }
599
+ default:
600
+ return "";
601
+ }
602
+ }
603
+ // --- publieke API -----------------------------------------------------------
604
+ /** Bouwt alleen de inhoud (zonder <html>-wrapper) van de pakbon. */
605
+ export function renderPakbonBody(config, order, opties = {}) {
606
+ const cfg = leesPakbonConfig(config);
607
+ const ctx = conditieContext(order);
608
+ const alleSecties = arr(cfg.secties);
609
+ const methode = pickMethode(alleSecties);
610
+ const secties = alleSecties.filter((s) => sectieZichtbaar(s, ctx) && !sectieOnderdrukt(s.type, methode));
611
+ const zoek = (t) => secties.find((s) => s.type === t);
612
+ const opts = { ...opties, logoId: opties.logoId ?? cfg.logo?.logoId ?? null };
613
+ const kop = documentLogoHtml(cfg.logo, opts);
614
+ const totalenSectie = zoek("totalen");
615
+ const totalenElkePagina = !!totalenSectie && opt(totalenSectie, "elke_pagina", false) === true;
616
+ const inhoud = secties
617
+ .filter((s) => !(s.type === "totalen" && totalenElkePagina))
618
+ .map((s) => s.type === "karren" ? karrenHtml(s, order, zoek) : blokFrame(s, sectieHtml(s, order, zoek, opts)))
619
+ .join("");
620
+ const voet = totalenElkePagina && totalenSectie
621
+ ? `<div class="paginavoet">${sectieHtml(totalenSectie, order, zoek, opts)}</div>`
622
+ : "";
623
+ return kop + inhoud + voet;
624
+ }
625
+ /** Het documentlogo bovenaan de pakbon. */
626
+ function documentLogoHtml(logoCfg, opties) {
627
+ if (!logoCfg?.logoId)
628
+ return "";
629
+ const logos = arr(opties.logos);
630
+ const logo = logos.find((l) => l.id === logoCfg.logoId);
631
+ if (!logo?.viewUrl)
632
+ return "";
633
+ return `<div class="blok" style="text-align:${esc(logoCfg.uitlijning ?? "left")}"><img src="${esc(logo.viewUrl)}" alt="${esc(logo.naam ?? "")}" style="height:${Number(logoCfg.hoogte ?? 18)}mm;object-fit:contain" /></div>`;
634
+ }
635
+ /** Bouwt een volledig printbaar HTML-document van de pakbon. */
636
+ export function renderPakbonDocument(config, order, opties = {}) {
637
+ const b = opties.breedteMm ?? 210;
638
+ const h = opties.hoogteMm ?? 297;
639
+ const marge = opties.margeMm ?? 12;
640
+ const font = opties.lettertype ?? 'Arial, Helvetica, "Segoe UI", sans-serif';
641
+ const grootte = opties.lettergrootte ?? 8.5;
642
+ const body = renderPakbonBody(config, order, opties);
643
+ return `<!doctype html><html lang="nl"><head><meta charset="utf-8" />
644
+ <title>${esc(opties.titel ?? "Pakbon")}</title>
645
+ <style>
646
+ @page { size: ${b}mm ${h}mm; margin: ${marge}mm; }
647
+ * { box-sizing: border-box; }
648
+ html, body { margin: 0; padding: 0; }
649
+ body { font-family: ${font}; font-size: ${grootte}pt; color: #111827; line-height: 1.35; }
650
+ .blok { margin-bottom: 2mm; }
651
+ .sectitel { font-size: 90%; font-weight: 600; text-transform: uppercase; color: #6b7280; margin-bottom: 0.6mm; }
652
+ .vet { font-weight: 600; }
653
+ .kv { display: flex; justify-content: space-between; gap: 3mm; }
654
+ .kv .k { color: #6b7280; }
655
+ .kv .v { font-weight: 500; }
656
+ .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 6mm; }
657
+ .kader { border: 0.25mm solid #d1d5db; border-radius: 1mm; padding: 1.5mm; }
658
+ table.regels { width: 100%; border-collapse: collapse; }
659
+ table.regels.vast { table-layout: fixed; }
660
+ table.regels.vast td, table.regels.vast th { overflow-wrap: anywhere; }
661
+ table.regels th, table.regels td { padding: 0.7mm 1mm; vertical-align: top; }
662
+ table.regels th { font-size: 90%; text-transform: uppercase; color: #6b7280; font-weight: 600; text-align: left; }
663
+ .rand { border: 0.2mm solid #d1d5db; }
664
+ .onder { border-bottom: 0.2mm solid #d1d5db; }
665
+ table.regels.compact th, table.regels.compact td { padding: 0.3mm 0.8mm; }
666
+ tr.zebra td { background: #f3f4f6; }
667
+ .plijst { margin: 0; padding-left: 4mm; }
668
+ .groep { margin-bottom: 1.5mm; }
669
+ .regelrij { display: flex; gap: 2mm; }
670
+ .tweekolom { display: grid; grid-template-columns: 1fr 1fr; gap: 3mm; }
671
+ .karblok { margin-bottom: 2mm; }
672
+ .karblok.omkaderd { border: 0.25mm solid #d1d5db; border-radius: 1mm; padding: 1.5mm; }
673
+ .karkop { display: flex; justify-content: space-between; align-items: center; padding: 0.8mm 1.5mm; border-radius: 1mm; margin-bottom: 1mm; }
674
+ .bundel { display: inline-block; border: 0.3mm dashed #6b7280; border-radius: 1mm; padding: 0.4mm 1.5mm; margin-bottom: 1mm; font-weight: 700; text-transform: uppercase; letter-spacing: 0.2mm; font-size: 90%; }
675
+ .laag { margin-bottom: 1.5mm; }
676
+ .laagkop { display: flex; justify-content: space-between; font-weight: 500; border-bottom: 0.2mm solid #d1d5db; padding-bottom: 0.4mm; margin-bottom: 0.6mm; }
677
+ .laagkop.compact { border-bottom: none; }
678
+ .meta { display: flex; gap: 3mm; font-size: 88%; font-weight: 400; color: #4b5563; }
679
+ .totalen { display: flex; flex-wrap: wrap; gap: 5mm; border-top: 0.25mm solid #d1d5db; padding-top: 1.2mm; font-weight: 500; }
680
+ .paginavoet { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; }
681
+ .handtekening { margin-top: 3mm; }
682
+ .lijnveld { margin-top: 4mm; border-bottom: 0.25mm solid #6b7280; padding-bottom: 3mm; }
683
+ .lijnveld.hoog { padding-bottom: 10mm; }
684
+ .lijn { border-top: 0.25mm solid #d1d5db; margin: 1.5mm 0; }
685
+ .balk { height: 1mm; background: #374151; margin: 1.5mm 0; }
686
+ .qr { border: 0.25mm dashed #9ca3af; }
687
+ .streepjes { width: 45mm; background: repeating-linear-gradient(to right, #111827 0 2px, transparent 2px 5px); }
688
+ .klein { font-size: 85%; color: #6b7280; }
689
+ .breekvoor { break-before: page; page-break-before: always; }
690
+ .heelhouden { break-inside: avoid; page-break-inside: avoid; }
691
+ img { max-width: 100%; }
692
+ </style></head>
693
+ <body>${body}</body></html>`;
694
+ }
695
+ /** Handig voor testen: het document met de voorbeeldgegevens. */
696
+ export function voorbeeldPakbonOrder() {
697
+ return DEMO_ORDER;
698
+ }
699
+ /**
700
+ * Opent het printvenster met de pakbon. Andere modules hoeven hiervoor niets
701
+ * zelf te bouwen: layoutconfiguratie + ordergegevens erin, afdruk eruit.
702
+ */
703
+ export function printPakbon(config, order, opties = {}) {
704
+ if (typeof window === "undefined")
705
+ return;
706
+ const html = renderPakbonDocument(config, order, opties);
707
+ const frame = document.createElement("iframe");
708
+ frame.setAttribute("aria-hidden", "true");
709
+ frame.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0";
710
+ document.body.appendChild(frame);
711
+ const doc = frame.contentDocument;
712
+ if (!doc) {
713
+ frame.remove();
714
+ return;
715
+ }
716
+ doc.open();
717
+ doc.write(html);
718
+ doc.close();
719
+ const start = () => {
720
+ frame.contentWindow?.focus();
721
+ frame.contentWindow?.print();
722
+ window.setTimeout(() => frame.remove(), 60000);
723
+ };
724
+ // Wachten tot afbeeldingen (logo's) geladen zijn, anders drukt de browser
725
+ // een pakbon af zonder logo.
726
+ const wachtOpAfbeeldingen = async () => {
727
+ const imgs = Array.from(doc.images ?? []);
728
+ await Promise.all(imgs.map((img) => img.complete
729
+ ? Promise.resolve()
730
+ : new Promise((res) => {
731
+ img.onload = () => res();
732
+ img.onerror = () => res();
733
+ })));
734
+ start();
735
+ };
736
+ window.setTimeout(() => void wachtOpAfbeeldingen(), 200);
737
+ }
738
+ /**
739
+ * Zelfde als `printPakbon`, maar haalt de logo's zelf uit de centrale Logo
740
+ * Bibliotheek en bedt ze in. Andere modules hoeven zo niets over logo's te
741
+ * weten: het documentlogo verschijnt automatisch op de afdruk.
742
+ */
743
+ export async function printPakbonMetLogos(config, order, opties = {}) {
744
+ let logos = opties.logos;
745
+ if (!Array.isArray(logos) || logos.length === 0) {
746
+ try {
747
+ logos = await haalPrintLogos();
748
+ }
749
+ catch {
750
+ logos = [];
751
+ }
752
+ }
753
+ printPakbon(config, order, { ...opties, logos });
754
+ }