@flowselections/layout-maker 1.38.4 → 1.45.8

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 (124) hide show
  1. package/README.md +139 -139
  2. package/dist-lib/components/AfdrukKnop.d.ts +19 -19
  3. package/dist-lib/components/AfleverbriefBuilderPage.d.ts +10 -0
  4. package/dist-lib/components/AfleverbriefBuilderPage.d.ts.map +1 -0
  5. package/dist-lib/components/AfleverbriefBuilderPage.js +101 -0
  6. package/dist-lib/components/BlokOntwerperPage.d.ts.map +1 -1
  7. package/dist-lib/components/BlokOntwerperPage.js +8 -3
  8. package/dist-lib/components/ComputersLijst.d.ts.map +1 -1
  9. package/dist-lib/components/ComputersLijst.js +1 -1
  10. package/dist-lib/components/FustKolomVolgorde.d.ts +11 -0
  11. package/dist-lib/components/FustKolomVolgorde.d.ts.map +1 -0
  12. package/dist-lib/components/FustKolomVolgorde.js +52 -0
  13. package/dist-lib/components/LayoutsPage.d.ts.map +1 -1
  14. package/dist-lib/components/LayoutsPage.js +10 -9
  15. package/dist-lib/components/NetwerkPrintersDialog.d.ts +7 -7
  16. package/dist-lib/components/PakbonBuilderPage.d.ts.map +1 -1
  17. package/dist-lib/components/PakbonBuilderPage.js +32 -14
  18. package/dist-lib/components/PakbonPreview.d.ts.map +1 -1
  19. package/dist-lib/components/PakbonPreview.js +21 -14
  20. package/dist-lib/components/PakbonSectiesPanel.d.ts +4 -2
  21. package/dist-lib/components/PakbonSectiesPanel.d.ts.map +1 -1
  22. package/dist-lib/components/PakbonSectiesPanel.js +5 -3
  23. package/dist-lib/components/PdfMapToestemmingBanner.d.ts +2 -1
  24. package/dist-lib/components/PdfMapToestemmingBanner.d.ts.map +1 -1
  25. package/dist-lib/components/PdfMapToestemmingBanner.js +73 -14
  26. package/dist-lib/components/PdfVoorbeeld.d.ts +20 -0
  27. package/dist-lib/components/PdfVoorbeeld.d.ts.map +1 -0
  28. package/dist-lib/components/PdfVoorbeeld.js +95 -0
  29. package/dist-lib/components/PrintProgrammaUitleg.d.ts.map +1 -1
  30. package/dist-lib/components/PrintProgrammaUitleg.js +1 -1
  31. package/dist-lib/components/PrintQueuePage.d.ts.map +1 -1
  32. package/dist-lib/components/PrintQueuePage.js +81 -43
  33. package/dist-lib/components/PrinterSetupWizard.js +3 -3
  34. package/dist-lib/components/PrintersRedirectPage.d.ts +1 -1
  35. package/dist-lib/components/PrintersRedirectPage.js +14 -14
  36. package/dist-lib/components/PrintplekDetailPage.d.ts.map +1 -1
  37. package/dist-lib/components/PrintplekDetailPage.js +2 -2
  38. package/dist-lib/components/PrintplekWizard.d.ts.map +1 -1
  39. package/dist-lib/components/PrintplekWizard.js +6 -2
  40. package/dist-lib/components/ProductiebonBuilderPage.d.ts.map +1 -1
  41. package/dist-lib/components/ProductiebonBuilderPage.js +13 -7
  42. package/dist-lib/index.d.ts +13 -5
  43. package/dist-lib/index.d.ts.map +1 -1
  44. package/dist-lib/index.js +10 -5
  45. package/dist-lib/integrations/supabase/auth-middleware.d.ts +2008 -118
  46. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  47. package/dist-lib/integrations/supabase/client.server.d.ts +2008 -118
  48. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  49. package/dist-lib/integrations/supabase/client.server.js +1 -1
  50. package/dist-lib/integrations/supabase/moduleClient.d.ts +2008 -118
  51. package/dist-lib/integrations/supabase/moduleClient.d.ts.map +1 -1
  52. package/dist-lib/integrations/supabase/types.d.ts +2031 -99
  53. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  54. package/dist-lib/integrations/supabase/types.js +1 -0
  55. package/dist-lib/lib/aiLayout.functions.d.ts +2008 -118
  56. package/dist-lib/lib/aiLayout.functions.d.ts.map +1 -1
  57. package/dist-lib/lib/aiLayout.server.js +9 -9
  58. package/dist-lib/lib/browserPrint.js +1 -1
  59. package/dist-lib/lib/documentVoorbeeld.functions.d.ts +23 -0
  60. package/dist-lib/lib/documentVoorbeeld.functions.d.ts.map +1 -0
  61. package/dist-lib/lib/documentVoorbeeld.functions.js +37 -0
  62. package/dist-lib/lib/fontMetrics.d.ts +15 -0
  63. package/dist-lib/lib/fontMetrics.d.ts.map +1 -0
  64. package/dist-lib/lib/fontMetrics.js +118 -0
  65. package/dist-lib/lib/htmlNaarPdf.server.d.ts +2 -2
  66. package/dist-lib/lib/htmlNaarPdf.server.d.ts.map +1 -1
  67. package/dist-lib/lib/htmlNaarPdf.server.js +2 -1
  68. package/dist-lib/lib/htmlPdf.server.d.ts +11 -0
  69. package/dist-lib/lib/htmlPdf.server.d.ts.map +1 -1
  70. package/dist-lib/lib/htmlPdf.server.js +118 -15
  71. package/dist-lib/lib/layoutPreview.js +7 -7
  72. package/dist-lib/lib/orderData.server.d.ts.map +1 -1
  73. package/dist-lib/lib/orderData.server.js +43 -16
  74. package/dist-lib/lib/pakbonBarcodeVerzoek.d.ts +39 -0
  75. package/dist-lib/lib/pakbonBarcodeVerzoek.d.ts.map +1 -0
  76. package/dist-lib/lib/pakbonBarcodeVerzoek.js +80 -0
  77. package/dist-lib/lib/pakbonBuilder.d.ts +62 -3
  78. package/dist-lib/lib/pakbonBuilder.d.ts.map +1 -1
  79. package/dist-lib/lib/pakbonBuilder.js +386 -50
  80. package/dist-lib/lib/pakbonContent.d.ts +41 -0
  81. package/dist-lib/lib/pakbonContent.d.ts.map +1 -0
  82. package/dist-lib/lib/pakbonContent.js +668 -0
  83. package/dist-lib/lib/pakbonDocument.d.ts +199 -0
  84. package/dist-lib/lib/pakbonDocument.d.ts.map +1 -0
  85. package/dist-lib/lib/pakbonDocument.js +735 -0
  86. package/dist-lib/lib/pakbonHtml.d.ts +7 -0
  87. package/dist-lib/lib/pakbonHtml.d.ts.map +1 -1
  88. package/dist-lib/lib/pakbonHtml.js +142 -126
  89. package/dist-lib/lib/pakbonPresentation.d.ts +8 -0
  90. package/dist-lib/lib/pakbonPresentation.d.ts.map +1 -0
  91. package/dist-lib/lib/pakbonPresentation.js +31 -0
  92. package/dist-lib/lib/pakbonScan.d.ts.map +1 -1
  93. package/dist-lib/lib/pakbonScan.js +20 -38
  94. package/dist-lib/lib/pdfJobSaver.d.ts +10 -6
  95. package/dist-lib/lib/pdfJobSaver.d.ts.map +1 -1
  96. package/dist-lib/lib/pdfJobSaver.js +302 -99
  97. package/dist-lib/lib/pdfMap.d.ts +37 -2
  98. package/dist-lib/lib/pdfMap.d.ts.map +1 -1
  99. package/dist-lib/lib/pdfMap.js +120 -7
  100. package/dist-lib/lib/pdfOpslagStatus.d.ts +23 -0
  101. package/dist-lib/lib/pdfOpslagStatus.d.ts.map +1 -0
  102. package/dist-lib/lib/pdfOpslagStatus.js +74 -0
  103. package/dist-lib/lib/printApi.d.ts +31 -0
  104. package/dist-lib/lib/printApi.d.ts.map +1 -1
  105. package/dist-lib/lib/printApi.js +62 -1
  106. package/dist-lib/lib/printQueue.server.d.ts +6 -0
  107. package/dist-lib/lib/printQueue.server.d.ts.map +1 -1
  108. package/dist-lib/lib/printQueue.server.js +450 -94
  109. package/dist-lib/lib/printService.functions.d.ts +7916 -356
  110. package/dist-lib/lib/printService.functions.d.ts.map +1 -1
  111. package/dist-lib/lib/productiebonHtml.js +15 -15
  112. package/dist-lib/lib/profielHtml.d.ts.map +1 -1
  113. package/dist-lib/lib/profielHtml.js +26 -19
  114. package/dist-lib/lib/supabaseAdmin.server.d.ts +5 -5
  115. package/dist-lib/lib/supabaseAdmin.server.js +43 -43
  116. package/dist-lib/lib/testpagina.js +3 -3
  117. package/dist-lib/lib/testprint.js +4 -4
  118. package/dist-lib/lib/useDocumentData.d.ts +8 -0
  119. package/dist-lib/lib/useDocumentData.d.ts.map +1 -1
  120. package/dist-lib/lib/useDocumentData.js +1 -1
  121. package/dist-lib/lib/voorbeeldPrint.d.ts +3 -0
  122. package/dist-lib/lib/voorbeeldPrint.d.ts.map +1 -1
  123. package/dist-lib/lib/voorbeeldPrint.js +65 -17
  124. package/package.json +158 -151
