@flowselections/layout-maker 1.32.0 → 1.38.4

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 (115) hide show
  1. package/README.md +139 -139
  2. package/dist-lib/components/AfdrukKnop.d.ts +19 -19
  3. package/dist-lib/components/ComputersLijst.d.ts.map +1 -1
  4. package/dist-lib/components/ComputersLijst.js +5 -5
  5. package/dist-lib/components/DocumentenPage.d.ts.map +1 -1
  6. package/dist-lib/components/DocumentenPage.js +3 -2
  7. package/dist-lib/components/LayoutPrinterSelect.d.ts +8 -0
  8. package/dist-lib/components/LayoutPrinterSelect.d.ts.map +1 -0
  9. package/dist-lib/components/LayoutPrinterSelect.js +23 -0
  10. package/dist-lib/components/LayoutsPage.d.ts.map +1 -1
  11. package/dist-lib/components/LayoutsPage.js +16 -3
  12. package/dist-lib/components/NetwerkPrintersDialog.d.ts +7 -7
  13. package/dist-lib/components/PaginaDocument.d.ts +4 -1
  14. package/dist-lib/components/PaginaDocument.d.ts.map +1 -1
  15. package/dist-lib/components/PaginaDocument.js +10 -5
  16. package/dist-lib/components/PakbonBuilderPage.d.ts.map +1 -1
  17. package/dist-lib/components/PakbonBuilderPage.js +6 -2
  18. package/dist-lib/components/PakbonPreview.d.ts +4 -2
  19. package/dist-lib/components/PakbonPreview.d.ts.map +1 -1
  20. package/dist-lib/components/PakbonPreview.js +55 -12
  21. package/dist-lib/components/PakbonSectiesPanel.d.ts +2 -2
  22. package/dist-lib/components/PakbonSectiesPanel.d.ts.map +1 -1
  23. package/dist-lib/components/PakbonSectiesPanel.js +7 -2
  24. package/dist-lib/components/PdfMapAutoSaver.d.ts +6 -0
  25. package/dist-lib/components/PdfMapAutoSaver.d.ts.map +1 -0
  26. package/dist-lib/components/PdfMapAutoSaver.js +12 -0
  27. package/dist-lib/components/PdfMapGlobaleBanner.d.ts +12 -0
  28. package/dist-lib/components/PdfMapGlobaleBanner.d.ts.map +1 -0
  29. package/dist-lib/components/PdfMapGlobaleBanner.js +30 -0
  30. package/dist-lib/components/PdfMapToestemmingBanner.d.ts +7 -0
  31. package/dist-lib/components/PdfMapToestemmingBanner.d.ts.map +1 -0
  32. package/dist-lib/components/PdfMapToestemmingBanner.js +139 -0
  33. package/dist-lib/components/PrintQueuePage.d.ts.map +1 -1
  34. package/dist-lib/components/PrintQueuePage.js +42 -3
  35. package/dist-lib/components/PrintenPage.d.ts.map +1 -1
  36. package/dist-lib/components/PrintenPage.js +3 -2
  37. package/dist-lib/components/PrinterSetupWizard.d.ts.map +1 -1
  38. package/dist-lib/components/PrinterSetupWizard.js +28 -5
  39. package/dist-lib/components/PrintersRedirectPage.d.ts +1 -1
  40. package/dist-lib/components/PrintersRedirectPage.js +14 -14
  41. package/dist-lib/components/PrintplekDetailPage.d.ts.map +1 -1
  42. package/dist-lib/components/PrintplekDetailPage.js +30 -4
  43. package/dist-lib/components/PrintplekWizard.d.ts.map +1 -1
  44. package/dist-lib/components/PrintplekWizard.js +81 -3
  45. package/dist-lib/components/ProductiebonBuilderPage.d.ts.map +1 -1
  46. package/dist-lib/components/ProductiebonBuilderPage.js +3 -1
  47. package/dist-lib/index.d.ts +10 -2
  48. package/dist-lib/index.d.ts.map +1 -1
  49. package/dist-lib/index.js +13 -2
  50. package/dist-lib/integrations/supabase/auth-middleware.d.ts +1396 -727
  51. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  52. package/dist-lib/integrations/supabase/client.server.d.ts +1396 -727
  53. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  54. package/dist-lib/integrations/supabase/client.server.js +1 -1
  55. package/dist-lib/integrations/supabase/moduleClient.d.ts +24008 -0
  56. package/dist-lib/integrations/supabase/moduleClient.d.ts.map +1 -0
  57. package/dist-lib/integrations/supabase/moduleClient.js +29 -0
  58. package/dist-lib/integrations/supabase/types.d.ts +1403 -750
  59. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  60. package/dist-lib/lib/aiLayout.functions.d.ts +1396 -727
  61. package/dist-lib/lib/aiLayout.functions.d.ts.map +1 -1
  62. package/dist-lib/lib/aiLayout.server.js +9 -9
  63. package/dist-lib/lib/browserPrint.js +1 -1
  64. package/dist-lib/lib/code128.d.ts +5 -0
  65. package/dist-lib/lib/code128.d.ts.map +1 -0
  66. package/dist-lib/lib/code128.js +51 -0
  67. package/dist-lib/lib/htmlPdf.server.d.ts.map +1 -1
  68. package/dist-lib/lib/htmlPdf.server.js +101 -6
  69. package/dist-lib/lib/layoutPreview.d.ts.map +1 -1
  70. package/dist-lib/lib/layoutPreview.js +11 -8
  71. package/dist-lib/lib/orderData.server.d.ts.map +1 -1
  72. package/dist-lib/lib/orderData.server.js +123 -10
  73. package/dist-lib/lib/pakbonBuilder.d.ts +28 -2
  74. package/dist-lib/lib/pakbonBuilder.d.ts.map +1 -1
  75. package/dist-lib/lib/pakbonBuilder.js +69 -8
  76. package/dist-lib/lib/pakbonHtml.d.ts +14 -2
  77. package/dist-lib/lib/pakbonHtml.d.ts.map +1 -1
  78. package/dist-lib/lib/pakbonHtml.js +155 -63
  79. package/dist-lib/lib/pakbonScan.d.ts +44 -0
  80. package/dist-lib/lib/pakbonScan.d.ts.map +1 -0
  81. package/dist-lib/lib/pakbonScan.js +105 -0
  82. package/dist-lib/lib/pdfJobSaver.d.ts +20 -0
  83. package/dist-lib/lib/pdfJobSaver.d.ts.map +1 -0
  84. package/dist-lib/lib/pdfJobSaver.js +173 -0
  85. package/dist-lib/lib/pdfMap.d.ts +48 -0
  86. package/dist-lib/lib/pdfMap.d.ts.map +1 -0
  87. package/dist-lib/lib/pdfMap.js +219 -0
  88. package/dist-lib/lib/pdfPrinter.d.ts +9 -0
  89. package/dist-lib/lib/pdfPrinter.d.ts.map +1 -0
  90. package/dist-lib/lib/pdfPrinter.js +34 -0
  91. package/dist-lib/lib/previewPdf.d.ts.map +1 -1
  92. package/dist-lib/lib/previewPdf.js +41 -9
  93. package/dist-lib/lib/printApi.d.ts.map +1 -1
  94. package/dist-lib/lib/printApi.js +9 -1
  95. package/dist-lib/lib/printQueue.server.d.ts +7 -0
  96. package/dist-lib/lib/printQueue.server.d.ts.map +1 -1
  97. package/dist-lib/lib/printQueue.server.js +346 -52
  98. package/dist-lib/lib/printService.functions.d.ts +5365 -2689
  99. package/dist-lib/lib/printService.functions.d.ts.map +1 -1
  100. package/dist-lib/lib/productiebonHtml.js +15 -15
  101. package/dist-lib/lib/profielHtml.js +18 -18
  102. package/dist-lib/lib/supabaseAdmin.server.d.ts +5 -5
  103. package/dist-lib/lib/supabaseAdmin.server.js +43 -43
  104. package/dist-lib/lib/supabaseClient.d.ts +1 -3
  105. package/dist-lib/lib/supabaseClient.d.ts.map +1 -1
  106. package/dist-lib/lib/supabaseClient.js +4 -8
  107. package/dist-lib/lib/testpagina.js +3 -3
  108. package/dist-lib/lib/testprint.js +4 -4
  109. package/dist-lib/lib/useDocumentData.d.ts +10 -0
  110. package/dist-lib/lib/useDocumentData.d.ts.map +1 -1
  111. package/dist-lib/lib/useDocumentData.js +19 -0
  112. package/dist-lib/lib/voorbeeldPrint.d.ts +4 -0
  113. package/dist-lib/lib/voorbeeldPrint.d.ts.map +1 -1
  114. package/dist-lib/lib/voorbeeldPrint.js +13 -2
  115. package/package.json +151 -151
