@flowselections/layout-maker 1.3.3 → 1.6.6

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 (112) hide show
  1. package/README.md +24 -24
  2. package/dist-lib/components/AfdrukKnop.d.ts +3 -1
  3. package/dist-lib/components/AfdrukKnop.d.ts.map +1 -1
  4. package/dist-lib/components/AfdrukKnop.js +24 -3
  5. package/dist-lib/components/ComputersLijst.d.ts +23 -0
  6. package/dist-lib/components/ComputersLijst.d.ts.map +1 -0
  7. package/dist-lib/components/ComputersLijst.js +75 -0
  8. package/dist-lib/components/LayoutsPage.d.ts.map +1 -1
  9. package/dist-lib/components/LayoutsPage.js +2 -0
  10. package/dist-lib/components/LogosPage.d.ts.map +1 -1
  11. package/dist-lib/components/LogosPage.js +2 -0
  12. package/dist-lib/components/NetwerkPrintersDialog.d.ts +8 -0
  13. package/dist-lib/components/NetwerkPrintersDialog.d.ts.map +1 -0
  14. package/dist-lib/components/NetwerkPrintersDialog.js +79 -0
  15. package/dist-lib/components/PrintProgrammaUitleg.d.ts +5 -0
  16. package/dist-lib/components/PrintProgrammaUitleg.d.ts.map +1 -0
  17. package/dist-lib/components/PrintProgrammaUitleg.js +19 -0
  18. package/dist-lib/components/PrintQueuePage.d.ts.map +1 -1
  19. package/dist-lib/components/PrintQueuePage.js +37 -3
  20. package/dist-lib/components/PrinterKaart.d.ts +17 -0
  21. package/dist-lib/components/PrinterKaart.d.ts.map +1 -0
  22. package/dist-lib/components/PrinterKaart.js +14 -0
  23. package/dist-lib/components/PrinterSetupWizard.d.ts +11 -1
  24. package/dist-lib/components/PrinterSetupWizard.d.ts.map +1 -1
  25. package/dist-lib/components/PrinterSetupWizard.js +134 -64
  26. package/dist-lib/components/PrinterStatsKaarten.d.ts +11 -0
  27. package/dist-lib/components/PrinterStatsKaarten.d.ts.map +1 -0
  28. package/dist-lib/components/PrinterStatsKaarten.js +8 -0
  29. package/dist-lib/components/PrintersPage.d.ts.map +1 -1
  30. package/dist-lib/components/PrintersPage.js +120 -49
  31. package/dist-lib/components/WerkplekConfiguratiePage.d.ts +2 -0
  32. package/dist-lib/components/WerkplekConfiguratiePage.d.ts.map +1 -0
  33. package/dist-lib/components/WerkplekConfiguratiePage.js +101 -0
  34. package/dist-lib/components/WerkplekkenTabel.d.ts +14 -0
  35. package/dist-lib/components/WerkplekkenTabel.d.ts.map +1 -0
  36. package/dist-lib/components/WerkplekkenTabel.js +16 -0
  37. package/dist-lib/index.d.ts +11 -0
  38. package/dist-lib/index.d.ts.map +1 -1
  39. package/dist-lib/index.js +11 -0
  40. package/dist-lib/integrations/supabase/auth-middleware.d.ts +925 -34
  41. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  42. package/dist-lib/integrations/supabase/client.server.d.ts +925 -34
  43. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  44. package/dist-lib/integrations/supabase/types.d.ts +920 -29
  45. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  46. package/dist-lib/integrations/supabase/types.js +12 -1
  47. package/dist-lib/lib/aiLayout.functions.d.ts +925 -34
  48. package/dist-lib/lib/aiLayout.functions.d.ts.map +1 -1
  49. package/dist-lib/lib/aiLayout.server.js +9 -9
  50. package/dist-lib/lib/browserPrint.d.ts +1 -0
  51. package/dist-lib/lib/browserPrint.d.ts.map +1 -1
  52. package/dist-lib/lib/browserPrint.js +6 -6
  53. package/dist-lib/lib/centralAuth.server.d.ts +13 -0
  54. package/dist-lib/lib/centralAuth.server.d.ts.map +1 -0
  55. package/dist-lib/lib/centralAuth.server.js +60 -0
  56. package/dist-lib/lib/directPrint.d.ts +26 -0
  57. package/dist-lib/lib/directPrint.d.ts.map +1 -0
  58. package/dist-lib/lib/directPrint.js +65 -0
  59. package/dist-lib/lib/documentPdf.d.ts +11 -0
  60. package/dist-lib/lib/documentPdf.d.ts.map +1 -0
  61. package/dist-lib/lib/documentPdf.js +62 -0
  62. package/dist-lib/lib/htmlNaarPdf.server.d.ts +14 -0
  63. package/dist-lib/lib/htmlNaarPdf.server.d.ts.map +1 -0
  64. package/dist-lib/lib/htmlNaarPdf.server.js +34 -0
  65. package/dist-lib/lib/htmlPdf.server.d.ts +29 -0
  66. package/dist-lib/lib/htmlPdf.server.d.ts.map +1 -0
  67. package/dist-lib/lib/htmlPdf.server.js +1009 -0
  68. package/dist-lib/lib/layoutPreview.d.ts +1 -1
  69. package/dist-lib/lib/layoutPreview.d.ts.map +1 -1
  70. package/dist-lib/lib/layoutPreview.js +9 -9
  71. package/dist-lib/lib/logoLibrary.d.ts +1 -1
  72. package/dist-lib/lib/logoLibrary.js +5 -3
  73. package/dist-lib/lib/orderData.server.d.ts +15 -0
  74. package/dist-lib/lib/orderData.server.d.ts.map +1 -0
  75. package/dist-lib/lib/orderData.server.js +194 -0
  76. package/dist-lib/lib/pakbonBuilder.d.ts.map +1 -1
  77. package/dist-lib/lib/pakbonBuilder.js +22 -2
  78. package/dist-lib/lib/pakbonHtml.js +50 -50
  79. package/dist-lib/lib/printQueue.server.d.ts +31 -0
  80. package/dist-lib/lib/printQueue.server.d.ts.map +1 -0
  81. package/dist-lib/lib/printQueue.server.js +435 -0
  82. package/dist-lib/lib/printService.functions.d.ts +103742 -0
  83. package/dist-lib/lib/printService.functions.d.ts.map +1 -0
  84. package/dist-lib/lib/printService.functions.js +94 -0
  85. package/dist-lib/lib/printService.server.d.ts +31 -0
  86. package/dist-lib/lib/printService.server.d.ts.map +1 -0
  87. package/dist-lib/lib/printService.server.js +110 -0
  88. package/dist-lib/lib/productiebonHtml.d.ts +18 -0
  89. package/dist-lib/lib/productiebonHtml.d.ts.map +1 -0
  90. package/dist-lib/lib/productiebonHtml.js +166 -0
  91. package/dist-lib/lib/profielHtml.d.ts +14 -0
  92. package/dist-lib/lib/profielHtml.d.ts.map +1 -0
  93. package/dist-lib/lib/profielHtml.js +256 -0
  94. package/dist-lib/lib/runnerRegistry.server.d.ts +10 -0
  95. package/dist-lib/lib/runnerRegistry.server.d.ts.map +1 -0
  96. package/dist-lib/lib/runnerRegistry.server.js +79 -0
  97. package/dist-lib/lib/supabaseAdmin.server.d.ts +6 -0
  98. package/dist-lib/lib/supabaseAdmin.server.d.ts.map +1 -0
  99. package/dist-lib/lib/supabaseAdmin.server.js +43 -0
  100. package/dist-lib/lib/testpagina.d.ts +2 -0
  101. package/dist-lib/lib/testpagina.d.ts.map +1 -0
  102. package/dist-lib/lib/testpagina.js +31 -0
  103. package/dist-lib/lib/useAutoRefresh.d.ts +8 -0
  104. package/dist-lib/lib/useAutoRefresh.d.ts.map +1 -0
  105. package/dist-lib/lib/useAutoRefresh.js +44 -0
  106. package/dist-lib/lib/useDocumentData.d.ts +4 -3
  107. package/dist-lib/lib/useDocumentData.d.ts.map +1 -1
  108. package/dist-lib/lib/useDocumentData.js +10 -7
  109. package/dist-lib/lib/useNetwerkPrinters.d.ts +12 -0
  110. package/dist-lib/lib/useNetwerkPrinters.d.ts.map +1 -0
  111. package/dist-lib/lib/useNetwerkPrinters.js +26 -0
  112. package/package.json +11 -3