@@ -5,9 +5,10 @@
5
5
  // ============================================================================
6
6
  import { maakPakbonOrder, renderPakbonDocument, voorbeeldPakbonOrder, } from "./pakbonHtml.js";
7
7
  import { bouwPakbonScanInhoud, bewaarPakbonScanInhoud } from "./pakbonScan.js";
8
- import { pakbonLayoutVariant } from "./pakbonBuilder.js";
8
+ import { bouwPakbonBarcodeVerzoek, geleverdeBarcodeVoorJob, vraagPakbonBarcodeAan, } from "./pakbonBarcodeVerzoek.js";
9
+ import { deelkarRegels, leesAfleverbriefConfig, pakbonLayoutVariant } from "./pakbonBuilder.js";
9
10
  import { renderProductiebonDocument } from "./productiebonHtml.js";
10
- import { heeftOrderInhoud, laadOrderVanuitDatabase } from "./orderData.server.js";
11
+ import { heeftOrderInhoud, laadOrderVanuitDatabase, laadPakbonData } from "./orderData.server.js";
11
12
  import { profielHeeftInhoud, renderProfielDocument } from "./profielHtml.js";
12
13
  import { bouwDocumentHtml, voorbeeldContext } from "./layoutPreview.js";
13
14
  import { htmlNaarPdfServer, PdfRenderFout } from "./htmlNaarPdf.server.js";
@@ -56,6 +57,13 @@ export class WachtOpLayoutFout extends Error {
56
57
  this.name = "WachtOpLayoutFout";
57
58
  }
58
59
  }