@@ -4,6 +4,8 @@
4
4
  // de juiste printer. Draait alleen op de server (cron / interne aanroep).
5
5
  // ============================================================================
6
6
  import { maakPakbonOrder, renderPakbonDocument, voorbeeldPakbonOrder, } from "./pakbonHtml.js";
7
+ import { bouwPakbonScanInhoud, bewaarPakbonScanInhoud } from "./pakbonScan.js";
8
+ import { pakbonLayoutVariant } from "./pakbonBuilder.js";
7
9
  import { renderProductiebonDocument } from "./productiebonHtml.js";
8
10
  import { heeftOrderInhoud, laadOrderVanuitDatabase } from "./orderData.server.js";
9
11
  import { profielHeeftInhoud, renderProfielDocument } from "./profielHtml.js";
@@ -47,6 +49,13 @@ export class GeenDocumentFout extends Error {
47
49
  this.name = "GeenDocumentFout";
48
50
  }
49
51
  }
52
+ /** De Engine heeft de taak gepland, maar de layout is nog niet aangeleverd. */
53
+ export class WachtOpLayoutFout extends Error {
54
+ constructor() {
55
+ super(...arguments);
56
+ this.name = "WachtOpLayoutFout";
57
+ }
58
+ }
50
59
  /**
51
60
  * Oudere/andere benamingen voor hetzelfde document. De Logistiek Engine noemt
52
61
  * de afleverbrief bijvoorbeeld nog "verzendbrief"; hier vertalen we dat naar
@@ -65,6 +74,85 @@ export function normaliseerDocumenttype(type) {
65
74
  return "document";
66
75
  return DOCUMENTTYPE_ALIASSEN[t] ?? t;
67
76
  }
77
+ // ---------------------------------------------------------------------------
78
+ // Contract met de Logistics Engine.
79
+ // De Engine bepaalt documentsoort, variant, layout en printplek; deze module
80
+ // rendert en print alleen. Alles hieronder leest die keuzes uit de opdracht en
81
+ // overruled ze nooit.
82
+ // ---------------------------------------------------------------------------
83
+ /** Het layout-id zoals de Engine het aanlevert (kolom of payload). */
84
+ export function leesEngineLayoutId(job) {
85
+ const p = (job.payload ?? {});
86
+ const d = (p.data ?? {});
87
+ for (const k of [job.layout_id, p.layout_id, p.layoutId, d.layout_id, d.layoutId]) {
88
+ const v = String(k ?? "").trim();
89
+ if (v)
90
+ return v;
91
+ }
92
+ return "";
93
+ }
94
+ /** Documentsoort zoals de Engine hem noemt (`document_code`). */
95
+ function leesDocumentCode(job) {
96
+ const p = (job.payload ?? {});
97
+ const d = (p.data ?? {});
98
+ for (const k of [job.document_code, p.document_code, p.documentCode, d.document_code]) {
99
+ const v = String(k ?? "").trim();
100
+ if (v)
101
+ return v;
102
+ }
103
+ return "";
104
+ }
105
+ /** Is dit een opdracht die door de Logistics Engine is gepland? */
106
+ function isEngineOpdracht(job) {
107
+ const p = (job.payload ?? {});
108
+ if (leesDocumentCode(job))
109
+ return true;
110
+ const bron = String(p.bron_module ?? p.owner_module ?? p.source_module ?? "").toLowerCase();
111
+ return bron.includes("logistic") || bron.includes("engine");
112
+ }
113
+ /** Handmatig opnieuw printen? */
114
+ function isHerprint(job) {
115
+ const p = (job.payload ?? {});
116
+ const d = (p.data ?? {});
117
+ return job.is_herprint === true || p.is_herprint === true || d.is_herprint === true;
118
+ }
119
+ /** De ordersleutel waarop een herprint bij de oorspronkelijke print hoort. */
120
+ function leesOrderRef(job) {
121
+ const p = (job.payload ?? {});
122
+ const d = (p.data ?? {});
123
+ for (const k of [job.order_ref, p.order_ref, p.orderRef, d.order_ref, d.orderRef, job.order_id]) {
124
+ const v = String(k ?? "").trim();
125
+ if (v)
126
+ return v;
127
+ }
128
+ return "";
129
+ }
130
+ /**
131
+ * Een herprint gebruikt dezelfde layout als de oorspronkelijke print. Stuurt de
132
+ * Engine geen layout mee, dan zoeken we die op bij de laatste geslaagde print
133
+ * van hetzelfde document van dezelfde order.
134
+ */
135
+ async function zoekHerprintLayoutId(db, job) {
136
+ const orderRef = leesOrderRef(job);
137
+ if (!orderRef)
138
+ return "";
139
+ const type = normaliseerDocumenttype(leesDocumentCode(job) || job.documenttype);
140
+ const { data } = await db
141
+ .from("print_jobs")
142
+ .select("gebruikte_layout_id, documenttype, created_at")
143
+ .eq("order_ref", orderRef)
144
+ .not("gebruikte_layout_id", "is", null)
145
+ .order("created_at", { ascending: false })
146
+ .limit(10);
147
+ for (const rij of arr(data)) {
148
+ if (normaliseerDocumenttype(rij.documenttype) !== type)
149
+ continue;
150
+ const id = String(rij.gebruikte_layout_id ?? "").trim();
151
+ if (id)
152
+ return id;
153
+ }
154
+ return "";
155
+ }
68
156
  /**
69
157
  * Zoekt de layout die bij de opdracht hoort.
70
158
  * Staat er een layout_id op de opdracht (bijv. gezet door de Logistiek
@@ -94,15 +182,19 @@ function magOntwerpWinnen(layout) {
94
182
  return String(layout.bron ?? "") !== "floriday";
95
183
  }
96
184
  async function zoekLayout(db, job) {
185
+ const engineJob = isEngineOpdracht(job);
97
186
  const heeftEigenOpmaak = typeof job.render_html === "string" && job.render_html.trim().length > 40;
98
- // Een layout-id in het kenmerkenveld is alleen een verwijzing, geen
99
- // koppeling: is er opmaak meegestuurd (bijv. een voorbeeldafdruk uit de
100
- // ontwerper), dan is die opmaak leidend.
101
- const uitPayload = heeftEigenOpmaak
187
+ const uitPayload = engineJob
102
188
  ? null
103
189
  : (job.payload?.layoutId ?? job.payload?.data?.layoutId ?? job.payload?.layout_id ?? null);
104
- const id = job.layout_id ?? uitPayload;
105
- const voorbeeld = heeftEigenOpmaak && isVoorbeeldAfdruk(job);
190
+ let id = leesEngineLayoutId(job) || job.layout_id || uitPayload;
191
+ if (engineJob && !id && isHerprint(job)) {
192
+ id = await zoekHerprintLayoutId(db, job);
193
+ }
194
+ if (engineJob && !id) {
195
+ throw new WachtOpLayoutFout("Wacht op layout_id van de Logistics Engine. Deze taak wordt niet met een vervangende layout uitgevoerd.");
196
+ }
197
+ const voorbeeld = !engineJob && heeftEigenOpmaak && isVoorbeeldAfdruk(job);
106
198
  if (id) {
107
199
  const { data } = await db
108
200
  .from("document_layouts")
@@ -113,19 +205,24 @@ async function zoekLayout(db, job) {
113
205
  throw new GeenDocumentFout("De layout die aan deze opdracht hangt bestaat niet meer. Koppel een geldige layout.");
114
206
  }
115
207
  const layout = data;
116
- return { layout, ontwerpWint: !voorbeeld && magOntwerpWinnen(layout) };
208
+ return {
209
+ layout,
210
+ // Een Engine-layout is altijd leidend, ook als er render_html is meegestuurd.
211
+ ontwerpWint: engineJob || (!voorbeeld && magOntwerpWinnen(layout)),
212
+ };
117
213
  }
214
+ const documenttype = normaliseerDocumenttype(leesDocumentCode(job) || job.documenttype);
118
215
  const { data } = await db
119
216
  .from("document_layouts")
120
217
  .select("*")
121
- .eq("documenttype", normaliseerDocumenttype(job.documenttype))
218
+ .eq("documenttype", documenttype)
122
219
  .eq("actief", true)
123
220
  .order("is_standaard", { ascending: false })
124
221
  .order("updated_at", { ascending: false })
125
222
  .limit(1);
126
223
  const gevonden = (arr(data)[0] ?? null);
127
224
  if (!gevonden && !job.render_html) {
128
- throw new GeenDocumentFout(`Geen layout gevonden voor documenttype '${normaliseerDocumenttype(job.documenttype)}'. ` +
225
+ throw new GeenDocumentFout(`Geen layout gevonden voor documenttype '${documenttype}'. ` +
129
226
  "Maak een layout aan en zet die op actief.");
130
227
  }
131
228
  return { layout: gevonden, ontwerpWint: !voorbeeld && magOntwerpWinnen(gevonden) };
@@ -148,7 +245,7 @@ async function zoekBronregel(db, job) {
148
245
  const regel = data;
149
246
  if (!regel || regel.bron === "auto")
150
247
  return null;
151
- if (regel.bron === "eigen_layout" && regel.layout_id && !job.layout_id) {
248
+ if (regel.bron === "eigen_layout" && regel.layout_id && !job.layout_id && !isEngineOpdracht(job)) {
152
249
  job.layout_id = regel.layout_id;
153
250
  }
154
251
  return regel.bron;
@@ -159,6 +256,8 @@ async function zoekBronregel(db, job) {
159
256
  */