@@ -0,0 +1,435 @@
1
+ // ============================================================================
2
+ // src/lib/printQueue.server.ts — verwerkt de printwachtrij op de achtergrond.
3
+ // Pakt wachtende opdrachten op, maakt er een PDF van en stuurt die stil naar
4
+ // de juiste printer. Draait alleen op de server (cron / interne aanroep).
5
+ // ============================================================================
6
+ import { adminDb as supabaseAdmin } from "./supabaseAdmin.server";
7
+ import { maakPakbonOrder, renderPakbonDocument } from "./pakbonHtml";
8
+ import { renderProductiebonDocument } from "./productiebonHtml";
9
+ import { heeftOrderInhoud, laadOrderVanuitDatabase } from "./orderData.server";
10
+ import { profielHeeftInhoud, renderProfielDocument } from "./profielHtml";
11
+ import { bouwDocumentHtml } from "./layoutPreview";
12
+ import { htmlNaarPdfServer, PdfRenderFout } from "./htmlNaarPdf.server";
13
+ import { stuurPdfNaarPrinter, opdrachtStatus } from "./printService.server";
14
+ const MAX_POGINGEN = 3;
15
+ const arr = (v) => (Array.isArray(v) ? v : []);
16
+ async function log(jobId, gebeurtenis, detail) {
17
+ await supabaseAdmin.from("print_job_logs").insert({
18
+ job_id: jobId,
19
+ gebeurtenis,
20
+ ...(detail ? { detail: String(detail).slice(0, 1000) } : {}),
21
+ });
22
+ }
23
+ /** Fout waarbij opnieuw proberen zinloos is (er valt niets te printen). */
24
+ class GeenDocumentFout extends Error {
25
+ constructor() {
26
+ super(...arguments);
27
+ this.name = "GeenDocumentFout";
28
+ }
29
+ }
30
+ /**
31
+ * Zoekt de layout die bij de opdracht hoort.
32
+ * Staat er een layout_id op de opdracht (bijv. gezet door de Logistiek
33
+ * Engine), dan is die leidend: die layout wordt gebruikt of de opdracht
34
+ * mislukt. Alleen zonder layout_id valt de wachtrij terug op de actieve
35
+ * layout van het documenttype.
36
+ */
37
+ async function zoekLayout(job) {
38
+ const uitPayload = job.payload?.layoutId ?? job.payload?.data?.layoutId ?? job.payload?.layout_id ?? null;
39
+ const id = job.layout_id ?? uitPayload;
40
+ if (id) {
41
+ const { data } = await supabaseAdmin
42
+ .from("document_layouts")
43
+ .select("*")
44
+ .eq("id", id)
45
+ .maybeSingle();
46
+ if (!data) {
47
+ throw new GeenDocumentFout("De layout die aan deze opdracht hangt bestaat niet meer. Koppel een geldige layout.");
48
+ }
49
+ return { layout: data, expliciet: true };
50
+ }
51
+ const { data } = await supabaseAdmin
52
+ .from("document_layouts")
53
+ .select("*")
54
+ .eq("documenttype", job.documenttype)
55
+ .eq("actief", true)
56
+ .order("is_standaard", { ascending: false })
57
+ .order("updated_at", { ascending: false })
58
+ .limit(1);
59
+ return { layout: (arr(data)[0] ?? null), expliciet: false };
60
+ }
61
+ /**
62
+ * Haalt een kant-en-klare PDF van de opdracht op (klantstickers, standaard
63
+ * Floriday-documenten). Die gaat ongewijzigd naar de printer.
64
+ */
65
+ export async function kantEnKlarePdf(job) {
66
+ const direct = typeof job.pdf_base64 === "string" ? job.pdf_base64.trim() : "";
67
+ if (direct.length > 100)
68
+ return direct.replace(/^data:application\/pdf;base64,/i, "");
69
+ const url = typeof job.pdf_url === "string" ? job.pdf_url.trim() : "";
70
+ if (!url)
71
+ return null;
72
+ if (!/^https?:\/\//i.test(url)) {
73
+ throw new GeenDocumentFout("Het PDF-adres op deze opdracht is geen geldig webadres.");
74
+ }
75
+ const afbreken = AbortSignal.timeout(20000);
76
+ let bytes;
77
+ try {
78
+ const res = await fetch(url, { signal: afbreken });
79
+ if (!res.ok)
80
+ throw new Error(`PDF ophalen mislukte (${res.status}).`);
81
+ bytes = new Uint8Array(await res.arrayBuffer());
82
+ }
83
+ catch (e) {
84
+ throw new PdfRenderFout("mislukt", `De kant-en-klare PDF kon niet worden opgehaald: ${e instanceof Error ? e.message : "onbekend"}`);
85
+ }
86
+ const kop = String.fromCharCode(...bytes.slice(0, 5));
87
+ if (!kop.startsWith("%PDF")) {
88
+ throw new GeenDocumentFout("Het opgehaalde bestand is geen PDF.");
89
+ }
90
+ let ruw = "";
91
+ for (let i = 0; i < bytes.length; i += 8192) {
92
+ ruw += String.fromCharCode(...bytes.subarray(i, i + 8192));
93
+ }
94
+ return btoa(ruw);
95
+ }
96
+ /** Bepaalt op welke printer de opdracht moet uitkomen. */
97
+ async function zoekPrinter(job, layout) {
98
+ const pak = async (id) => {
99
+ if (!id)
100
+ return null;
101
+ const { data } = await supabaseAdmin
102
+ .from("printers")
103
+ .select("id, naam, extern_printer_id, actief")
104
+ .eq("id", id)
105
+ .maybeSingle();
106
+ return data && data.actief !== false ? data : null;
107
+ };
108
+ let printer = await pak(job.printer_id);
109
+ if (printer)
110
+ return printer;
111
+ if (layout?.id) {
112
+ const { data } = await supabaseAdmin
113
+ .from("printer_mappings")
114
+ .select("printer_id")
115
+ .eq("layout_id", layout.id)
116
+ .order("prioriteit", { ascending: true })
117
+ .limit(1);
118
+ printer = await pak(arr(data)[0]?.printer_id);
119
+ if (printer)
120
+ return printer;
121
+ }
122
+ const formaat = layout?.papierformaat ?? "A4";
123
+ const { data: fm } = await supabaseAdmin
124
+ .from("document_formaat_printers")
125
+ .select("printer_id")
126
+ .eq("documenttype", job.documenttype)
127
+ .eq("formaat_code", formaat)
128
+ .maybeSingle();
129
+ printer = await pak(fm?.printer_id);
130
+ if (printer)
131
+ return printer;
132
+ const { data: enige } = await supabaseAdmin
133
+ .from("printers")
134
+ .select("id, naam, extern_printer_id, actief")
135
+ .eq("actief", true)
136
+ .not("extern_printer_id", "is", null)
137
+ .limit(2);
138
+ const kandidaten = arr(enige);
139
+ return kandidaten.length === 1 ? kandidaten[0] : null;
140
+ }
141
+ /** Haalt de logo's op die in de layout gebruikt worden, met tijdelijke URL. */
142
+ async function logosVoor(layout) {
143
+ const logoId = layout?.config?.logo?.logoId ?? null;
144
+ if (!logoId)
145
+ return [];
146
+ const { data } = await supabaseAdmin
147
+ .from("layout_logos")
148
+ .select("id, naam, bestandspad, bestand_url")
149
+ .eq("id", logoId)
150
+ .maybeSingle();
151
+ const logo = data;
152
+ if (!logo)
153
+ return [];
154
+ let viewUrl = logo.bestand_url ?? null;
155
+ if (logo.bestandspad) {
156
+ const { data: sign } = await supabaseAdmin.storage
157
+ .from("layout-logos")
158
+ .createSignedUrl(logo.bestandspad, 3600);
159
+ if (sign?.signedUrl)
160
+ viewUrl = sign.signedUrl;
161
+ }
162
+ return viewUrl ? [{ id: logo.id, naam: logo.naam, viewUrl }] : [];
163
+ }
164
+ /**
165
+ * Bouwt de HTML van de opdracht.
166
+ * Volgorde: gekoppelde layout wint > meegestuurde HTML > actieve layout van
167
+ * het documenttype. Zo werkt een bijgewerkte layout meteen door.
168
+ */
169
+ export async function bouwHtml(job, keuze) {
170
+ const layout = keuze.layout;
171
+ const meegestuurd = typeof job.render_html === "string" && job.render_html.trim().length > 40
172
+ ? job.render_html
173
+ : null;
174
+ if (!layout) {
175
+ if (meegestuurd)
176
+ return meegestuurd;
177
+ throw new GeenDocumentFout("Er is geen PDF, geen layout en geen opmaak meegestuurd voor dit document.");
178
+ }
179
+ if (!keuze.expliciet && meegestuurd)
180
+ return meegestuurd;
181
+ const p = (job.payload ?? {});
182
+ const liggend = String(layout.orientatie ?? "portrait") === "landscape";
183
+ const b = Number(layout.breedte_mm ?? 210);
184
+ const h = Number(layout.hoogte_mm ?? 297);
185
+ const breedteMm = liggend ? h : b;
186
+ const hoogteMm = liggend ? b : h;
187
+ const cfg = (layout.config ?? {});
188
+ // Stickers met "kies altijd de layout van de klant": meegestuurde opmaak van
189
+ // de klant wint. Is er niets meegestuurd, dan bepaalt de terugvalkeuze of we
190
+ // deze layout gebruiken of de opdracht netjes laten mislukken.
191
+ if (cfg.klantlayout?.gebruik_klantlayout === true) {
192
+ if (meegestuurd)
193
+ return meegestuurd;
194
+ if (cfg.klantlayout?.terugval === "niet_printen") {
195
+ throw new GeenDocumentFout("Deze layout gebruikt altijd het ontwerp van de klant, maar er is geen klantontwerp meegestuurd.");
196
+ }
197
+ }
198
+ // 1) Pakbon-layouts. De secties staan onder `pakbon.secties`; oudere
199
+ // layouts hebben ze op het hoogste niveau. Beide vormen tellen mee.
200
+ const pakbonSecties = Array.isArray(cfg.secties)
201
+ ? cfg.secties
202
+ : Array.isArray(cfg.pakbon?.secties)
203
+ ? cfg.pakbon.secties
204
+ : null;
205
+ if ((pakbonSecties && pakbonSecties.length) || String(layout.documenttype) === "pakbon") {
206
+ const orderBron = p.order ?? p.data?.order ?? p.data ?? p;
207
+ const uitDb = heeftOrderInhoud(orderBron) ? null : await laadOrderVanuitDatabase(p);
208
+ const order = maakPakbonOrder(uitDb ?? orderBron);
209
+ return renderPakbonDocument(layout.config, order, {
210
+ breedteMm,
211
+ hoogteMm,
212
+ margeMm: Number(layout.marge_boven ?? 12),
213
+ titel: job.documentnaam ?? "Document",
214
+ logos: await logosVoor(layout),
215
+ });
216
+ }
217
+ // 2) Productiebon-layouts (blokken onder `productiebon.blokken`).
218
+ if (Array.isArray(cfg.productiebon?.blokken) ||
219
+ String(layout.documenttype) === "productiebon") {
220
+ return renderProductiebonDocument(layout.config, p.data ?? p, {
221
+ breedteMm,
222
+ hoogteMm,
223
+ margeMm: Number(layout.marge_boven ?? 12),
224
+ titel: job.documentnaam ?? "Productiebon",
225
+ });
226
+ }
227
+ // 3) Vrije layouts met losse elementen (verzendlabel, etiket, vrij, ...)
228
+ const { data: elementen } = await supabaseAdmin
229
+ .from("document_layout_elements")
230
+ .select("*")
231
+ .eq("layout_id", layout.id)
232
+ .order("z_index", { ascending: true });
233
+ const model = arr(elementen).map((e) => ({
234
+ id: String(e.id),
235
+ type: String(e.type ?? "tekst"),
236
+ x: Number(e.x ?? 0),
237
+ y: Number(e.y ?? 0),
238
+ breedte: Number(e.breedte ?? 40),
239
+ hoogte: Number(e.hoogte ?? 10),
240
+ rotatie: Number(e.rotatie ?? 0),
241
+ zIndex: Number(e.z_index ?? 0),
242
+ content: e.content ?? "",
243
+ stijl: e.stijl ?? {},
244
+ config: e.config ?? {},
245
+ condities: e.condities ?? [],
246
+ verborgen: e.verborgen === true,
247
+ }));
248
+ if (model.length) {
249
+ return bouwDocumentHtml(layout, model, plattePayload(p));
250
+ }
251
+ // 4) Alle overige documentsoorten: renderen op basis van het profiel.
252
+ // Zo werkt elke bestaande én toekomstige layout meteen, ook zonder eigen
253
+ // renderer (sticker, verzendlabel, factuur, etiket, magazijnlijst, ...).
254
+ if (!profielHeeftInhoud(String(layout.documenttype), cfg)) {
255
+ throw new GeenDocumentFout(`De layout "${String(layout.naam ?? "")}" heeft geen onderdelen aanstaan om af te drukken. ` +
256
+ "Zet in de layoutinstellingen minstens één onderdeel aan.");
257
+ }
258
+ const logos = await logosVoor(layout);
259
+ return renderProfielDocument(String(layout.documenttype), cfg, p.data ?? p, {
260
+ breedteMm,
261
+ hoogteMm,
262
+ margeMm: Number(layout.marge_boven ?? 8),
263
+ titel: job.documentnaam ?? String(layout.naam ?? "Document"),
264
+ logoUrl: logos[0]?.viewUrl ?? null,
265
+ });
266
+ }
267
+ /**
268
+ * Controleert of het document echt iets afdrukt. Zonder deze controle kan een
269
+ * lege layout of een opdracht zonder gegevens een blanco vel opleveren.
270
+ */
271
+ export function heeftZichtbareInhoud(html) {
272
+ const body = /<body[^>]*>([\s\S]*?)<\/body>/i.exec(html)?.[1] ?? html;
273
+ const zonderStijl = body
274
+ .replace(/<style[\s\S]*?<\/style>/gi, "")
275
+ .replace(/<script[\s\S]*?<\/script>/gi, "");
276
+ if (/<img\b|class="[^"]*\b(qr|barcode|balk)\b/i.test(zonderStijl))
277
+ return true;
278
+ const tekst = zonderStijl
279
+ .replace(/<[^>]+>/g, " ")
280
+ .replace(/&nbsp;/g, " ")
281
+ .replace(/[\s—–\-|]+/g, " ")
282
+ .trim();
283
+ return tekst.length >= 3;
284
+ }
285
+ /** Maakt van de opdrachtgegevens een platte veldenlijst ({{order.nummer}} enz.). */
286
+ function plattePayload(payload) {
287
+ const uit = {};
288
+ const loop = (waarde, pad, diepte) => {
289
+ if (diepte > 4 || waarde === null || waarde === undefined)
290
+ return;
291
+ if (typeof waarde === "object") {
292
+ if (Array.isArray(waarde))
293
+ return;
294
+ for (const [k, v] of Object.entries(waarde)) {
295
+ loop(v, pad ? `${pad}.${k}` : k, diepte + 1);
296
+ }
297
+ return;
298
+ }
299
+ if (pad)
300
+ uit[pad] = String(waarde);
301
+ };
302
+ loop(payload, "", 0);
303
+ return uit;
304
+ }
305
+ async function mislukt(job, reden, terugNaarWachtend) {
306
+ const pogingen = Number(job.pogingen ?? 0);
307
+ const opnieuw = terugNaarWachtend && pogingen < MAX_POGINGEN;
308
+ // oplopende wachttijd: 1, 4, 9 minuten
309
+ const wachtMin = Math.min(30, Math.max(1, pogingen * pogingen));
310
+ await supabaseAdmin
311
+ .from("print_jobs")
312
+ .update({
313
+ status: opnieuw ? "wachtend" : "mislukt",
314
+ foutmelding: reden.slice(0, 500),
315
+ volgende_poging: opnieuw ? new Date(Date.now() + wachtMin * 60000).toISOString() : null,
316
+ voltooid_op: opnieuw ? null : new Date().toISOString(),
317
+ updated_at: new Date().toISOString(),
318
+ })
319
+ .eq("id", job.id);
320
+ await log(job.id, opnieuw ? "opnieuw_proberen" : "mislukt", reden);
321
+ }
322
+ async function verwerkJob(job) {
323
+ try {
324
+ // Dubbel printen voorkomen: deze opdracht staat al bij de printer.
325
+ if (job.extern_job_id) {
326
+ await log(job.id, "overgeslagen", "Stond al bij de printer.");
327
+ return "verstuurd";
328
+ }
329
+ // 1) Kant-en-klare PDF (klantsticker, Floriday-document) gaat ongewijzigd door.
330
+ const klaar = await kantEnKlarePdf(job);
331
+ const keuze = klaar ? { layout: null, expliciet: false } : await zoekLayout(job);
332
+ const layout = keuze.layout;
333
+ const printer = await zoekPrinter(job, layout);
334
+ if (!printer?.extern_printer_id) {
335
+ await mislukt(job, "Er is geen printer gekoppeld voor dit document.", true);
336
+ return "mislukt";
337
+ }
338
+ let pdfBase64 = klaar;
339
+ if (!pdfBase64) {
340
+ const html = await bouwHtml(job, keuze);
341
+ if (!heeftZichtbareInhoud(html)) {
342
+ throw new GeenDocumentFout("Het document was leeg: er zijn geen gegevens gevonden om af te drukken. " +
343
+ "Controleer of de layout blokken bevat en of de order gegevens heeft.");
344
+ }
345
+ const liggend = String(layout?.orientatie ?? "portrait") === "landscape";
346
+ pdfBase64 = await htmlNaarPdfServer({
347
+ html,
348
+ breedteMm: Number(layout?.breedte_mm ?? 210),
349
+ hoogteMm: Number(layout?.hoogte_mm ?? 297),
350
+ margeMm: Number(layout?.marge_boven ?? 10),
351
+ landschap: liggend,
352
+ });
353
+ }
354
+ const externJobId = await stuurPdfNaarPrinter({
355
+ externPrinterId: String(printer.extern_printer_id),
356
+ documentnaam: String(job.documentnaam ?? "Document"),
357
+ pdfBase64,
358
+ kopieen: Number(job.kopieen ?? 1),
359
+ });
360
+ await supabaseAdmin
361
+ .from("print_jobs")
362
+ .update({
363
+ printer_id: printer.id,
364
+ layout_id: job.layout_id ?? layout?.id ?? null,
365
+ extern_job_id: externJobId,
366
+ foutmelding: null,
367
+ volgende_poging: null,
368
+ updated_at: new Date().toISOString(),
369
+ })
370
+ .eq("id", job.id);
371
+ await log(job.id, "verstuurd", `Naar printer ${printer.naam ?? ""}`);
372
+ return "verstuurd";
373
+ }
374
+ catch (e) {
375
+ // Ontbreekt er een document of layout? Dan heeft opnieuw proberen geen zin.
376
+ const blijvend = e instanceof GeenDocumentFout;
377
+ const tijdelijk = !blijvend && (e instanceof PdfRenderFout || e?.name === "PrintToegangFout");
378
+ await mislukt(job, e instanceof Error ? e.message : "Afdrukken mislukt", tijdelijk);
379
+ return "mislukt";
380
+ }
381
+ }
382
+ /** Werkt de status bij van opdrachten die al naar een printer zijn gestuurd. */
383
+ async function synchroniseerStatussen() {
384
+ const { data } = await supabaseAdmin
385
+ .from("print_jobs")
386
+ .select("id, extern_job_id")
387
+ .eq("status", "bezig")
388
+ .not("extern_job_id", "is", null)
389
+ .limit(25);
390
+ let bijgewerkt = 0;
391
+ for (const job of arr(data)) {
392
+ try {
393
+ const status = await opdrachtStatus(String(job.extern_job_id));
394
+ if (status === "voltooid" || status === "mislukt") {
395
+ await supabaseAdmin
396
+ .from("print_jobs")
397
+ .update({
398
+ status,
399
+ voltooid_op: new Date().toISOString(),
400
+ foutmelding: status === "mislukt" ? "De printer meldde een fout." : null,
401
+ updated_at: new Date().toISOString(),
402
+ })
403
+ .eq("id", job.id);
404
+ await log(job.id, status);
405
+ bijgewerkt++;
406
+ }
407
+ }
408
+ catch {
409
+ // status opnieuw proberen bij de volgende ronde
410
+ }
411
+ }
412
+ return bijgewerkt;
413
+ }
414
+ /** Eén ronde: wachtende opdrachten afdrukken en statussen bijwerken. */
415
+ export async function verwerkWachtrij(max = 5) {
416
+ const { data, error } = await supabaseAdmin.rpc("claim_cloud_print_jobs", { _max: max });
417
+ if (error)
418
+ throw new Error(error.message);
419
+ const jobs = arr(data);
420
+ let verstuurd = 0;
421
+ let mislukteJobs = 0;
422
+ for (const job of jobs) {
423
+ const uitkomst = await verwerkJob(job);
424
+ if (uitkomst === "verstuurd")
425
+ verstuurd++;
426
+ else
427
+ mislukteJobs++;
428
+ }
429
+ return {
430
+ opgepakt: jobs.length,
431
+ verstuurd,
432
+ mislukt: mislukteJobs,
433
+ bijgewerkt: await synchroniseerStatussen(),
434
+ };
435
+ }