60
+ /** Blijvende contractfout: opnieuw proberen met dezelfde invoer kan niet slagen. */
61
+ export class EngineContractFout extends GeenDocumentFout {
62
+ constructor() {
63
+ super(...arguments);
64
+ this.name = "EngineContractFout";
65
+ }
66
+ }
59
67
  /**
60
68
  * Oudere/andere benamingen voor hetzelfde document. De Logistiek Engine noemt
61
69
  * de afleverbrief bijvoorbeeld nog "verzendbrief"; hier vertalen we dat naar
@@ -95,7 +103,17 @@ export function leesEngineLayoutId(job) {
95
103
  function leesDocumentCode(job) {
96
104
  const p = (job.payload ?? {});
97
105
  const d = (p.data ?? {});
98
- for (const k of [job.document_code, p.document_code, p.documentCode, d.document_code]) {
106
+ for (const k of [
107
+ job.document_code,
108
+ job.document_type_code,
109
+ p.document_code,
110
+ p.documentCode,
111
+ p.document_type_code,
112
+ p.documentTypeCode,
113
+ d.document_code,
114
+ d.document_type_code,
115
+ d.documentTypeCode,
116
+ ]) {
99
117
  const v = String(k ?? "").trim();
100
118
  if (v)
101
119
  return v;
@@ -116,6 +134,59 @@ function isHerprint(job) {
116
134
  const d = (p.data ?? {});
117
135
  return job.is_herprint === true || p.is_herprint === true || d.is_herprint === true;
118
136
  }
137
+ /**
138
+ * Een herprint behoudt de eerder door de Engine aangeleverde barcode en kar.
139
+ * Er wordt nooit lokaal een barcode bedacht. We zoeken uitsluitend binnen
140
+ * dezelfde queue-/orderreferentie en hetzelfde documenttype.
141
+ */
142
+ async function zoekHerprintBarcodeBron(db, job) {
143
+ if (!isHerprint(job))
144
+ return null;
145
+ const p = (job.payload ?? {});
146
+ const d = (p.data ?? {});
147
+ const bronJobId = String(job.bron_job_id ?? p.bron_job_id ?? "").trim();
148
+ if (bronJobId) {
149
+ const { data: bron } = await db
150
+ .from("print_jobs")
151
+ .select("payload")
152
+ .eq("id", bronJobId)
153
+ .maybeSingle();
154
+ const bp = (bron?.payload ?? {});
155
+ const bd = (bp.data ?? {});
156
+ const barcode = String(bp.barcode ?? bd.cart?.barcode ?? bp.cart?.barcode ?? "").trim();
157
+ if (barcode) {
158
+ const cart = (bd.cart ?? bp.cart ?? null);
159
+ return { barcode, cart: cart && typeof cart === "object" ? cart : null };
160
+ }
161
+ }
162
+ const queueItem = String(p.queue_item_id ?? d.queue_item_id ?? "").trim();
163
+ const orderNummer = String(p.order_number ?? p.ordernummer ?? d.order_number ?? d.ordernummer ?? "").trim();
164
+ if (!queueItem && !orderNummer)
165
+ return null;
166
+ const { data } = await db
167
+ .from("print_jobs")
168
+ .select("id, documenttype, payload, created_at")
169
+ .lt("created_at", String(job.created_at ?? new Date().toISOString()))
170
+ .order("created_at", { ascending: false })
171
+ .limit(30);
172
+ const type = normaliseerDocumenttype(leesDocumentCode(job) || job.documenttype);
173
+ for (const kandidaat of arr(data)) {
174
+ if (normaliseerDocumenttype(leesDocumentCode(kandidaat) || kandidaat.documenttype) !== type)
175
+ continue;
176
+ const kp = (kandidaat.payload ?? {});
177
+ const kd = (kp.data ?? {});
178
+ const zelfdeQueue = queueItem && String(kp.queue_item_id ?? kd.queue_item_id ?? "").trim() === queueItem;
179
+ const zelfdeOrder = orderNummer && String(kp.order_number ?? kp.ordernummer ?? kd.order_number ?? kd.ordernummer ?? "").trim() === orderNummer;
180
+ if (!zelfdeQueue && !zelfdeOrder)
181
+ continue;
182
+ const barcode = String(kp.barcode ?? kd.cart?.barcode ?? kp.cart?.barcode ?? "").trim();
183
+ if (!barcode)
184
+ continue;
185
+ const cart = (kd.cart ?? kp.cart ?? null);
186
+ return { barcode, cart: cart && typeof cart === "object" ? cart : null };
187
+ }
188
+ return null;
189
+ }
119
190
  /** De ordersleutel waarop een herprint bij de oorspronkelijke print hoort. */
120
191
  function leesOrderRef(job) {
121
192
  const p = (job.payload ?? {});
@@ -139,14 +210,15 @@ async function zoekHerprintLayoutId(db, job) {
139
210
  const type = normaliseerDocumenttype(leesDocumentCode(job) || job.documenttype);
140
211
  const { data } = await db
141
212
  .from("print_jobs")
142
- .select("gebruikte_layout_id, documenttype, created_at")
143
- .eq("order_ref", orderRef)
213
+ .select("gebruikte_layout_id, documenttype, payload, created_at")
144
214
  .not("gebruikte_layout_id", "is", null)
145
215
  .order("created_at", { ascending: false })
146
- .limit(10);
216
+ .limit(50);
147
217
  for (const rij of arr(data)) {
148
218
  if (normaliseerDocumenttype(rij.documenttype) !== type)
149
219
  continue;
220
+ if (leesOrderRef(rij) !== orderRef)
221
+ continue;
150
222
  const id = String(rij.gebruikte_layout_id ?? "").trim();
151
223
  if (id)
152
224
  return id;
@@ -368,8 +440,37 @@ async function zoekPrintPlekId(db, job) {
368
440
  .select("print_plek_id")
369
441
  .eq("computer_naam", werkplek)
370
442
  .maybeSingle();
371
- return String(data?.print_plek_id ?? "").trim();
443
+ const id = String(data?.print_plek_id ?? "").trim();
444
+ if (id)
445
+ return id;
372
446
  }
447
+ // Terugval: de opdracht kwam zonder plek binnen (bijvoorbeeld van de scanner
448
+ // bij "kar compleet"). Gebruik dan de plek die in de documentroutering voor
449
+ // deze documentsoort staat. Zo blijft een PDF-opdracht niet eindeloos op een
450
+ // werkplek wachten die niemand heeft toegewezen.
451
+ const documenttype = normaliseerDocumenttype(job.documenttype);
452
+ if (documenttype) {
453
+ const { data } = await db
454
+ .from("document_printplek_printers")
455
+ .select("print_plek_id")
456
+ .eq("documenttype", documenttype)
457
+ .not("print_plek_id", "is", null);
458
+ const plekken = Array.isArray(data) ? data : [];
459
+ const uniek = Array.from(new Set(plekken.map((r) => String(r?.print_plek_id ?? "").trim()).filter(Boolean)));
460
+ if (uniek.length === 1)
461
+ return uniek[0];
462
+ }
463
+ // Laatste terugval: is er in de hele routering maar één plek bekend, dan kan
464
+ // er niets misgaan door die te kiezen. Bij meerdere plekken raden we nooit.
465
+ const { data: alle } = await db
466
+ .from("document_printplek_printers")
467
+ .select("print_plek_id")
468
+ .not("print_plek_id", "is", null);
469
+ const alleUniek = Array.from(new Set((Array.isArray(alle) ? alle : [])
470
+ .map((r) => String(r?.print_plek_id ?? "").trim())
471
+ .filter(Boolean)));
472
+ if (alleUniek.length === 1)
473
+ return alleUniek[0];
373
474
  return "";
374
475
  }
375
476
  /** Naam van de printplek waar deze opdracht bij hoort, voor nette meldingen. */
@@ -429,11 +530,12 @@ async function zoekPrinter(db, job, layout) {
429
530
  return data && data.actief !== false ? data : null;
430
531
  };
431
532
  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);
533
+ // Bij Engine-opdrachten is de centrale routering altijd leidend. De Engine
534
+ // kan een printer_id als momentopname meesturen, maar die kan inmiddels zijn
535
+ // gewijzigd (bijvoorbeeld van PDF naar een fysieke printer). Laat zo'n oud
536
+ // id daarom nooit de actuele printplek-, werkplek- of standaardregel
537
+ // overschrijven.
538
+ let printer = isEngineOpdracht(job) ? null : await pak(job.printer_id);
437
539
  if (printer)
438
540
  return { printer, reden: "" };
439
541
  const werkplek = String(job.payload?.werkplek ?? "").trim();
@@ -629,6 +731,40 @@ export async function bouwHtml(db, job, keuze, uitkomst) {
629
731
  throw new GeenDocumentFout("Deze layout gebruikt altijd het ontwerp van de klant, maar er is geen klantontwerp meegestuurd.");
630
732
  }
631
733
  }