160
257
  async function isFloridayLayoutKeuze(db, job) {
161
258
  const payload = (job.payload ?? {});
259
+ if (isEngineOpdracht(job))
260
+ return false;
162
261
  const id = job.layout_id ?? payload?.["layoutId"] ?? payload?.["layout_id"] ?? null;
163
262
  if (!id)
164
263
  return false;
@@ -324,18 +423,40 @@ async function zoekPrinter(db, job, layout) {
324
423
  return null;
325
424
  const { data } = await db
326
425
  .from("printers")
327
- .select("id, naam, extern_printer_id, actief, papierformaat, ondersteunde_formaten, ondersteunde_laden, formaten_bijgewerkt_op")
426
+ .select("id, naam, extern_printer_id, actief, papierformaat, ondersteunde_formaten, ondersteunde_laden, formaten_bijgewerkt_op, afdrukmethode")
328
427
  .eq("id", id)
329
428
  .maybeSingle();
330
429
  return data && data.actief !== false ? data : null;
331
430
  };
332
- const type = normaliseerDocumenttype(job.documenttype);
333
- // 1. Printer die expliciet op de opdracht staat (bijv. handmatig herprinten).
334
- let printer = await pak(job.printer_id);
431
+ const type = normaliseerDocumenttype(leesDocumentCode(job) || job.documenttype);
432
+ // Bij Engine-opdrachten is print_plek_id leidend. Een eventueel oud
433
+ // printer_id mag die keuze niet overschrijven.
434
+ let printer = isEngineOpdracht(job) && leesPrintPlekId(job)
435
+ ? null
436
+ : await pak(job.printer_id);
335
437
  if (printer)
336
438
  return { printer, reden: "" };
337
439
  const werkplek = String(job.payload?.werkplek ?? "").trim();
338
440
  const pickstation = leesPickstation(job);
441
+ const explicietePlek = leesPrintPlekId(job);
442
+ // Een Engine-printplek is een harde keuze. Nooit terugvallen op computer,
443
+ // layout of bedrijfsregels wanneer de Engine een print_plek_id meegaf.
444
+ if (explicietePlek) {
445
+ const { data: pp } = await db
446
+ .from("document_printplek_printers")
447
+ .select("printer_id")
448
+ .eq("print_plek_id", explicietePlek)
449
+ .eq("documenttype", type)
450
+ .maybeSingle();
451
+ printer = await pak(pp?.printer_id);
452
+ if (printer)
453
+ return { printer, reden: "" };
454
+ const { data: plek } = await db.from("print_plekken").select("naam").eq("id", explicietePlek).maybeSingle();
455
+ return {
456
+ printer: null,
457
+ reden: `Printplek '${String(plek?.naam ?? explicietePlek)}' heeft geen printer voor documenttype '${type}'.`,
458
+ };
459
+ }
339
460
  // 2. Regel van de printplek waar de opdracht vandaan komt.
340
461
  const plekId = await zoekPrintPlekId(db, job);
341
462
  if (plekId) {
@@ -414,24 +535,31 @@ async function zoekPrinter(db, job, layout) {
414
535
  reden: `Documenttype '${type}' heeft geen standaardprinter. Stel dit in bij Printen > Standaardregels.`,
415
536
  };
416
537
  }
538
+ /** Verzamelt elk logo-id ergens in de layoutconfiguratie (op elke diepte). */
539
+ function verzamelLogoIds(waarde, uit, diepte = 0) {
540
+ if (diepte > 8 || waarde === null || typeof waarde !== "object")
541
+ return;
542
+ if (Array.isArray(waarde)) {
543
+ for (const item of waarde)
544
+ verzamelLogoIds(item, uit, diepte + 1);
545
+ return;
546
+ }
547
+ for (const [sleutel, val] of Object.entries(waarde)) {
548
+ if ((sleutel === "logoId" || sleutel === "logo_id") && typeof val === "string" && val) {
549
+ uit.add(val);
550
+ }
551
+ else {
552
+ verzamelLogoIds(val, uit, diepte + 1);
553
+ }
554
+ }
555
+ }
417
556
  /** Haalt de logo's op die in de layout gebruikt worden, met tijdelijke URL. */
418
557
  async function logosVoor(db, layout) {
419
558
  const config = layout?.config ?? null;
420
559
  const ids = new Set();
421
- const documentLogo = config?.logo?.logoId;
422
- if (documentLogo)
423
- ids.add(String(documentLogo));
424
- // Ook logo's die als blok in de ontwerper staan meenemen.
425
- for (const sectie of Array.isArray(config?.sections) ? config.sections : []) {
426
- const id = sectie?.opties?.logoId ?? sectie?.logoId;
427
- if (id)
428
- ids.add(String(id));
429
- }
430
- for (const blok of Array.isArray(config?.blokken) ? config.blokken : []) {
431
- const id = blok?.opties?.logoId ?? blok?.logoId;
432
- if (id)
433
- ids.add(String(id));
434
- }
560
+ // Logo's staan op verschillende plekken in de configuratie: als documentlogo,
561
+ // als blok in de ontwerper, of binnen `pakbon.secties` / `productiebon.blokken`.
562
+ verzamelLogoIds(config, ids);
435
563
  if (!ids.size)
436
564
  return [];
437
565
  const { data } = await db
@@ -510,6 +638,13 @@ export async function bouwHtml(db, job, keuze, uitkomst) {
510
638
  : null;
511
639
  if ((pakbonSecties && pakbonSecties.length) || String(layout.documenttype) === "pakbon") {
512
640
  const orderBron = p.order ?? p.data?.order ?? p.data ?? p;
641
+ const pakbonMethode = Array.isArray(pakbonSecties)
642
+ ? String(pakbonSecties.find((s) => s?.type === "pickmethode")?.opties?.methode ?? "kar")
643
+ : "kar";
644
+ const engineVariant = job.layout_variant ?? p.layout_variant ?? p.layoutVariant ?? p.data?.layout_variant ?? null;
645
+ if (isEngineOpdracht(job) && pakbonMethode === "beide" && !["full_cart", "partial_order"].includes(String(engineVariant ?? ""))) {
646
+ throw new WachtOpLayoutFout("Wacht op layout_variant van de Logistics Engine: kies full_cart of partial_order.");
647
+ }
513
648
  const uitDb = heeftOrderInhoud(orderBron) ? null : await laadOrderVanuitDatabase(db, p);
514
649
  // Een voorbeeldafdruk zonder ordergegevens krijgt een voorbeeldorder, zodat
515
650
  // je ziet hoe het document er in de praktijk uitziet.
@@ -517,14 +652,82 @@ export async function bouwHtml(db, job, keuze, uitkomst) {
517
652
  (p.voorbeeld === true && !heeftOrderInhoud(orderBron)
518
653
  ? voorbeeldPakbonOrder()
519
654
  : orderBron);
520
- const order = maakPakbonOrder(bron);
521
- return renderPakbonDocument(layout.config, order, {
655
+ // Barcode en kargegevens van de Engine mogen niet verloren gaan wanneer
656
+ // de ordergegevens uit een subobject komen.
657
+ const engineBarcode = p.barcode ?? p.data?.cart?.barcode ?? null;
658
+ const engineCart = p.data?.cart ?? null;
659
+ const bronRecord = (bron && typeof bron === "object" ? bron : {});
660
+ // Verwijder order-/payloadbarcodes: bij Engine-opdrachten zijn alleen de
661
+ // twee contractvelden hierboven toegestaan als bron voor de afleverbriefscan.
662
+ const { barcode: _bronBarcode, cart: _bronCart, ...bronZonderEngineBarcode } = bronRecord;
663
+ let order = maakPakbonOrder({
664
+ ...bronZonderEngineBarcode,
665
+ ...(engineBarcode ? { barcode: engineBarcode } : {}),
666
+ ...(engineCart ? { cart: engineCart } : {}),
667
+ });
668
+ if (isEngineOpdracht(job)) {
669
+ const barcode = String(engineBarcode ?? "").trim();
670
+ if (!barcode) {
671
+ throw new WachtOpLayoutFout("Wacht op barcode van de Logistics Engine: iedere kar moet een eigen pakbonbarcode krijgen.");
672
+ }
673
+ const cartId = String(engineCart?.cart_id ?? "").trim();
674
+ const karren = Array.isArray(order.karren) ? order.karren : [];
675
+ const passendeKarren = cartId
676
+ ? karren.filter((kar) => String(kar.cart_id ?? "").trim() === cartId)
677
+ : karren;
678
+ // Eén Engine-opdracht staat voor precies één kar en dus één pakbon. Een
679
+ // opdracht met meerdere karren mag niet één barcode aan meerdere karren
680
+ // koppelen; de Engine moet daarvoor afzonderlijke jobs aanmaken.
681
+ if (passendeKarren.length > 1 || (!cartId && karren.length > 1)) {
682
+ throw new WachtOpLayoutFout("Wacht op afzonderlijke kar-opdrachten van de Logistics Engine: maak per kar één print_job met een eigen barcode.");
683
+ }
684
+ if (cartId && passendeKarren.length === 0 && karren.length > 1) {
685
+ throw new WachtOpLayoutFout("De door de Logistics Engine gekozen kar is niet gevonden in de pakbongegevens.");
686
+ }
687
+ const gekozenKar = passendeKarren[0] ?? (karren.length === 1 ? karren[0] : null);
688
+ if (gekozenKar) {
689
+ order = {
690
+ ...order,
691
+ cart: {
692
+ ...order.cart,
693
+ ...engineCart,
694
+ barcode,
695
+ cart_id: cartId || gekozenKar.cart_id,
696
+ cart_label: String(engineCart?.cart_label ?? gekozenKar.cart_label ?? "").trim() || undefined,
697
+ cart_index: Number(engineCart?.cart_index ?? gekozenKar.cart_index ?? 0) || undefined,
698
+ cart_total: Number(engineCart?.cart_total ?? gekozenKar.cart_total ?? 0) || undefined,
699
+ },
700
+ karren: [{ ...gekozenKar, barcode }],
701
+ };
702
+ }
703
+ }
704
+ const html = renderPakbonDocument(layout.config, order, {
522
705
  breedteMm,
523
706
  hoogteMm,
524
707
  margeMm: Number(layout.marge_boven ?? 12),
525
708
  titel: job.documentnaam ?? "Document",
709
+ layoutVariant: engineVariant,
526
710
  logos: await logosVoor(db, layout),
527
711
  });
712
+ // De Engine bepaalt de barcode. De module registreert uitsluitend de
713
+ // gerenderde inhoud onder die bestaande waarde voor de scanner. Als de
714
+ // registratie tijdelijk niet kan, mag dat een afdruk nooit blokkeren.
715
+ const scanInhoud = bouwPakbonScanInhoud(order, {
716
+ barcode: engineBarcode,
717
+ layoutVariant: pakbonLayoutVariant(engineVariant),
718
+ layoutId: String(layout.id ?? "") || null,
719
+ printJobId: String(job.id ?? "") || null,
720
+ isHerprint: isHerprint(job),
721
+ });
722
+ if (scanInhoud) {
723
+ try {
724
+ await bewaarPakbonScanInhoud(db, scanInhoud);
725
+ }
726
+ catch (e) {
727
+ await log(db, job.id, "barcode_inhoud_niet_opgeslagen", e instanceof Error ? e.message : "Registratie van barcode-inhoud mislukt");
728
+ }
729
+ }
730
+ return html;
528
731
  }
529
732
  // 2) Productiebon-layouts (blokken onder `productiebon.blokken`).
530
733
  if (Array.isArray(cfg.productiebon?.blokken) ||
@@ -616,6 +819,20 @@ function plattePayload(payload) {
616
819
  loop(payload, "", 0);
617
820
  return uit;
618
821
  }
822
+ async function wachtOpLayout(db, job, reden) {
823
+ await db
824
+ .from("print_jobs")
825
+ .update({
826
+ status: "wachtend",
827
+ foutmelding: reden.slice(0, 500),
828
+ volgende_poging: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
829
+ opgepakt_op: null,
830
+ voltooid_op: null,
831
+ updated_at: new Date().toISOString(),
832
+ })
833
+ .eq("id", job.id);
834
+ await log(db, job.id, "wacht_op_engine_layout", reden);
835
+ }
619
836
  async function mislukt(db, job, reden, terugNaarWachtend) {
620
837
  const pogingen = Number(job.pogingen ?? 0);
621
838
  const opnieuw = terugNaarWachtend && pogingen < MAX_POGINGEN;
@@ -643,19 +860,20 @@ async function verwerkJob(db, job) {
643
860
  // 1) Bronregel: bepaalt of het meegestuurde document wordt gebruikt of dat
644
861
  // er altijd een eigen ontwerp wordt opgemaakt.
645
862
  let bron = await zoekBronregel(db, job);
646
- // 1b) Is er een vaste Floriday-layout gekozen? Die staat alleen voor het
647
- // kant-en-klare document uit Floriday: dat gaat 1-op-1 naar de printer.
648
- if (await isFloridayLayoutKeuze(db, job)) {
863
+ // 1b) Legacy Floriday-keuze; Engine-opdrachten volgen uitsluitend hun
864
+ // eigen layout_id en worden nooit teruggezet naar een externe PDF.
865
+ if (!isEngineOpdracht(job) && await isFloridayLayoutKeuze(db, job)) {
649
866
  bron = "extern_pdf";
650
867
  job.layout_id = null;
651
868
  }
652
- if (bron === "eigen_layout") {
869
+ if (bron === "eigen_layout" || isEngineOpdracht(job)) {
653
870
  job.pdf_base64 = null;
654
871
  job.pdf_url = null;
655
872
  }
656
- // 2) Kant-en-klare PDF (klantsticker, Floriday-document) gaat ongewijzigd door.
657
- const klaar = await kantEnKlarePdf(db, job);
658
- if (!klaar && bron === "extern_pdf") {
873
+ // 2) Kant-en-klare PDF gaat alleen buiten het Engine-contract ongewijzigd
874
+ // door. De Engine bepaalt altijd de layout, ook als er een PDF aanwezig is.
875
+ const klaar = isEngineOpdracht(job) ? null : await kantEnKlarePdf(db, job);
876
+ if (!klaar && bron === "extern_pdf" && !isEngineOpdracht(job)) {
659
877
  await mislukt(db, job, "Floriday-document ontbreekt: voor dit document is ingesteld dat het " +
660
878
  "aangeleverde document wordt gebruikt, maar er is geen PDF meegestuurd.", true);
661
879
  return "mislukt";
@@ -664,7 +882,9 @@ async function verwerkJob(db, job) {
664
882
  const layout = keuze.layout;
665
883
  const printerKeuze = await zoekPrinter(db, job, layout);
666
884
  const printer = printerKeuze.printer;
667
- if (!printer?.extern_printer_id) {
885
+ // Virtuele PDF-printer: geen PrintNode nodig; de PDF wordt bewaard.
886
+ const isPdfPrinter = String(printer?.afdrukmethode ?? "") === "pdf";
887
+ if (!printer || (!isPdfPrinter && !printer.extern_printer_id)) {
668
888
  await mislukt(db, job, printerKeuze.reden ||
669
889
  `Printer '${printer?.naam ?? "onbekend"}' is niet aan PrintNode gekoppeld.`, true);
670
890
  return "mislukt";
@@ -679,18 +899,69 @@ async function verwerkJob(db, job) {
679
899
  throw new GeenDocumentFout("Het document was leeg: er zijn geen gegevens gevonden om af te drukken. " +
680
900
  "Controleer of de layout blokken bevat en of de order gegevens heeft.");
681
901
  }
682
- const liggend = String(layout?.orientatie ?? "portrait") === "landscape";
902
+ // Een voorbeeldafdruk koppelt bewust geen layout (het meegestuurde
903
+ // ontwerp is leidend), maar geeft wél het papierformaat van de bewerkte
904
+ // layout mee. Zonder dit zou het voorbeeld altijd op A4 uitkomen.
905
+ const vf = (job.payload ?? {})["voorbeeld_formaat"];
906
+ const liggend = String(layout?.orientatie ?? vf?.["orientatie"] ?? "portrait") === "landscape";
683
907
  pdfBase64 = await htmlNaarPdfServer({
684
908
  html,
685
- breedteMm: Number(layout?.breedte_mm ?? 210),
686
- hoogteMm: Number(layout?.hoogte_mm ?? 297),
687
- margeMm: Number(layout?.marge_boven ?? 10),
909
+ breedteMm: Number(layout?.breedte_mm ?? vf?.["breedte_mm"] ?? 210),
910
+ hoogteMm: Number(layout?.hoogte_mm ?? vf?.["hoogte_mm"] ?? 297),
911
+ margeMm: Number(layout?.marge_boven ?? vf?.["marge_boven"] ?? 10),
688
912
  landschap: liggend,
689
913
  });
690
914
  }
915
+ // Virtuele PDF-printer: niets gaat naar een fysieke printer; de opgemaakte
916
+ // PDF wordt op de opdracht bewaard en is daarna te downloaden in de
917
+ // wachtrij. De opdracht is meteen voltooid.
918
+ if (isPdfPrinter) {
919
+ // In twee stappen: eerst de status (klein en altijd betrouwbaar), daarna
920
+ // pas de PDF zelf. Een grote PDF kan door de databaselimiet geweigerd
921
+ // worden; de opdracht mag daar nooit door blijven hangen op 'bezig'.
922
+ const { error: statusFout } = await db
923
+ .from("print_jobs")
924
+ .update({
925
+ printer_id: printer.id,
926
+ gebruikte_layout_id: uitkomst.bron === "extern_pdf" ? null : (layout?.id ?? null),
927
+ gebruikte_bron: uitkomst.bron,
928
+ status: "voltooid",
929
+ lokale_pdf_status: "gereed",
930
+ lokale_pdf_fout: null,
931
+ lokaal_opgeslagen_op: null,
932
+ foutmelding: null,
933
+ volgende_poging: null,
934
+ voltooid_op: new Date().toISOString(),
935
+ updated_at: new Date().toISOString(),
936
+ })
937
+ .eq("id", job.id);
938
+ if (statusFout) {
939
+ await mislukt(db, job, `Opdracht afronden mislukt: ${statusFout.message}`, true);
940
+ return "mislukt";
941
+ }
942
+ const { error: pdfFout } = await db
943
+ .from("print_jobs")
944
+ .update({ resultaat_pdf_base64: pdfBase64 })
945
+ .eq("id", job.id);
946
+ if (pdfFout) {
947
+ await db
948
+ .from("print_jobs")
949
+ .update({
950
+ lokale_pdf_status: "fout",
951
+ lokale_pdf_fout: `PDF kon niet bewaard worden: ${pdfFout.message}`.slice(0, 500),
952
+ updated_at: new Date().toISOString(),
953
+ })
954
+ .eq("id", job.id);
955
+ await log(db, job.id, "mislukt", `PDF bewaren mislukt: ${pdfFout.message}`);
956
+ return "verstuurd";
957
+ }
958
+ await log(db, job.id, "pdf_gereed", `PDF gereed voor lokale opslag via ${printer.naam ?? "PDF-printer"}.`);
959
+ return "verstuurd";
960
+ }
691
961
  const formaten = await zorgVoorFormaten(db, printer);
692
- const breedteMm = Number(layout?.breedte_mm ?? standaardMaat(printer).breedte);
693
- const hoogteMm = Number(layout?.hoogte_mm ?? standaardMaat(printer).hoogte);
962
+ const vfPapier = (job.payload ?? {})["voorbeeld_formaat"];
963
+ const breedteMm = Number(layout?.breedte_mm ?? vfPapier?.["breedte_mm"] ?? standaardMaat(printer).breedte);
964
+ const hoogteMm = Number(layout?.hoogte_mm ?? vfPapier?.["hoogte_mm"] ?? standaardMaat(printer).hoogte);
694
965
  const papier = kiesPapier(formaten, breedteMm, hoogteMm);
695
966
  const externJobId = await stuurPdfNaarPrinter({
696
967
  externPrinterId: String(printer.extern_printer_id),
@@ -717,6 +988,10 @@ async function verwerkJob(db, job) {
717
988
  return "verstuurd";
718
989
  }
719
990
  catch (e) {
991
+ if (e instanceof WachtOpLayoutFout) {
992
+ await wachtOpLayout(db, job, e.message);
993
+ return "wachtend";
994
+ }
720
995
  // Ontbreekt er een document of layout? Dan heeft opnieuw proberen geen zin.
721
996
  const blijvend = e instanceof GeenDocumentFout;
722
997
  const tijdelijk = !blijvend && (e instanceof PdfRenderFout || e?.name === "PrintToegangFout");
@@ -812,6 +1087,7 @@ async function vervalControle(db) {
812
1087
  .select("id, pogingen")
813
1088
  .eq("status", "bezig")
814
1089
  .is("extern_job_id", null)
1090
+ .neq("lokale_pdf_status", "gereed")
815
1091
  .or(`opgepakt_op.lt.${grensClaim},and(opgepakt_op.is.null,updated_at.lt.${grensClaim})`)
816
1092
  .limit(50);
817
1093
  for (const job of arr(vastgelopen)) {
@@ -844,16 +1120,31 @@ export async function verwerkWachtrij(db, max = 5) {
844
1120
  catch (e) {
845
1121
  console.warn(`[printwachtrij] vervalcontrole mislukt: ${e instanceof Error ? e.message : "onbekend"}`);
846
1122
  }
847
- const { data, error } = await db.rpc("claim_cloud_print_jobs", { _max: max });
848
- if (error)
849
- throw new Error(error.message);
850
- const jobs = arr(data);
1123
+ // PDF-opdrachten hebben een eigen claimroute. Daardoor kunnen oudere
1124
+ // gepubliceerde moduleversies, die een PDF-printer nog als PrintNode-printer
1125
+ // behandelen, deze opdrachten niet meer vóór de actuele runner oppakken.
1126
+ const { data: pdfData, error: pdfError } = await db.rpc("claim_pdf_print_jobs", { _max: max });
1127
+ if (pdfError)
1128
+ throw new Error(pdfError.message);
1129
+ const pdfJobs = arr(pdfData);
1130
+ const resterend = Math.max(0, max - pdfJobs.length);
1131
+ let gewoneJobs = [];
1132
+ if (resterend > 0) {
1133
+ const { data, error } = await db.rpc("claim_cloud_print_jobs", { _max: resterend });
1134
+ if (error)
1135
+ throw new Error(error.message);
1136
+ gewoneJobs = arr(data);
1137
+ }
1138
+ const jobs = [...pdfJobs, ...gewoneJobs];
851
1139
  let verstuurd = 0;
852
1140
  let mislukteJobs = 0;
1141
+ let wachtend = 0;
853
1142
  for (const job of jobs) {
854
1143
  const uitkomst = await verwerkJob(db, job);
855
1144
  if (uitkomst === "verstuurd")
856
1145
  verstuurd++;
1146
+ else if (uitkomst === "wachtend")
1147
+ wachtend++;
857
1148
  else
858
1149
  mislukteJobs++;
859
1150
  }
@@ -861,6 +1152,7 @@ export async function verwerkWachtrij(db, max = 5) {
861
1152
  opgepakt: jobs.length,
862
1153
  verstuurd,
863
1154
  mislukt: mislukteJobs,
1155
+ wachtend,
864
1156
  bijgewerkt: await synchroniseerStatussen(db),
865
1157
  };
866
1158
  }
@@ -871,16 +1163,18 @@ export async function verwerkWachtrij(db, max = 5) {
871
1163
  */
872
1164
  export async function bepaalDocumentKeuze(db, job) {
873
1165
  let bron = await zoekBronregel(db, job);
874
- if (await isFloridayLayoutKeuze(db, job)) {
1166
+ // Engine-opdrachten houden hun eigen layout_id; nooit vervangen door een
1167
+ // Floriday-PDF-keuze.
1168
+ if (!isEngineOpdracht(job) && (await isFloridayLayoutKeuze(db, job))) {
875
1169
  bron = "extern_pdf";
876
1170
  job.layout_id = null;
877
1171
  }
878
- if (bron === "eigen_layout") {
1172
+ if (bron === "eigen_layout" || isEngineOpdracht(job)) {
879
1173
  job.pdf_base64 = null;
880
1174
  job.pdf_url = null;
881
1175
  }
882
- const klaar = await kantEnKlarePdf(db, job);
883
- if (!klaar && bron === "extern_pdf") {
1176
+ const klaar = isEngineOpdracht(job) ? null : await kantEnKlarePdf(db, job);
1177
+ if (!klaar && bron === "extern_pdf" && !isEngineOpdracht(job)) {
884
1178
  throw new GeenDocumentFout("Voor dit document is ingesteld dat het aangeleverde document wordt " +
885
1179
  "gebruikt, maar er is geen PDF meegestuurd.");
886
1180
  }