@flowselections/layout-maker 1.29.0 → 1.32.0

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.
@@ -258,7 +258,9 @@ function stijlVan(n, ouder) {
258
258
  const cl = klassen(n);
259
259
  if (n.tag === "th" || cl.includes("vet") || n.tag === "b" || n.tag === "strong")
260
260
  s.vet = true;
261
- if (cl.includes("sectitel")) {
261
+ // De stylesheet is leidend voor .sectitel; alleen zonder opmaakregels valt de
262
+ // motor terug op de bekende grijze kop.
263
+ if (cl.includes("sectitel") && !Object.keys(eig(n)).length) {
262
264
  s.grootte = ouder.grootte * 0.9;
263
265
  s.vet = true;
264
266
  s.kleur = GRIJS;
@@ -335,6 +337,8 @@ class Tekenaar {
335
337
  this.paginas = [];
336
338
  this.voetHoogte = 0;
337
339
  this.voetTekenaar = null;
340
+ /** Zolang dit aanstaat komt er geen nieuwe pagina bij (voet tekenen). */
341
+ this.vast = false;
338
342
  this.pagina = pdf.addPage([breedte, hoogte]);
339
343
  this.paginas.push(this.pagina);
340
344
  this.y = hoogte - marge;
@@ -356,6 +360,8 @@ class Tekenaar {
356
360
  this.voetTekenaar = teken;
357
361
  }
358
362
  nieuwePagina() {
363
+ if (this.vast)
364
+ return;
359
365
  this.pagina = this.pdf.addPage([this.breedte, this.hoogte]);
360
366
  this.paginas.push(this.pagina);
361
367
  this.y = this.hoogte - this.marge;
@@ -392,6 +398,26 @@ class Tekenaar {
392
398
  this.voetHoogte = bewaard.voet;
393
399
  return hoogte;
394
400
  }
401
+ /** Tekent iets op een bestaande pagina, bijvoorbeeld de vaste voet. */
402
+ async opPagina(p, y, teken) {
403
+ const bewaard = { pagina: this.pagina, y: this.y, paginas: this.paginas, voet: this.voetHoogte };
404
+ this.pagina = p;
405
+ this.y = y;
406
+ this.paginas = [p];
407
+ this.voetHoogte = 0;
408
+ this.vast = true;
409
+ try {
410
+ await teken();
411
+ }
412
+ catch {
413
+ // een voet mag het document nooit blokkeren
414
+ }
415
+ this.pagina = bewaard.pagina;
416
+ this.y = bewaard.y;
417
+ this.paginas = bewaard.paginas;
418
+ this.voetHoogte = bewaard.voet;
419
+ this.vast = false;
420
+ }
395
421
  rondAf() {
396
422
  if (!this.voetTekenaar)
397
423
  return;
@@ -735,6 +761,13 @@ async function tekenTabel(ctx, tabel, vak, s) {
735
761
  const padX = compact ? 0.8 * MM : 1 * MM;
736
762
  const randen = heeft(tabel, "rand");
737
763
  const kopRij = rijen.find((r) => r.kinderen.some((c) => c.tag === "th")) ?? null;
764
+ /** Celrand onderaan: uit de stylesheet, met de klasse `onder` als terugval. */
765
+ const celRandOnder = (c) => randVan(eig(c)["border-bottom"]) ??
766
+ randVan(eig(c)["border"]) ??
767
+ (klassen(c).includes("onder") || klassen(c).includes("rand") ? { dikte: 0.5, kleur: LIJN } : null);
768
+ // Zodra de cellen zelf randen dragen, is de automatische lijn onder de koprij
769
+ // dubbelop en landt hij op de verkeerde regel.
770
+ const eigenCelRanden = rijen.some((r) => r.kinderen.some((c) => (c.tag === "td" || c.tag === "th") && !!celRandOnder(c)));
738
771
  const tekenRij = (rij, herhaald = false) => {
739
772
  const cellen = rij.kinderen.filter((c) => c.tag === "td" || c.tag === "th");
740
773
  const cellenStijl = cellen.map((c) => stijlVan(c, s));
@@ -775,14 +808,28 @@ async function tekenTabel(ctx, tabel, vak, s) {
775
808
  borderColor: LIJN,
776
809
  });
777
810
  }
811
+ else {
812
+ const cr = celRandOnder(c);
813
+ if (cr) {
814
+ tk.pagina.drawLine({
815
+ start: { x, y: bovenY - hoogte },
816
+ end: { x: x + b, y: bovenY - hoogte },
817
+ thickness: cr.dikte,
818
+ color: cr.kleur,
819
+ });
820
+ }
821
+ }
778
822
  x += b;
779
823
  });
780
- if (!randen && (rij === kopRij || heeft(rij, "onder"))) {
824
+ const rijRand = randVan(eig(rij)["border-bottom"]) ?? (heeft(rij, "onder") ? { dikte: 0.5, kleur: LIJN } : null);
825
+ const automatisch = !eigenCelRanden && rij === kopRij ? { dikte: 0.5, kleur: LIJN } : null;
826
+ const lijn = !randen ? (rijRand ?? automatisch) : null;
827
+ if (lijn) {
781
828
  tk.pagina.drawLine({
782
829
  start: { x: vak.x, y: bovenY - hoogte },
783
830
  end: { x: vak.x + vak.breedte, y: bovenY - hoogte },
784
- thickness: 0.5,
785
- color: LIJN,
831
+ thickness: lijn.dikte,
832
+ color: lijn.kleur,
786
833
  });
787
834
  }
788
835
  tk.y = bovenY - hoogte;
@@ -1130,32 +1177,29 @@ export async function htmlNaarPdfBytes(opties) {
1130
1177
  const tk = new Tekenaar(pdf, breedte, hoogte, marge, normaal, vet);
1131
1178
  const ctx = { tk, afbeeldingen, beeldFouten };
1132
1179
  const stijl = { grootte: basis, vet: false, kleur: DONKER, uitlijning: "left" };
1133
- // vaste paginavoet (bijv. totalen op elke pagina)
1180
+ // vaste paginavoet (bijv. totalen op elke pagina) — als volwaardig blok
1181
+ const voetVak = { x: marge, breedte: breedte - marge * 2 };
1134
1182
  const voetKnoop = zoekVoet(wortel);
1135
- if (voetKnoop) {
1183
+ const voetOuder = voetKnoop
1184
+ ? { tag: "div", attrs: {}, kinderen: [voetKnoop] }
1185
+ : null;
1186
+ let voetHoogte = 0;
1187
+ if (voetOuder) {
1136
1188
  verwijder(wortel, voetKnoop);
1137
- tk.zetVoet(regelHoogte(stijl) + 2 * MM, () => { });
1189
+ voetHoogte = await tk.meet(() => tekenKinderen(ctx, voetOuder, voetVak, stijl));
1190
+ tk.zetVoet(voetHoogte + 2 * MM, () => { });
1138
1191
  }
1139
1192
  await tekenKinderen(ctx, wortel, { x: marge, breedte: breedte - marge * 2 }, stijl);
1140
- if (voetKnoop) {
1141
- const tekst = plattTekst(voetKnoop).trim();
1142
- for (const p of tk.paginas) {
1143
- p.drawLine({
1144
- start: { x: marge, y: marge + regelHoogte(stijl) + 1.5 * MM },
1145
- end: { x: breedte - marge, y: marge + regelHoogte(stijl) + 1.5 * MM },
1146
- thickness: 0.6,
1147
- color: LIJN,
1148
- });
1149
- p.drawText(veiligeTekst(tekst).slice(0, 300), {
1150
- x: marge,
1151
- y: marge + 2,
1152
- size: stijl.grootte,
1153
- font: normaal,
1154
- color: DONKER,
1155
- });
1193
+ if (voetOuder) {
1194
+ for (const p of [...tk.paginas]) {
1195
+ await tk.opPagina(p, marge + voetHoogte + 1 * MM, () => tekenKinderen(ctx, voetOuder, voetVak, stijl));
1156
1196
  }
1157
1197
  }
1158
- if (opties.paginanummers !== false && tk.paginas.length > 1) {
1198
+ // De layout bepaalt of paginanummers meegaan; zonder voorkeur alleen bij
1199
+ // meerdere pagina's.
1200
+ const uitMeta = /name=["']pdf-paginanummers["']\s+content=["']ja["']/i.test(opties.html);
1201
+ const nummers = opties.paginanummers ?? (uitMeta ? true : tk.paginas.length > 1);
1202
+ if (nummers) {
1159
1203
  tk.paginas.forEach((p, i) => {
1160
1204
  const t = `Pagina ${i + 1} van ${tk.paginas.length}`;
1161
1205
  p.drawText(t, {
@@ -1 +1 @@
1
- {"version":3,"file":"pakbonHtml.d.ts","sourceRoot":"","sources":["../../src/lib/pakbonHtml.ts"],"names":[],"mappings":"AAUA,OAAO,EAmBL,KAAK,SAAS,EAEd,KAAK,YAAY,EAGlB,MAAM,iBAAiB,CAAC;AAGzB,8EAA8E;AAC9E,MAAM,MAAM,WAAW,GAAG,SAAS,CAAC;AAEpC,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0BAA0B;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oEAAoE;IACpE,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAWD,wBAAgB,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAKtC;AAQD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,CAgE3D;AA6oBD,oEAAoE;AACpE,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,YAAY,GAAG,OAAO,EAC9B,KAAK,EAAE,WAAW,EAClB,MAAM,GAAE,kBAAuB,GAC9B,MAAM,CA8BR;AAgBD,gEAAgE;AAChE,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,YAAY,GAAG,OAAO,EAC9B,KAAK,EAAE,WAAW,EAClB,MAAM,GAAE,kBAAuB,GAC9B,MAAM,CA2DR;AAED,iEAAiE;AACjE,wBAAgB,oBAAoB,IAAI,WAAW,CAElD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,YAAY,GAAG,OAAO,EAC9B,KAAK,EAAE,WAAW,EAClB,MAAM,GAAE,kBAAuB,GAC9B,IAAI,CAsCN;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,YAAY,GAAG,OAAO,EAC9B,KAAK,EAAE,WAAW,EAClB,MAAM,GAAE,kBAAuB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAUf"}
1
+ {"version":3,"file":"pakbonHtml.d.ts","sourceRoot":"","sources":["../../src/lib/pakbonHtml.ts"],"names":[],"mappings":"AAUA,OAAO,EAmBL,KAAK,SAAS,EAEd,KAAK,YAAY,EAGlB,MAAM,iBAAiB,CAAC;AAGzB,8EAA8E;AAC9E,MAAM,MAAM,WAAW,GAAG,SAAS,CAAC;AAEpC,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0BAA0B;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oEAAoE;IACpE,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC;IACrB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAWD,wBAAgB,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAKtC;AAQD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,CAgE3D;AA8oBD,oEAAoE;AACpE,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,YAAY,GAAG,OAAO,EAC9B,KAAK,EAAE,WAAW,EAClB,MAAM,GAAE,kBAAuB,GAC9B,MAAM,CA8BR;AAgBD,gEAAgE;AAChE,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,YAAY,GAAG,OAAO,EAC9B,KAAK,EAAE,WAAW,EAClB,MAAM,GAAE,kBAAuB,GAC9B,MAAM,CAmER;AAED,iEAAiE;AACjE,wBAAgB,oBAAoB,IAAI,WAAW,CAElD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,YAAY,GAAG,OAAO,EAC9B,KAAK,EAAE,WAAW,EAClB,MAAM,GAAE,kBAAuB,GAC9B,IAAI,CAsCN;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,YAAY,GAAG,OAAO,EAC9B,KAAK,EAAE,WAAW,EAClB,MAAM,GAAE,kBAAuB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAUf"}
@@ -421,13 +421,13 @@ function fustVerzamellijstHtml(order, s) {
421
421
  const totaal = groepen.reduce((n, f) => n + f.aantal, 0);
422
422
  const blokken = groepen
423
423
  .map((f) => {
424
- const kop = `<div class="sectitel" style="display:flex;justify-content:space-between"><span>${esc([f.fusttype, toonHoogte ? f.hoogte : "", toonHoogte ? f.belading : ""]
424
+ const kop = `<div class="fustgroepkop"><span>${esc([f.fusttype, toonHoogte ? f.hoogte : "", toonHoogte ? f.belading : ""]
425
425
  .filter(Boolean)
426
426
  .join(" · "))}</span><span>${f.aantal}× fust</span></div>`;
427
427
  if (!kolommen.length)
428
428
  return kop;
429
429
  const koppen = kolommen
430
- .map((k) => `<th class="onder"${FUST_RECHTS.has(k) ? ' style="text-align:right"' : ""}>${esc(FUST_KOLOM_LABELS[k])}</th>`)
430
+ .map((k) => `<th${FUST_RECHTS.has(k) ? ' style="text-align:right"' : ""}>${esc(FUST_KOLOM_LABELS[k])}</th>`)
431
431
  .join("");
432
432
  const rijen = f.producten
433
433
  .map((pr) => `<tr>${kolommen
@@ -643,8 +643,14 @@ export function renderPakbonDocument(config, order, opties = {}) {
643
643
  const font = opties.lettertype ?? 'Arial, Helvetica, "Segoe UI", sans-serif';
644
644
  const grootte = opties.lettergrootte ?? 8.5;
645
645
  const body = renderPakbonBody(config, order, opties);
646
+ // De layout bepaalt of er paginanummers op de afdruk komen; de PDF-motor
647
+ // leest dit uit de meta, zodat de wachtrij niets extra hoeft door te geven.
648
+ const totalen = arr(leesPakbonConfig(config).secties).find((s) => s.type === "totalen");
649
+ const paginanummers = !!totalen && opt(totalen, "toon_paginanummer", false) === true;
646
650
  return `<!doctype html><html lang="nl"><head><meta charset="utf-8" />
651
+ <meta name="pdf-paginanummers" content="${paginanummers ? "ja" : "nee"}" />
647
652
  <title>${esc(opties.titel ?? "Pakbon")}</title>
653
+
648
654
  <style>
649
655
  @page { size: ${b}mm ${h}mm; margin: ${marge}mm; }
650
656
  * { box-sizing: border-box; }
@@ -652,6 +658,8 @@ html, body { margin: 0; padding: 0; }
652
658
  body { font-family: ${font}; font-size: ${grootte}pt; color: #111827; line-height: 1.35; }
653
659
  .blok { margin-bottom: 2mm; }
654
660
  .sectitel { font-size: 90%; font-weight: 600; text-transform: uppercase; color: #6b7280; margin-bottom: 0.6mm; }
661
+ .fustgroepkop { display: flex; justify-content: space-between; font-weight: 600; color: #111827; border-bottom: 0.45mm solid #111827; padding-bottom: 0.5mm; margin-bottom: 0.6mm; }
662
+
655
663
  .vet { font-weight: 600; }
656
664
  .kv { display: flex; justify-content: space-between; gap: 3mm; }
657
665
  .kv .k { color: #6b7280; }
@@ -680,7 +688,7 @@ tr.zebra td { background: #f3f4f6; }
680
688
  .laagkop.compact { border-bottom: none; }
681
689
  .meta { display: flex; gap: 3mm; font-size: 88%; font-weight: 400; color: #4b5563; }
682
690
  .totalen { display: flex; flex-wrap: wrap; gap: 5mm; border-top: 0.25mm solid #d1d5db; padding-top: 1.2mm; font-weight: 500; }
683
- .paginavoet { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; }
691
+ .paginavoet { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; border-top: 0.35mm solid #9ca3af; padding-top: 1mm; }
684
692
  .handtekening { margin-top: 3mm; }
685
693
  .lijnveld { margin-top: 4mm; border-bottom: 0.25mm solid #6b7280; padding-bottom: 3mm; }
686
694
  .lijnveld.hoog { padding-bottom: 10mm; }
@@ -0,0 +1,10 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ type Db = SupabaseClient<any, any, any>;
3
+ /**
4
+ * POST { station_slug, queue_item_id } → PDF-bytes.
5
+ * Fouten komen terug als JSON met een code, en worden vastgelegd in
6
+ * `print_preview_events`.
7
+ */
8
+ export declare function behandelPreviewPdf(request: Request, supabase: Db): Promise<Response>;
9
+ export {};
10
+ //# sourceMappingURL=previewPdf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"previewPdf.d.ts","sourceRoot":"","sources":["../../src/lib/previewPdf.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,KAAK,EAAE,GAAG,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAkExC;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAoK1F"}
@@ -0,0 +1,170 @@
1
+ // ============================================================================
2
+ // src/lib/previewPdf.ts — afhandeling van /api/public/print/preview-pdf.
3
+ // Toont hoe een pickregel eruit komt als PDF, zonder printopdracht en zonder
4
+ // iets in de database te wijzigen. De shell levert de databaseverbinding.
5
+ //
6
+ // Eén bron van waarheid: de gegevens komen uit de read-only databasefunctie
7
+ // `pick_preview_job` van de Logistiek Engine — dezelfde opbouw als
8
+ // `pick_enqueue_print_job`. Deze module spiegelt die mapping bewust niet.
9
+ // ============================================================================
10
+ /** Maximaal aantal voorbeelden per station binnen het venster. */
11
+ const MAX_PER_VENSTER = 6;
12
+ const VENSTER_SEC = 60;
13
+ function gelijk(a, b) {
14
+ if (a.length !== b.length)
15
+ return false;
16
+ let verschil = 0;
17
+ for (let i = 0; i < a.length; i++)
18
+ verschil |= a.charCodeAt(i) ^ b.charCodeAt(i);
19
+ return verschil === 0;
20
+ }
21
+ function leesSleutel(request) {
22
+ return (request.headers.get("apikey") ??
23
+ (request.headers.get("authorization") ?? "").replace(/^Bearer\s+/i, "")).trim();
24
+ }
25
+ /** Mag deze aanroeper voorbeelden opvragen? Zelfde regels als de queue-tick. */
26
+ async function magAanroepen(request, db) {
27
+ const meegegeven = leesSleutel(request);
28
+ if (!meegegeven)
29
+ return false;
30
+ const { isRunnerSleutel } = await import("./runnerRegistry.server.js");
31
+ if (await isRunnerSleutel(db, meegegeven))
32
+ return true;
33
+ const projectSleutel = process.env["SUPABASE_PUBLISHABLE_KEY"] ??
34
+ process.env["VITE_SUPABASE_PUBLISHABLE_KEY"] ??
35
+ "";
36
+ if (projectSleutel && gelijk(projectSleutel, meegegeven))
37
+ return true;
38
+ try {
39
+ const { data } = await db.auth.getUser(meegegeven);
40
+ return !!data?.user?.id;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
47
+ /** Legt een weigering vindbaar vast; faalt nooit hard. */
48
+ async function weiger(db, station, queueItem, code, reden, status) {
49
+ try {
50
+ await db.rpc("print_preview_weiger", {
51
+ _station_slug: station,
52
+ _queue_item_id: queueItem && UUID.test(queueItem) ? queueItem : null,
53
+ _code: code,
54
+ _reden: reden,
55
+ });
56
+ }
57
+ catch {
58
+ /* logboek mag een voorbeeld nooit blokkeren */
59
+ }
60
+ return Response.json({ ok: false, code, fout: reden }, { status });
61
+ }
62
+ /**
63
+ * POST { station_slug, queue_item_id } → PDF-bytes.
64
+ * Fouten komen terug als JSON met een code, en worden vastgelegd in
65
+ * `print_preview_events`.
66
+ */
67
+ export async function behandelPreviewPdf(request, supabase) {
68
+ if (!supabase) {
69
+ return Response.json({ ok: false, code: "geen_database", fout: "De server kreeg geen databaseverbinding." }, { status: 500 });
70
+ }
71
+ if (!(await magAanroepen(request, supabase))) {
72
+ return Response.json({ ok: false, code: "geen_toegang", fout: "Het voorbeeld weigerde het verzoek: geen geldig toegangsbewijs." }, { status: 401 });
73
+ }
74
+ let body = {};
75
+ try {
76
+ body = (await request.json());
77
+ }
78
+ catch {
79
+ body = {};
80
+ }
81
+ const station = typeof body["station_slug"] === "string" ? body["station_slug"].trim() : "";
82
+ const queueItem = typeof body["queue_item_id"] === "string" ? body["queue_item_id"].trim() : "";
83
+ if (!station || !queueItem || !UUID.test(queueItem)) {
84
+ return weiger(supabase, station || null, null, "ongeldig_verzoek", "Geef een station_slug en een geldig queue_item_id mee.", 400);
85
+ }
86
+ // 1) Snelheidsbegrenzing (telt en logt in één databasestap).
87
+ const { data: tempo, error: tempoFout } = await supabase.rpc("print_preview_toestaan", {
88
+ _station_slug: station,
89
+ _queue_item_id: queueItem,
90
+ _max: MAX_PER_VENSTER,
91
+ _venster_sec: VENSTER_SEC,
92
+ });
93
+ if (tempoFout) {
94
+ return weiger(supabase, station, queueItem, "render_mislukt", `Voorbeeld niet mogelijk: ${tempoFout.message}`, 500);
95
+ }
96
+ const toestemming = (tempo ?? {});
97
+ if (toestemming["toegestaan"] === false) {
98
+ const wacht = Number(toestemming["wacht_sec"] ?? VENSTER_SEC);
99
+ return Response.json({
100
+ ok: false,
101
+ code: "te_snel",
102
+ fout: `Te veel voorbeelden achter elkaar. Probeer het over ${wacht} seconden opnieuw.`,
103
+ wacht_sec: wacht,
104
+ }, { status: 429, headers: { "Retry-After": String(wacht) } });
105
+ }
106
+ // 2) Gegevens ophalen: read-only, met stationvalidatie in de database zelf.
107
+ const { data: rij, error: rpcFout } = await supabase.rpc("pick_preview_job", {
108
+ _station_slug: station,
109
+ _queue_item_id: queueItem,
110
+ });
111
+ if (rpcFout) {
112
+ const melding = String(rpcFout.message ?? "");
113
+ // Ontbreekt de functie zelf (Engine nog niet bijgewerkt)? Dat is geen
114
+ // stationprobleem maar een installatieprobleem.
115
+ if (/could not find the function|does not exist/i.test(melding)) {
116
+ return weiger(supabase, station, queueItem, "rpc_ontbreekt", "De databasefunctie pick_preview_job ontbreekt. Werk de Logistiek Engine bij.", 503);
117
+ }
118
+ if (/is not unique/i.test(melding)) {
119
+ return weiger(supabase, station, queueItem, "rpc_dubbel", "Er bestaan meerdere varianten van pick_preview_job in de database. Verwijder de oude variant.", 503);
120
+ }
121
+ const code = /station/i.test(melding)
122
+ ? "station_mismatch"
123
+ : /bestaat niet|niet gevonden|not found|onbekend/i.test(melding)
124
+ ? "regel_onbekend"
125
+ : "render_mislukt";
126
+ return weiger(supabase, station, queueItem, code, melding || "Voorbeeld niet mogelijk.", code === "render_mislukt" ? 500 : 404);
127
+ }
128
+ const job = (Array.isArray(rij) ? rij[0] : rij);
129
+ if (!job) {
130
+ return weiger(supabase, station, queueItem, "regel_onbekend", "Deze pickregel bestaat niet of hoort niet bij dit station.", 404);
131
+ }
132
+ // 3) Exact dezelfde documentkeuze en opmaak als de printwachtrij.
133
+ const { bepaalDocumentKeuze, bouwHtml, heeftZichtbareInhoud, GeenDocumentFout } = await import("./printQueue.server.js");
134
+ const { htmlNaarPdfServer } = await import("./htmlNaarPdf.server.js");
135
+ try {
136
+ const { keuze, klaarPdf } = await bepaalDocumentKeuze(supabase, job);
137
+ const layout = keuze.layout;
138
+ const uitkomst = { bron: "extern_pdf" };
139
+ let pdfBase64 = klaarPdf;
140
+ if (!pdfBase64) {
141
+ const html = await bouwHtml(supabase, job, keuze, uitkomst);
142
+ if (!heeftZichtbareInhoud(html)) {
143
+ return weiger(supabase, station, queueItem, "layout_leeg", "Het document blijft leeg: de layout heeft geen blokken of de order heeft geen gegevens.", 422);
144
+ }
145
+ pdfBase64 = await htmlNaarPdfServer({
146
+ html,
147
+ breedteMm: Number(layout?.breedte_mm ?? 210),
148
+ hoogteMm: Number(layout?.hoogte_mm ?? 297),
149
+ margeMm: Number(layout?.marge_boven ?? 10),
150
+ landschap: String(layout?.orientatie ?? "portrait") === "landscape",
151
+ });
152
+ }
153
+ const bytes = Uint8Array.from(atob(pdfBase64), (c) => c.charCodeAt(0));
154
+ return new Response(bytes, {
155
+ status: 200,
156
+ headers: {
157
+ "Content-Type": "application/pdf",
158
+ "Content-Disposition": 'inline; filename="voorbeeld.pdf"',
159
+ "Cache-Control": "no-store",
160
+ "X-Preview-Bron": String(uitkomst.bron),
161
+ "X-Preview-Layout-Id": String(layout?.id ?? ""),
162
+ },
163
+ });
164
+ }
165
+ catch (e) {
166
+ const melding = e instanceof Error ? e.message : "Voorbeeld maken mislukte.";
167
+ const blijvend = e?.name === "GeenDocumentFout" || e instanceof GeenDocumentFout;
168
+ return weiger(supabase, station, queueItem, blijvend ? "layout_ontbreekt" : "render_mislukt", melding, blijvend ? 422 : 500);
169
+ }
170
+ }
@@ -8,13 +8,17 @@ export type PrintDb = SupabaseClient<any, any, any>;
8
8
  * meer nu de shell de client levert.
9
9
  */
10
10
  export declare function controleerClient(db: PrintDb): Promise<string | null>;
11
+ /** Fout waarbij opnieuw proberen zinloos is (er valt niets te printen). */
12
+ export declare class GeenDocumentFout extends Error {
13
+ name: string;
14
+ }
11
15
  /**
12
16
  * `ontwerpWint` = deze layout is een echt ontwerp van deze module (actief, geen
13
17
  * Floriday-systeemlayout) en gaat daarom vóór meegestuurde HTML. Er is bewust
14
18
  * maar één schakelaar voor dit besluit: bron/systeem. Een tweede schakelaar
15
19
  * ("stond er een layout_id op de opdracht?") is verwijderd.
16
20
  */
17
- type LayoutKeuze = {
21
+ export type LayoutKeuze = {
18
22
  layout: Job | null;
19
23
  ontwerpWint: boolean;
20
24
  };
@@ -50,5 +54,14 @@ export interface WachtrijResultaat {
50
54
  }
51
55
  /** Eén ronde: vastgelopen opdrachten opruimen, afdrukken en statussen bijwerken. */
52
56
  export declare function verwerkWachtrij(db: PrintDb, max?: number): Promise<WachtrijResultaat>;
57
+ /**
58
+ * Dezelfde documentkeuze als de printwachtrij maakt, maar zonder printer en
59
+ * zonder statuswijziging. Gebruikt door het voorbeeld (`preview-pdf`) zodat
60
+ * het voorbeeld exact toont wat er straks uit de printer komt.
61
+ */
62
+ export declare function bepaalDocumentKeuze(db: PrintDb, job: Job): Promise<{
63
+ keuze: LayoutKeuze;
64
+ klaarPdf: string | null;
65
+ }>;
53
66
  export {};
54
67
  //# sourceMappingURL=printQueue.server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"printQueue.server.d.ts","sourceRoot":"","sources":["../../src/lib/printQueue.server.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAsB5D,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE/B,mFAAmF;AACnF,MAAM,MAAM,OAAO,GAAG,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAEpD;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAW1E;AAeD;;;;;GAKG;AACH,KAAK,WAAW,GAAG;IAAE,MAAM,EAAE,GAAG,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAAC;AAEhE,yEAAyE;AACzE,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG,cAAc,GAAG,aAAa,GAAG,aAAa,CAAC;AAE1F,6EAA6E;AAC7E,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC;AAcnD,4EAA4E;AAC5E,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAI7D;AAkHD;;;GAGG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA+BnF;AA+PD;;;;GAIG;AACH,wBAAsB,QAAQ,CAC5B,EAAE,EAAE,OAAO,EACX,GAAG,EAAE,GAAG,EACR,KAAK,EAAE,WAAW,EAClB,QAAQ,CAAC,EAAE,YAAY,GACtB,OAAO,CAAC,MAAM,CAAC,CA8IjB;AAGD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAY1D;AAmRD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,oFAAoF;AACpF,wBAAsB,eAAe,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,SAAI,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA4BtF"}
1
+ {"version":3,"file":"printQueue.server.d.ts","sourceRoot":"","sources":["../../src/lib/printQueue.server.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAsB5D,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE/B,mFAAmF;AACnF,MAAM,MAAM,OAAO,GAAG,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAEpD;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAW1E;AAUD,2EAA2E;AAC3E,qBAAa,gBAAiB,SAAQ,KAAK;IAChC,IAAI,SAAsB;CACpC;AAED;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG;IAAE,MAAM,EAAE,GAAG,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAAC;AAEvE,yEAAyE;AACzE,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG,cAAc,GAAG,aAAa,GAAG,aAAa,CAAC;AAE1F,6EAA6E;AAC7E,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC;AAcnD,4EAA4E;AAC5E,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAI7D;AAkHD;;;GAGG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA+BnF;AA+PD;;;;GAIG;AACH,wBAAsB,QAAQ,CAC5B,EAAE,EAAE,OAAO,EACX,GAAG,EAAE,GAAG,EACR,KAAK,EAAE,WAAW,EAClB,QAAQ,CAAC,EAAE,YAAY,GACtB,OAAO,CAAC,MAAM,CAAC,CA8IjB;AAGD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAY1D;AAmRD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,oFAAoF;AACpF,wBAAsB,eAAe,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,SAAI,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA4BtF;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,OAAO,EACX,GAAG,EAAE,GAAG,GACP,OAAO,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CA0B1D"}
@@ -41,7 +41,7 @@ async function log(db, jobId, gebeurtenis, detail) {
41
41
  });
42
42
  }
43
43
  /** Fout waarbij opnieuw proberen zinloos is (er valt niets te printen). */
44
- class GeenDocumentFout extends Error {
44
+ export class GeenDocumentFout extends Error {
45
45
  constructor() {
46
46
  super(...arguments);
47
47
  this.name = "GeenDocumentFout";
@@ -864,3 +864,28 @@ export async function verwerkWachtrij(db, max = 5) {
864
864
  bijgewerkt: await synchroniseerStatussen(db),
865
865
  };
866
866
  }
867
+ /**
868
+ * Dezelfde documentkeuze als de printwachtrij maakt, maar zonder printer en
869
+ * zonder statuswijziging. Gebruikt door het voorbeeld (`preview-pdf`) zodat
870
+ * het voorbeeld exact toont wat er straks uit de printer komt.
871
+ */
872
+ export async function bepaalDocumentKeuze(db, job) {
873
+ let bron = await zoekBronregel(db, job);
874
+ if (await isFloridayLayoutKeuze(db, job)) {
875
+ bron = "extern_pdf";
876
+ job.layout_id = null;
877
+ }
878
+ if (bron === "eigen_layout") {
879
+ job.pdf_base64 = null;
880
+ job.pdf_url = null;
881
+ }
882
+ const klaar = await kantEnKlarePdf(db, job);
883
+ if (!klaar && bron === "extern_pdf") {
884
+ throw new GeenDocumentFout("Voor dit document is ingesteld dat het aangeleverde document wordt " +
885
+ "gebruikt, maar er is geen PDF meegestuurd.");
886
+ }
887
+ const keuze = klaar
888
+ ? { layout: null, ontwerpWint: false }
889
+ : await zoekLayout(db, job);
890
+ return { keuze, klaarPdf: klaar };
891
+ }