734
+ // 0) Afleverbrief-/veilingbrieflayouts. Deze hebben hun eigen sectie-opbouw
735
+ // onder `afleverbrief`. Een afleverbrief hoort altijd bij precies één kar:
736
+ // hij vertelt wat er op díé kar staat.
737
+ const afleverType = String(layout.documenttype) === "afleverbrief" ||
738
+ String(layout.documenttype) === "veilingbrief";
739
+ if (afleverType || Array.isArray(cfg.afleverbrief?.secties)) {
740
+ const orderBron = p.order ?? p.data?.order ?? p.data ?? p;
741
+ const cartId = String(p.data?.cart?.cart_id ?? p.cart?.cart_id ?? p.data?.cart?.cartId ?? p.cart?.cartId ?? "").trim();
742
+ const uitDb = heeftOrderInhoud(orderBron)
743
+ ? null
744
+ : cartId
745
+ ? ((await laadPakbonData(db, { scope: "cart", id: cartId })) ??
746
+ (await laadOrderVanuitDatabase(db, p)))
747
+ : await laadOrderVanuitDatabase(db, p);
748
+ const bron = uitDb ??
749
+ (p.voorbeeld === true && !heeftOrderInhoud(orderBron)
750
+ ? voorbeeldPakbonOrder()
751
+ : orderBron);
752
+ const volledig = maakPakbonOrder((bron && typeof bron === "object" ? bron : {}));
753
+ // Beperk tot de aangewezen kar; zonder aanwijzing tot de eerste kar, zodat
754
+ // een afleverbrief nooit de inhoud van meerdere karren door elkaar toont.
755
+ const karren = arr(volledig.karren);
756
+ const gekozen = (cartId ? karren.find((k) => String(k?.cart_id ?? "").trim() === cartId) : null) ??
757
+ karren[0] ??
758
+ null;
759
+ const order = { ...volledig, karren: gekozen ? [gekozen] : [] };
760
+ return renderPakbonDocument(leesAfleverbriefConfig(layout.config), order, {
761
+ breedteMm,
762
+ hoogteMm,
763
+ margeMm: Number(layout.marge_boven ?? 10),
764
+ titel: job.documentnaam ?? String(layout.naam ?? "Afleverbrief"),
765
+ logos: await logosVoor(db, layout),
766
+ });
767
+ }
632
768
  // 1) Pakbon-layouts. De secties staan onder `pakbon.secties`; oudere
633
769
  // layouts hebben ze op het hoogste niveau. Beide vormen tellen mee.
634
770
  const pakbonSecties = Array.isArray(cfg.secties)
@@ -645,17 +781,38 @@ export async function bouwHtml(db, job, keuze, uitkomst) {
645
781
  if (isEngineOpdracht(job) && pakbonMethode === "beide" && !["full_cart", "partial_order"].includes(String(engineVariant ?? ""))) {
646
782
  throw new WachtOpLayoutFout("Wacht op layout_variant van de Logistics Engine: kies full_cart of partial_order.");
647
783
  }
648
- const uitDb = heeftOrderInhoud(orderBron) ? null : await laadOrderVanuitDatabase(db, p);
784
+ // Barcode en kargegevens van de Engine mogen niet verloren gaan wanneer
785
+ // de ordergegevens uit een subobject komen.
786
+ const herprintBron = await zoekHerprintBarcodeBron(db, job);
787
+ const engineBarcode = p.barcode ?? p.data?.cart?.barcode ?? p.cart?.barcode ?? herprintBron?.barcode ?? null;
788
+ const ruweEngineCart = p.data?.cart ?? p.cart ?? herprintBron?.cart ?? null;
789
+ const engineCart = ruweEngineCart && typeof ruweEngineCart === "object"
790
+ ? {
791
+ ...ruweEngineCart,
792
+ cart_id: ruweEngineCart.cart_id ?? ruweEngineCart.cartId,
793
+ cart_label: ruweEngineCart.cart_label ?? ruweEngineCart.cartLabel,
794
+ cart_index: ruweEngineCart.cart_index ?? ruweEngineCart.cartIndex,
795
+ cart_total: ruweEngineCart.cart_total ?? ruweEngineCart.cartTotal,
796
+ cart_items: ruweEngineCart.cart_items ?? ruweEngineCart.items,
797
+ barcode: ruweEngineCart.barcode ?? engineBarcode,
798
+ }
799
+ : null;
800
+ const engineCartId = String(engineCart?.cart_id ?? "").trim();
801
+ // Wijst de Engine één kar aan, dan halen we de producten rechtstreeks bij
802
+ // díé kar op. Zo staan altijd exact de artikelen op de pakbon die fysiek op
803
+ // de kar liggen — ook bij gedeelde karren met regels van meerdere orders.
804
+ const uitDb = heeftOrderInhoud(orderBron)
805
+ ? null
806
+ : engineCartId
807
+ ? ((await laadPakbonData(db, { scope: "cart", id: engineCartId })) ??
808
+ (await laadOrderVanuitDatabase(db, p)))
809
+ : await laadOrderVanuitDatabase(db, p);
649
810
  // Een voorbeeldafdruk zonder ordergegevens krijgt een voorbeeldorder, zodat
650
811
  // je ziet hoe het document er in de praktijk uitziet.
651
812
  const bron = uitDb ??
652
813
  (p.voorbeeld === true && !heeftOrderInhoud(orderBron)
653
814
  ? voorbeeldPakbonOrder()
654
815
  : orderBron);
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
816
  const bronRecord = (bron && typeof bron === "object" ? bron : {});
660
817
  // Verwijder order-/payloadbarcodes: bij Engine-opdrachten zijn alleen de
661
818
  // twee contractvelden hierboven toegestaan als bron voor de afleverbriefscan.
@@ -666,11 +823,38 @@ export async function bouwHtml(db, job, keuze, uitkomst) {
666
823
  ...(engineCart ? { cart: engineCart } : {}),
667
824
  });
