@flowselections/layout-maker 1.38.4 → 1.45.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-lib/components/AfleverbriefBuilderPage.d.ts +10 -0
- package/dist-lib/components/AfleverbriefBuilderPage.d.ts.map +1 -0
- package/dist-lib/components/AfleverbriefBuilderPage.js +101 -0
- package/dist-lib/components/BlokOntwerperPage.d.ts.map +1 -1
- package/dist-lib/components/BlokOntwerperPage.js +8 -3
- package/dist-lib/components/ComputersLijst.d.ts.map +1 -1
- package/dist-lib/components/ComputersLijst.js +1 -1
- package/dist-lib/components/FustKolomVolgorde.d.ts +11 -0
- package/dist-lib/components/FustKolomVolgorde.d.ts.map +1 -0
- package/dist-lib/components/FustKolomVolgorde.js +52 -0
- package/dist-lib/components/LayoutsPage.d.ts.map +1 -1
- package/dist-lib/components/LayoutsPage.js +10 -9
- package/dist-lib/components/PakbonBuilderPage.d.ts.map +1 -1
- package/dist-lib/components/PakbonBuilderPage.js +32 -14
- package/dist-lib/components/PakbonPreview.d.ts.map +1 -1
- package/dist-lib/components/PakbonPreview.js +21 -14
- package/dist-lib/components/PakbonSectiesPanel.d.ts +4 -2
- package/dist-lib/components/PakbonSectiesPanel.d.ts.map +1 -1
- package/dist-lib/components/PakbonSectiesPanel.js +5 -3
- package/dist-lib/components/PdfMapToestemmingBanner.d.ts +2 -1
- package/dist-lib/components/PdfMapToestemmingBanner.d.ts.map +1 -1
- package/dist-lib/components/PdfMapToestemmingBanner.js +124 -18
- package/dist-lib/components/PdfVoorbeeld.d.ts +20 -0
- package/dist-lib/components/PdfVoorbeeld.d.ts.map +1 -0
- package/dist-lib/components/PdfVoorbeeld.js +95 -0
- package/dist-lib/components/PrintProgrammaUitleg.d.ts.map +1 -1
- package/dist-lib/components/PrintProgrammaUitleg.js +1 -1
- package/dist-lib/components/PrintQueuePage.d.ts.map +1 -1
- package/dist-lib/components/PrintQueuePage.js +81 -43
- package/dist-lib/components/PrintplekDetailPage.d.ts.map +1 -1
- package/dist-lib/components/PrintplekDetailPage.js +2 -2
- package/dist-lib/components/PrintplekWizard.d.ts.map +1 -1
- package/dist-lib/components/PrintplekWizard.js +6 -2
- package/dist-lib/components/ProductiebonBuilderPage.d.ts.map +1 -1
- package/dist-lib/components/ProductiebonBuilderPage.js +13 -7
- package/dist-lib/index.d.ts +13 -5
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +10 -5
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +2280 -198
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +2280 -198
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.js +1 -1
- package/dist-lib/integrations/supabase/moduleClient.d.ts +2280 -198
- package/dist-lib/integrations/supabase/moduleClient.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +2306 -182
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +1 -0
- package/dist-lib/lib/aiLayout.functions.d.ts +2280 -198
- package/dist-lib/lib/aiLayout.functions.d.ts.map +1 -1
- package/dist-lib/lib/documentVoorbeeld.functions.d.ts +23 -0
- package/dist-lib/lib/documentVoorbeeld.functions.d.ts.map +1 -0
- package/dist-lib/lib/documentVoorbeeld.functions.js +37 -0
- package/dist-lib/lib/fontMetrics.d.ts +15 -0
- package/dist-lib/lib/fontMetrics.d.ts.map +1 -0
- package/dist-lib/lib/fontMetrics.js +118 -0
- package/dist-lib/lib/htmlNaarPdf.server.d.ts +2 -2
- package/dist-lib/lib/htmlNaarPdf.server.d.ts.map +1 -1
- package/dist-lib/lib/htmlNaarPdf.server.js +2 -1
- package/dist-lib/lib/htmlPdf.server.d.ts +11 -0
- package/dist-lib/lib/htmlPdf.server.d.ts.map +1 -1
- package/dist-lib/lib/htmlPdf.server.js +118 -15
- package/dist-lib/lib/orderData.server.d.ts.map +1 -1
- package/dist-lib/lib/orderData.server.js +43 -16
- package/dist-lib/lib/pakbonBarcodeVerzoek.d.ts +39 -0
- package/dist-lib/lib/pakbonBarcodeVerzoek.d.ts.map +1 -0
- package/dist-lib/lib/pakbonBarcodeVerzoek.js +80 -0
- package/dist-lib/lib/pakbonBuilder.d.ts +62 -3
- package/dist-lib/lib/pakbonBuilder.d.ts.map +1 -1
- package/dist-lib/lib/pakbonBuilder.js +386 -50
- package/dist-lib/lib/pakbonContent.d.ts +41 -0
- package/dist-lib/lib/pakbonContent.d.ts.map +1 -0
- package/dist-lib/lib/pakbonContent.js +668 -0
- package/dist-lib/lib/pakbonDocument.d.ts +199 -0
- package/dist-lib/lib/pakbonDocument.d.ts.map +1 -0
- package/dist-lib/lib/pakbonDocument.js +735 -0
- package/dist-lib/lib/pakbonHtml.d.ts +7 -0
- package/dist-lib/lib/pakbonHtml.d.ts.map +1 -1
- package/dist-lib/lib/pakbonHtml.js +89 -73
- package/dist-lib/lib/pakbonPresentation.d.ts +8 -0
- package/dist-lib/lib/pakbonPresentation.d.ts.map +1 -0
- package/dist-lib/lib/pakbonPresentation.js +31 -0
- package/dist-lib/lib/pakbonScan.d.ts.map +1 -1
- package/dist-lib/lib/pakbonScan.js +20 -38
- package/dist-lib/lib/pdfJobSaver.d.ts +10 -6
- package/dist-lib/lib/pdfJobSaver.d.ts.map +1 -1
- package/dist-lib/lib/pdfJobSaver.js +302 -99
- package/dist-lib/lib/pdfMap.d.ts +37 -2
- package/dist-lib/lib/pdfMap.d.ts.map +1 -1
- package/dist-lib/lib/pdfMap.js +120 -7
- package/dist-lib/lib/pdfOpslagStatus.d.ts +23 -0
- package/dist-lib/lib/pdfOpslagStatus.d.ts.map +1 -0
- package/dist-lib/lib/pdfOpslagStatus.js +74 -0
- package/dist-lib/lib/printApi.d.ts +31 -0
- package/dist-lib/lib/printApi.d.ts.map +1 -1
- package/dist-lib/lib/printApi.js +62 -1
- package/dist-lib/lib/printQueue.server.d.ts +6 -0
- package/dist-lib/lib/printQueue.server.d.ts.map +1 -1
- package/dist-lib/lib/printQueue.server.js +450 -94
- package/dist-lib/lib/printService.functions.d.ts +8857 -529
- package/dist-lib/lib/printService.functions.d.ts.map +1 -1
- package/dist-lib/lib/profielHtml.d.ts.map +1 -1
- package/dist-lib/lib/profielHtml.js +8 -1
- package/dist-lib/lib/useDocumentData.d.ts +8 -0
- package/dist-lib/lib/useDocumentData.d.ts.map +1 -1
- package/dist-lib/lib/useDocumentData.js +1 -1
- package/dist-lib/lib/voorbeeldPrint.d.ts +3 -0
- package/dist-lib/lib/voorbeeldPrint.d.ts.map +1 -1
- package/dist-lib/lib/voorbeeldPrint.js +65 -17
- package/package.json +9 -2
|
@@ -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 {
|
|
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 [
|
|
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(
|
|
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
|
-
|
|
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
|
|
433
|
-
// printer_id
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
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
|
-
|
|
826
|
+
let barcode = String(engineBarcode ?? "").trim();
|
|
670
827
|
if (!barcode) {
|
|
671
|
-
|
|
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
|
-
|
|
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
|
-
|
|
685
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
920
|
-
//
|
|
921
|
-
|
|
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: "
|
|
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
|
-
|
|
943
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
.
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
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
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|