668
825
  if (isEngineOpdracht(job)) {
669
- const barcode = String(engineBarcode ?? "").trim();
826
+ let barcode = String(engineBarcode ?? "").trim();
670
827
  if (!barcode) {
671
- throw new WachtOpLayoutFout("Wacht op barcode van de Logistics Engine: iedere kar moet een eigen pakbonbarcode krijgen.");
828
+ // De Engine heeft (nog) geen barcode meegestuurd. Heeft zij er
829
+ // inmiddels één geleverd op ons eerdere verzoek, dan gebruiken we die.
830
+ barcode = (await geleverdeBarcodeVoorJob(db, String(job.id ?? ""))) ?? "";
672
831
  }
673
- const cartId = String(engineCart?.cart_id ?? "").trim();
832
+ if (!barcode) {
833
+ // Zelf een barcode bedenken mag nooit. Wél laten we de Engine expliciet
834
+ // weten dat er voor deze pakbon een barcode ontbreekt, zodat zij er één
835
+ // kan aanleveren in plaats van dat de opdracht stil blijft hangen.
836
+ try {
837
+ await vraagPakbonBarcodeAan(db, bouwPakbonBarcodeVerzoek(order, {
838
+ printJobId: String(job.id ?? ""),
839
+ documentnaam: job.documentnaam,
840
+ cart: engineCart,
841
+ layoutVariant: pakbonLayoutVariant(engineVariant),
842
+ layoutId: String(layout.id ?? "") || null,
843
+ isHerprint: isHerprint(job),
844
+ }));
845
+ }
846
+ catch (e) {
847
+ await log(db, job.id, "barcode_verzoek_niet_opgeslagen", e instanceof Error ? e.message : "Barcodeverzoek kon niet worden vastgelegd");
848
+ }
849
+ throw new WachtOpLayoutFout("Barcode opgevraagd bij de Logistics Engine: deze kar heeft nog geen pakbonbarcode.");
850
+ }
851
+ // Restpakbon: zonder meegestuurde restregels weten we niet wát er nog
852
+ // verzameld moet worden. Dan liever geen pakbon dan een pakbon met de
853
+ // volledige karinhoud erop.
854
+ if (pakbonLayoutVariant(engineVariant) === "partial_order" && !deelkarRegels(order).length) {
855
+ throw new EngineContractFout("Restpakbon zonder restregels: stuur de resterende regels mee als cart_items.");
856
+ }
857
+ const cartId = engineCartId;
674
858
  const karren = Array.isArray(order.karren) ? order.karren : [];
675
859
  const passendeKarren = cartId
676
860
  ? karren.filter((kar) => String(kar.cart_id ?? "").trim() === cartId)
@@ -681,10 +865,20 @@ export async function bouwHtml(db, job, keuze, uitkomst) {
681
865
  if (passendeKarren.length > 1 || (!cartId && karren.length > 1)) {
682
866
  throw new WachtOpLayoutFout("Wacht op afzonderlijke kar-opdrachten van de Logistics Engine: maak per kar één print_job met een eigen barcode.");
683
867
  }
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.");
868
+ // Wijst de Engine een kar aan die niet in de gegevens voorkomt, dan mag
869
+ // er nóóit een andere kar worden afgedrukt: dan staan er producten op de
870
+ // pakbon die er niet op horen. De opdracht mislukt dan bewust.
871
+ if (cartId && passendeKarren.length === 0) {
872
+ throw new EngineContractFout("De door de Logistics Engine gekozen kar is niet gevonden in de pakbongegevens.");
686
873
  }
687
874
  const gekozenKar = passendeKarren[0] ?? (karren.length === 1 ? karren[0] : null);
875
+ // Een kar zonder productregels levert een lege pakbon op. Liever een
876
+ // duidelijke fout dan een pakbon zonder de producten die erop horen.
877
+ const heeftProducten = !!gekozenKar &&
878
+ arr(gekozenKar.lagen).some((laag) => arr(laag?.producten).length > 0);
879
+ if (gekozenKar && !heeftProducten) {
880
+ throw new EngineContractFout("De gekozen kar bevat geen productregels; de pakbon zou zonder producten worden afgedrukt.");
881
+ }
688
882
  if (gekozenKar) {
689
883
  order = {
690
884
  ...order,
@@ -713,7 +907,7 @@ export async function bouwHtml(db, job, keuze, uitkomst) {
713
907
  // gerenderde inhoud onder die bestaande waarde voor de scanner. Als de
714
908
  // registratie tijdelijk niet kan, mag dat een afdruk nooit blokkeren.
715
909
  const scanInhoud = bouwPakbonScanInhoud(order, {
716
- barcode: engineBarcode,
910
+ barcode: order?.cart?.barcode ?? engineBarcode,
717
911
  layoutVariant: pakbonLayoutVariant(engineVariant),
718
912
  layoutId: String(layout.id ?? "") || null,
719
913
  printJobId: String(job.id ?? "") || null,
@@ -771,15 +965,30 @@ export async function bouwHtml(db, job, keuze, uitkomst) {
771
965
  "Zet in de layoutinstellingen minstens één onderdeel aan.");
772
966
  }
773
967
  const logos = await logosVoor(db, layout);
774
- const gegevens = (p.data ?? p);
968
+ const meegegeven = (p.data ?? p);
969
+ // De opdracht bevat vaak alleen verwijzingen (order-id, kar-id). Zonder de
970
+ // echte ordergegevens zou het document als een lijst lege veldnamen worden
971
+ // afgedrukt, terwijl het ontwerp op het scherm wél gevuld is. Daarom halen
972
+ // we dezelfde ordergegevens op als de pakbon gebruikt.
973
+ const uitDatabase = heeftOrderInhoud(meegegeven)
974
+ ? null
975
+ : await laadOrderVanuitDatabase(db, p).catch(() => null);
976
+ const gegevens = { ...(uitDatabase ?? {}), ...(meegegeven ?? {}) };
775
977
  const profielData = p.voorbeeld === true ? { ...voorbeeldContext(), ...(gegevens ?? {}) } : gegevens;
776
- return renderProfielDocument(String(layout.documenttype), cfg, profielData, {
978
+ const profielHtml = renderProfielDocument(String(layout.documenttype), cfg, profielData, {
777
979
  breedteMm,
778
980
  hoogteMm,
779
981
  margeMm: Number(layout.marge_boven ?? 8),
780
982
  titel: job.documentnaam ?? String(layout.naam ?? "Document"),
781
983
  logoUrl: logos[0]?.viewUrl ?? null,
782
984
  });
985
+ // Fail closed: liever een duidelijke fout dan een vel papier met alleen
986
+ // veldnamen en streepjes erop.
987
+ if (!/<div class="blok"/.test(profielHtml)) {
988
+ throw new GeenDocumentFout(`Voor "${String(layout.naam ?? "")}" zijn geen gegevens gevonden om af te drukken. ` +
989
+ "Controleer of de opdracht een order of kar meestuurt.");
990
+ }
991
+ return profielHtml;
783
992
  }
784
993
  /**
785
994
  * Controleert of het document echt iets afdrukt. Zonder deze controle kan een
@@ -824,6 +1033,8 @@ async function wachtOpLayout(db, job, reden) {
824
1033
  .from("print_jobs")
825
1034
  .update({
826
1035
  status: "wachtend",
1036
+ lokale_pdf_status: "wachtend",
1037
+ lokale_pdf_fout: reden.slice(0, 500),
827
1038
  foutmelding: reden.slice(0, 500),
828
1039
  volgende_poging: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
829
1040
  opgepakt_op: null,
@@ -842,6 +1053,9 @@ async function mislukt(db, job, reden, terugNaarWachtend) {
842
1053
  .from("print_jobs")
843
1054
  .update({
844
1055
  status: opnieuw ? "wachtend" : "mislukt",
1056
+ verwerkingsfase: opnieuw ? "retry_wacht" : "mislukt",
1057
+ fase_voltooid_op: opnieuw ? null : new Date().toISOString(),
1058
+ foutcode: opnieuw ? "tijdelijke_fout" : "blijvende_fout",
845
1059
  foutmelding: reden.slice(0, 500),
846
1060
  volgende_poging: opnieuw ? new Date(Date.now() + wachtMin * 60000).toISOString() : null,
847
1061
  voltooid_op: opnieuw ? null : new Date().toISOString(),
@@ -850,7 +1064,67 @@ async function mislukt(db, job, reden, terugNaarWachtend) {
850
1064
  .eq("id", job.id);
851
1065
  await log(db, job.id, opnieuw ? "opnieuw_proberen" : "mislukt", reden);
852
1066
  }
1067
+ function pdfBytes(base64) {
1068
+ const bron = Buffer.from(base64, "base64");
1069
+ const inhoud = new Uint8Array(new ArrayBuffer(bron.byteLength));
1070
+ inhoud.set(bron);
1071
+ return inhoud;
1072
+ }
1073
+ async function bewaarPdfArtefact(db, jobId, base64) {
1074
+ const inhoud = pdfBytes(base64);
1075
+ const pad = `${jobId}.pdf`;
1076
+ const digest = await crypto.subtle.digest("SHA-256", inhoud);
1077
+ const sha256 = Array.from(new Uint8Array(digest), (waarde) => waarde.toString(16).padStart(2, "0")).join("");
1078
+ const { error } = await db.storage
1079
+ .from("print-artifacts")
1080
+ .upload(pad, inhoud, { contentType: "application/pdf", upsert: true });
1081
+ if (error)
1082
+ throw new PdfRenderFout("mislukt", `Het PDF-bestand kon niet veilig worden opgeslagen: ${error.message}`);
1083
+ return { pad, sha256, bytes: inhoud.byteLength };
1084
+ }
1085
+ /**
1086
+ * Kleine faseklok: meet per opdracht hoe lang claimen, layout kiezen, printer
1087
+ * kiezen, renderen en versturen duren. Eén logregel per opdracht, zodat de
1088
+ * meting zelf het printpad niet vertraagt.
1089
+ */
1090
+ class Faseklok {
1091
+ constructor(job) {
1092
+ this.start = Date.now();
1093
+ this.vorige = Date.now();
1094
+ this.fasen = [];
1095
+ const aangemaakt = job.created_at ? Date.parse(String(job.created_at)) : NaN;
1096
+ if (Number.isFinite(aangemaakt))
1097
+ this.fasen.push(`claimed=${this.start - aangemaakt}ms`);
1098
+ }
1099
+ markeer(fase) {
1100
+ const nu = Date.now();
1101
+ this.fasen.push(`${fase}=${nu - this.vorige}ms`);
1102
+ this.vorige = nu;
1103
+ }
1104
+ samenvatting() {
1105
+ return [...this.fasen, `totaal=${Date.now() - this.start}ms`].join(" ");
1106
+ }
1107
+ }
1108
+ /**
1109
+ * Verwerkt items met een vaste maximale gelijktijdigheid. Geen onbeperkte
1110
+ * Promise.all: één trage of mislukte opdracht mag de rest niet meeslepen.
1111
+ */
1112
+ async function metPool(items, max, fn) {
1113
+ const uit = new Array(items.length);
1114
+ let volgende = 0;
1115
+ const werkers = Array.from({ length: Math.max(1, Math.min(max, items.length)) }, async () => {
1116
+ for (;;) {
1117
+ const i = volgende++;
1118
+ if (i >= items.length)
1119
+ return;
1120
+ uit[i] = await fn(items[i]);
1121
+ }
1122
+ });
1123
+ await Promise.all(werkers);
1124
+ return uit;
1125
+ }
853
1126
  async function verwerkJob(db, job) {
1127
+ const klok = new Faseklok(job);
854
1128
  try {
855
1129
  // Dubbel printen voorkomen: deze opdracht staat al bij de printer.
856
1130
  if (job.extern_job_id) {
@@ -879,8 +1153,10 @@ async function verwerkJob(db, job) {
879
1153
  return "mislukt";
880
1154
  }
881
1155
  const keuze = klaar ? { layout: null, ontwerpWint: false } : await zoekLayout(db, job);
1156
+ klok.markeer("layout_resolved");
882
1157
  const layout = keuze.layout;
883
1158
  const printerKeuze = await zoekPrinter(db, job, layout);
1159
+ klok.markeer("printer_resolved");
884
1160
  const printer = printerKeuze.printer;
885
1161
  // Virtuele PDF-printer: geen PrintNode nodig; de PDF wordt bewaard.
886
1162
  const isPdfPrinter = String(printer?.afdrukmethode ?? "") === "pdf";
@@ -912,26 +1188,38 @@ async function verwerkJob(db, job) {
912
1188
  landschap: liggend,
913
1189
  });
914
1190
  }
1191
+ klok.markeer("rendered");
915
1192
  // Virtuele PDF-printer: niets gaat naar een fysieke printer; de opgemaakte
916
1193
  // PDF wordt op de opdracht bewaard en is daarna te downloaden in de
917
1194
  // wachtrij. De opdracht is meteen voltooid.
918
1195
  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'.
1196
+ // Bewaar de PDF één keer als binair artefact. De wachtrij en Realtime
1197
+ // vervoeren daarna alleen metadata; de werkplek downloadt precies dit bestand.
1198
+ const artefact = await bewaarPdfArtefact(db, String(job.id), pdfBase64);
1199
+ // De opdracht blijft bewust op 'bezig' staan: hij is pas voltooid als de
1200
+ // werkplek het bestand echt in de PDF-map heeft gezet. De lokale service
1201
+ // zet daarna 'opgeslagen' + 'voltooid', of 'mislukt' met een leesbare zin.
922
1202
  const { error: statusFout } = await db
923
1203
  .from("print_jobs")
924
1204
  .update({
925
1205
  printer_id: printer.id,
926
1206
  gebruikte_layout_id: uitkomst.bron === "extern_pdf" ? null : (layout?.id ?? null),
927
1207
  gebruikte_bron: uitkomst.bron,
928
- status: "voltooid",
1208
+ status: "bezig",
1209
+ verwerkingsfase: "pdf_gereed",
1210
+ fase_voltooid_op: new Date().toISOString(),
1211
+ foutcode: null,
929
1212
  lokale_pdf_status: "gereed",
1213
+ lokale_pdf_print_plek_id: (await zoekPrintPlekId(db, job)) || null,
930
1214
  lokale_pdf_fout: null,
931
1215
  lokaal_opgeslagen_op: null,
932
1216
  foutmelding: null,
1217
+ artefact_pad: artefact.pad,
1218
+ artefact_sha256: artefact.sha256,
1219
+ artefact_bytes: artefact.bytes,
1220
+ artefact_gereed_op: new Date().toISOString(),
1221
+ resultaat_pdf_base64: null,
933
1222
  volgende_poging: null,
934
- voltooid_op: new Date().toISOString(),
935
1223
  updated_at: new Date().toISOString(),
936
1224
  })
937
1225
  .eq("id", job.id);
@@ -939,23 +1227,8 @@ async function verwerkJob(db, job) {
939
1227
  await mislukt(db, job, `Opdracht afronden mislukt: ${statusFout.message}`, true);
940
1228
  return "mislukt";
941
1229
  }
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"}.`);
1230
+ klok.markeer("pdf_ready");
1231
+ await log(db, job.id, "pdf_gereed", `PDF gereed voor lokale opslag via ${printer.naam ?? "PDF-printer"}. ${klok.samenvatting()}`);
959
1232
  return "verstuurd";
960
1233
  }
961
1234
  const formaten = await zorgVoorFormaten(db, printer);
@@ -979,12 +1252,16 @@ async function verwerkJob(db, job) {
979
1252
  gebruikte_layout_id: uitkomst.bron === "extern_pdf" ? null : (layout?.id ?? null),
980
1253
  gebruikte_bron: uitkomst.bron,
981
1254
  extern_job_id: externJobId,
1255
+ verwerkingsfase: "afleveren",
1256
+ fase_voltooid_op: new Date().toISOString(),
1257
+ foutcode: null,
982
1258
  foutmelding: null,
983
1259
  volgende_poging: null,
984
1260
  updated_at: new Date().toISOString(),
985
1261
  })
986
1262
  .eq("id", job.id);
987
- await log(db, job.id, "verstuurd", `Naar printer ${printer.naam ?? ""}. ${papier.uitleg}`);
1263
+ klok.markeer("submitted");
1264
+ await log(db, job.id, "verstuurd", `Naar printer ${printer.naam ?? ""}. ${papier.uitleg} ${klok.samenvatting()}`);
988
1265
  return "verstuurd";
989
1266
  }
990
1267
  catch (e) {
@@ -1003,16 +1280,26 @@ async function verwerkJob(db, job) {
1003
1280
  const VERVAL_BEZIG_MIN = 30;
1004
1281
  /** Een claim zonder verzending mag hooguit zo lang blijven hangen. */
1005
1282
  const VERVAL_CLAIM_MIN = 10;
1006
- /** Werkt de status bij van opdrachten die al naar een printer zijn gestuurd. */
1283
+ /** Hoeveel statuscontroles hooguit per ronde, en met welke gelijktijdigheid. */
1284
+ const STATUS_MAX_PER_RONDE = 12;
1285
+ const STATUS_PARALLEL = 3;
1286
+ /**
1287
+ * Werkt de status bij van opdrachten die al naar een printer zijn gestuurd.
1288
+ * Dit is historisch werk: het draait pas nadat nieuwe opdrachten zijn
1289
+ * afgehandeld, met een begrensde pool en een vaste limiet per ronde.
1290
+ */
1007
1291
  async function synchroniseerStatussen(db) {
1008
1292
  const { data } = await db
1009
1293
  .from("print_jobs")
1010
1294
  .select("id, extern_job_id, pogingen, opgepakt_op")
1011
1295
  .eq("status", "bezig")
1012
1296
  .not("extern_job_id", "is", null)
1013
- .limit(25);
1014
- let bijgewerkt = 0;
1015
- for (const job of arr(data)) {
1297
+ .order("created_at", { ascending: true })
1298
+ .limit(STATUS_MAX_PER_RONDE);
1299
+ const jobs = arr(data);
1300
+ if (!jobs.length)
1301
+ return 0;
1302
+ const uitkomsten = await metPool(jobs, STATUS_PARALLEL, async (job) => {
1016
1303
  try {
1017
1304
  const status = await opdrachtStatus(String(job.extern_job_id));
1018
1305
  if (status === "voltooid" || status === "mislukt") {
@@ -1026,8 +1313,9 @@ async function synchroniseerStatussen(db) {
1026
1313
  })
1027
1314
  .eq("id", job.id);
1028
1315
  await log(db, job.id, status);
1029
- bijgewerkt++;
1316
+ return 1;
1030
1317
  }
1318
+ return 0;
1031
1319
  }
1032
1320
  catch (e) {
1033
1321
  // Een onbereikbare printdienst mag niet stil blijven: tel hem als poging
@@ -1043,9 +1331,10 @@ async function synchroniseerStatussen(db) {
1043
1331
  .eq("id", job.id);
1044
1332
  await log(db, job.id, "status_onbekend", reden);
1045
1333
  console.warn(`[printwachtrij] status ophalen mislukt voor job ${job.id}: ${reden}`);
1334
+ return 0;
1046
1335
  }
1047
- }
1048
- return bijgewerkt;
1336
+ });
1337
+ return uitkomsten.reduce((n, v) => n + v, 0);
1049
1338
  }
1050
1339
  /**
1051
1340
  * Vangnet voor opdrachten die blijven hangen op 'bezig'. Twee gevallen:
@@ -1087,7 +1376,8 @@ async function vervalControle(db) {
1087
1376
  .select("id, pogingen")
1088
1377
  .eq("status", "bezig")
1089
1378
  .is("extern_job_id", null)
1090
- .neq("lokale_pdf_status", "gereed")
1379
+ // PDF-opdrachten die op de werkplek wachten zijn niet vastgelopen.
1380
+ .or("lokale_pdf_status.is.null,lokale_pdf_status.not.in.(wachtend,bezig,gereed)")
1091
1381
  .or(`opgepakt_op.lt.${grensClaim},and(opgepakt_op.is.null,updated_at.lt.${grensClaim})`)
1092
1382
  .limit(50);
1093
1383
  for (const job of arr(vastgelopen)) {
@@ -1111,50 +1401,116 @@ async function vervalControle(db) {
1111
1401
  }
1112
1402
  return aantal;
1113
1403
  }
1404
+ /** Hoeveel opdrachten tegelijk worden gerenderd binnen één ronde. */
1405
+ const JOB_PARALLEL = 3;
1406
+ /** Zo lang mag één ronde het slot houden; daarna is hij vanzelf herstelbaar. */
1407
+ const LEASE_SECONDEN = 120;
1408
+ const LEASE_NAAM = "print-queue-worker";
1409
+ /** Rustpauze voordat een volgende ronde wordt gewekt. */
1410
+ const VERVOLG_COOLDOWN_MS = 1500;
1411
+ /**
1412
+ * Wekt de wachtrij opnieuw, maar uitsluitend wanneer er aantoonbaar wachtend
1413
+ * werk overblijft. Het adres van de runner staat in de database, nooit in code.
1414
+ */
1415
+ async function wekVervolgRonde(db) {
1416
+ const { count } = await db
1417
+ .from("print_jobs")
1418
+ .select("id", { count: "exact", head: true })
1419
+ .eq("status", "wachtend")
1420
+ .or(`volgende_poging.is.null,volgende_poging.lte.${new Date().toISOString()}`);
1421
+ if (!count)
1422
+ return;
1423
+ await new Promise((r) => setTimeout(r, VERVOLG_COOLDOWN_MS));
1424
+ const { error } = await db.rpc("wake_print_queue", { _min_interval_ms: 1000 });
1425
+ if (error)
1426
+ console.warn(`[printwachtrij] vervolgronde wekken mislukt: ${error.message}`);
1427
+ }
1114
1428
  /** Eén ronde: vastgelopen opdrachten opruimen, afdrukken en statussen bijwerken. */
1115
1429
  export async function verwerkWachtrij(db, max = 5) {
1116
- // Eerst het vangnet: dit draait ook als er niets te claimen valt.
1430
+ const leeg = {
1431
+ opgepakt: 0,
1432
+ verstuurd: 0,
1433
+ mislukt: 0,
1434
+ wachtend: 0,
1435
+ bijgewerkt: 0,
1436
+ };
1437
+ // Single-flight: hooguit één ronde tegelijk. Het slot verloopt vanzelf, dus
1438
+ // een gecrashte ronde blokkeert de wachtrij nooit langer dan de leasetijd.
1439
+ const houder = crypto.randomUUID();
1440
+ const { data: slot, error: slotFout } = await db.rpc("claim_print_queue_lease", {
1441
+ _naam: LEASE_NAAM,
1442
+ _houder: houder,
1443
+ _seconden: LEASE_SECONDEN,
1444
+ });
1445
+ if (slotFout)
1446
+ throw new Error(`Printronde kon niet starten: ${slotFout.message}`);
1447
+ if (slot !== true)
1448
+ return { ...leeg, overgeslagen: "Er draait al een printronde." };
1449
+ let vervolgNodig = false;
1117
1450
  try {
1118
- await vervalControle(db);
1119
- }
1120
- catch (e) {
1121
- console.warn(`[printwachtrij] vervalcontrole mislukt: ${e instanceof Error ? e.message : "onbekend"}`);
1451
+ // Eerst het vangnet: dit draait ook als er niets te claimen valt.
1452
+ try {
1453
+ await vervalControle(db);
1454
+ }
1455
+ catch (e) {
1456
+ console.warn(`[printwachtrij] vervalcontrole mislukt: ${e instanceof Error ? e.message : "onbekend"}`);
1457
+ }
1458
+ // PDF-opdrachten hebben een eigen claimroute. Daardoor kunnen oudere
1459
+ // gepubliceerde moduleversies, die een PDF-printer nog als PrintNode-printer
1460
+ // behandelen, deze opdrachten niet meer vóór de actuele runner oppakken.
1461
+ const { data: pdfData, error: pdfError } = await db.rpc("claim_pdf_print_jobs", { _max: max });
1462
+ if (pdfError)
1463
+ throw new Error(pdfError.message);
1464
+ const pdfJobs = arr(pdfData);
1465
+ const resterend = Math.max(0, max - pdfJobs.length);
1466
+ let gewoneJobs = [];
1467
+ if (resterend > 0) {
1468
+ const { data, error } = await db.rpc("claim_cloud_print_jobs", { _max: resterend });
1469
+ if (error)
1470
+ throw new Error(error.message);
1471
+ gewoneJobs = arr(data);
1472
+ }
1473
+ // Begrensd parallel: meerdere pakbonnen van één order lopen gelijktijdig,
1474
+ // maar één mislukte opdracht annuleert de andere nooit.
1475
+ const jobs = [...pdfJobs, ...gewoneJobs];
1476
+ const uitkomsten = await metPool(jobs, JOB_PARALLEL, async (job) => {
1477
+ try {
1478
+ return await verwerkJob(db, job);
1479
+ }
1480
+ catch (e) {
1481
+ console.error(`[printwachtrij] opdracht ${job.id} viel onverwacht uit: ${e instanceof Error ? e.message : "onbekend"}`);
1482
+ return "mislukt";
1483
+ }
1484
+ });
1485
+ const resultaat = {
1486
+ opgepakt: jobs.length,
1487
+ verstuurd: uitkomsten.filter((u) => u === "verstuurd").length,
1488
+ wachtend: uitkomsten.filter((u) => u === "wachtend").length,
1489
+ mislukt: uitkomsten.filter((u) => u === "mislukt").length,
1490
+ // Historische statuscontrole komt ná het nieuwe werk.
1491
+ bijgewerkt: await synchroniseerStatussen(db),
1492
+ };
1493
+ // Alleen wanneer de batchlimiet vol zat kan er nog werk klaarstaan.
1494
+ vervolgNodig = jobs.length >= max;
1495
+ return resultaat;
1122
1496
  }
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 });
1497
+ finally {
1498
+ const { error } = await db.rpc("release_print_queue_lease", {
1499
+ _naam: LEASE_NAAM,
1500
+ _houder: houder,
1501
+ });
1134
1502
  if (error)
1135
- throw new Error(error.message);
1136
- gewoneJobs = arr(data);
1137
- }
1138
- const jobs = [...pdfJobs, ...gewoneJobs];
1139
- let verstuurd = 0;
1140
- let mislukteJobs = 0;
1141
- let wachtend = 0;
1142
- for (const job of jobs) {
1143
- const uitkomst = await verwerkJob(db, job);
1144
- if (uitkomst === "verstuurd")
1145
- verstuurd++;
1146
- else if (uitkomst === "wachtend")
1147
- wachtend++;
1148
- else
1149
- mislukteJobs++;
1503
+ console.warn(`[printwachtrij] slot vrijgeven mislukt: ${error.message}`);
1504
+ if (vervolgNodig) {
1505
+ try {
1506
+ await wekVervolgRonde(db);
1507
+ }
1508
+ catch (fout) {
1509
+ const melding = fout instanceof Error ? fout.message : "onbekend";
1510
+ console.warn(`[printwachtrij] vervolgronde mislukt: ${melding}`);
1511
+ }
1512
+ }
1150
1513
  }
1151
- return {
1152
- opgepakt: jobs.length,
1153
- verstuurd,
1154
- mislukt: mislukteJobs,
1155
- wachtend,
1156
- bijgewerkt: await synchroniseerStatussen(db),
1157
- };
1158
1514
  }
1159
1515
  /**
1160
1516
  * Dezelfde documentkeuze als de printwachtrij maakt, maar zonder printer en