@flowselections/layout-maker 1.26.0 → 1.29.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.
@@ -72,6 +72,27 @@ export function normaliseerDocumenttype(type) {
72
72
  * mislukt. Alleen zonder layout_id valt de wachtrij terug op de actieve
73
73
  * layout van het documenttype.
74
74
  */
75
+ /**
76
+ * Is dit een voorbeeldafdruk uit de ontwerper? Dan is de meegestuurde opmaak
77
+ * altijd leidend: anders drukt de ontwerper de opgeslagen versie af in plaats
78
+ * van de wijziging die je op het scherm ziet.
79
+ */
80
+ function isVoorbeeldAfdruk(job) {
81
+ const p = (job.payload ?? {});
82
+ if (p?.voorbeeld === true || p?.data?.voorbeeld === true)
83
+ return true;
84
+ return Boolean(p?.layoutId ?? p?.data?.layoutId ?? p?.layout_id);
85
+ }
86
+ /** Mag deze layout als ontwerp winnen van meegestuurde HTML? */
87
+ function magOntwerpWinnen(layout) {
88
+ if (!layout)
89
+ return false;
90
+ if (layout.actief === false)
91
+ return false;
92
+ if (layout.systeem === true)
93
+ return false;
94
+ return String(layout.bron ?? "") !== "floriday";
95
+ }
75
96
  async function zoekLayout(db, job) {
76
97
  const heeftEigenOpmaak = typeof job.render_html === "string" && job.render_html.trim().length > 40;
77
98
  // Een layout-id in het kenmerkenveld is alleen een verwijzing, geen
@@ -81,6 +102,7 @@ async function zoekLayout(db, job) {
81
102
  ? null
82
103
  : (job.payload?.layoutId ?? job.payload?.data?.layoutId ?? job.payload?.layout_id ?? null);
83
104
  const id = job.layout_id ?? uitPayload;
105
+ const voorbeeld = heeftEigenOpmaak && isVoorbeeldAfdruk(job);
84
106
  if (id) {
85
107
  const { data } = await db
86
108
  .from("document_layouts")
@@ -90,7 +112,8 @@ async function zoekLayout(db, job) {
90
112
  if (!data) {
91
113
  throw new GeenDocumentFout("De layout die aan deze opdracht hangt bestaat niet meer. Koppel een geldige layout.");
92
114
  }
93
- return { layout: data, expliciet: Boolean(job.layout_id) };
115
+ const layout = data;
116
+ return { layout, ontwerpWint: !voorbeeld && magOntwerpWinnen(layout) };
94
117
  }
95
118
  const { data } = await db
96
119
  .from("document_layouts")
@@ -105,7 +128,7 @@ async function zoekLayout(db, job) {
105
128
  throw new GeenDocumentFout(`Geen layout gevonden voor documenttype '${normaliseerDocumenttype(job.documenttype)}'. ` +
106
129
  "Maak een layout aan en zet die op actief.");
107
130
  }
108
- return { layout: gevonden, expliciet: false };
131
+ return { layout: gevonden, ontwerpWint: !voorbeeld && magOntwerpWinnen(gevonden) };
109
132
  }
110
133
  /**
111
134
  * De bronregel voor deze documentsoort en de herkomst van de order. Geeft
@@ -393,44 +416,72 @@ async function zoekPrinter(db, job, layout) {
393
416
  }
394
417
  /** Haalt de logo's op die in de layout gebruikt worden, met tijdelijke URL. */
395
418
  async function logosVoor(db, layout) {
396
- const logoId = layout?.config?.logo?.logoId ?? null;
397
- if (!logoId)
419
+ const config = layout?.config ?? null;
420
+ const ids = new Set();
421
+ const documentLogo = config?.logo?.logoId;
422
+ if (documentLogo)
423
+ ids.add(String(documentLogo));
424
+ // Ook logo's die als blok in de ontwerper staan meenemen.
425
+ for (const sectie of Array.isArray(config?.sections) ? config.sections : []) {
426
+ const id = sectie?.opties?.logoId ?? sectie?.logoId;
427
+ if (id)
428
+ ids.add(String(id));
429
+ }
430
+ for (const blok of Array.isArray(config?.blokken) ? config.blokken : []) {
431
+ const id = blok?.opties?.logoId ?? blok?.logoId;
432
+ if (id)
433
+ ids.add(String(id));
434
+ }
435
+ if (!ids.size)
398
436
  return [];
399
437
  const { data } = await db
400
438
  .from("layout_logos")
401
439
  .select("id, naam, bestandspad, bestand_url")
402
- .eq("id", logoId)
403
- .maybeSingle();
404
- const logo = data;
405
- if (!logo)
406
- return [];
407
- let viewUrl = logo.bestand_url ?? null;
408
- if (logo.bestandspad) {
409
- const { data: sign } = await db.storage
410
- .from("layout-logos")
411
- .createSignedUrl(logo.bestandspad, 3600);
412
- if (sign?.signedUrl)
413
- viewUrl = sign.signedUrl;
414
- }
415
- return viewUrl ? [{ id: logo.id, naam: logo.naam, viewUrl }] : [];
440
+ .in("id", [...ids]);
441
+ const uit = [];
442
+ for (const logo of Array.isArray(data) ? data : []) {
443
+ let viewUrl = logo["bestand_url"] ?? null;
444
+ if (logo["bestandspad"]) {
445
+ const { data: sign } = await db.storage
446
+ .from("layout-logos")
447
+ .createSignedUrl(String(logo["bestandspad"]), 3600);
448
+ if (sign?.signedUrl)
449
+ viewUrl = sign.signedUrl;
450
+ }
451
+ if (viewUrl)
452
+ uit.push({ id: logo["id"], naam: logo["naam"], viewUrl });
453
+ }
454
+ return uit;
416
455
  }
417
456
  /**
418
457
  * Bouwt de HTML van de opdracht.
419
458
  * Volgorde: gekoppelde layout wint > meegestuurde HTML > actieve layout van
420
459
  * het documenttype. Zo werkt een bijgewerkte layout meteen door.
421
460
  */
422
- export async function bouwHtml(db, job, keuze) {
461
+ export async function bouwHtml(db, job, keuze, uitkomst) {
423
462
  const layout = keuze.layout;
463
+ const noteer = (bron) => {
464
+ if (uitkomst)
465
+ uitkomst.bron = bron;
466
+ };
467
+ noteer("eigen_layout");
424
468
  const meegestuurd = typeof job.render_html === "string" && job.render_html.trim().length > 40
425
469
  ? job.render_html
426
470
  : null;
427
471
  if (!layout) {
428
- if (meegestuurd)
472
+ if (meegestuurd) {
473
+ noteer("render_html");
429
474
  return meegestuurd;
475
+ }
430
476
  throw new GeenDocumentFout("Er is geen PDF, geen layout en geen opmaak meegestuurd voor dit document.");
431
477
  }
432
- if (!keuze.expliciet && meegestuurd)
478
+ // Het ontwerp wint van meegestuurde HTML zodra het een echt, actief ontwerp
479
+ // van deze module is. Is dat niet zo (Floriday-systeemlayout, inactief, of
480
+ // een voorbeeldafdruk uit de ontwerper), dan blijft de meegestuurde opmaak.
481
+ if (!keuze.ontwerpWint && meegestuurd) {
482
+ noteer("render_html");
433
483
  return meegestuurd;
484
+ }
434
485
  const p = (job.payload ?? {});
435
486
  const liggend = String(layout.orientatie ?? "portrait") === "landscape";
436
487
  const b = Number(layout.breedte_mm ?? 210);
@@ -442,8 +493,10 @@ export async function bouwHtml(db, job, keuze) {
442
493
  // de klant wint. Is er niets meegestuurd, dan bepaalt de terugvalkeuze of we
443
494
  // deze layout gebruiken of de opdracht netjes laten mislukken.
444
495
  if (cfg.klantlayout?.gebruik_klantlayout === true) {
445
- if (meegestuurd)
496
+ if (meegestuurd) {
497
+ noteer("klantlayout");
446
498
  return meegestuurd;
499
+ }
447
500
  if (cfg.klantlayout?.terugval === "niet_printen") {
448
501
  throw new GeenDocumentFout("Deze layout gebruikt altijd het ontwerp van de klant, maar er is geen klantontwerp meegestuurd.");
449
502
  }
@@ -607,7 +660,7 @@ async function verwerkJob(db, job) {
607
660
  "aangeleverde document wordt gebruikt, maar er is geen PDF meegestuurd.", true);
608
661
  return "mislukt";
609
662
  }
610
- const keuze = klaar ? { layout: null, expliciet: false } : await zoekLayout(db, job);
663
+ const keuze = klaar ? { layout: null, ontwerpWint: false } : await zoekLayout(db, job);
611
664
  const layout = keuze.layout;
612
665
  const printerKeuze = await zoekPrinter(db, job, layout);
613
666
  const printer = printerKeuze.printer;
@@ -616,9 +669,12 @@ async function verwerkJob(db, job) {
616
669
  `Printer '${printer?.naam ?? "onbekend"}' is niet aan PrintNode gekoppeld.`, true);
617
670
  return "mislukt";
618
671
  }
672
+ // Uitkomst van de opmaakkeuze; wordt na verzending vastgelegd naast de
673
+ // aangeleverde invoer, zodat achteraf zichtbaar is wat er gekozen is.
674
+ const uitkomst = { bron: "extern_pdf" };
619
675
  let pdfBase64 = klaar;
620
676
  if (!pdfBase64) {
621
- const html = await bouwHtml(db, job, keuze);
677
+ const html = await bouwHtml(db, job, keuze, uitkomst);
622
678
  if (!heeftZichtbareInhoud(html)) {
623
679
  throw new GeenDocumentFout("Het document was leeg: er zijn geen gegevens gevonden om af te drukken. " +
624
680
  "Controleer of de layout blokken bevat en of de order gegevens heeft.");
@@ -647,7 +703,10 @@ async function verwerkJob(db, job) {
647
703
  .from("print_jobs")
648
704
  .update({
649
705
  printer_id: printer.id,
650
- layout_id: job.layout_id ?? layout?.id ?? null,
706
+ // layout_id blijft de aangeleverde invoer en wordt bewust niet
707
+ // overschreven met de uitkomst.
708
+ gebruikte_layout_id: uitkomst.bron === "extern_pdf" ? null : (layout?.id ?? null),
709
+ gebruikte_bron: uitkomst.bron,
651
710
  extern_job_id: externJobId,
652
711
  foutmelding: null,
653
712
  volgende_poging: null,
@@ -665,11 +724,15 @@ async function verwerkJob(db, job) {
665
724
  return "mislukt";
666
725
  }
667
726
  }
727
+ /** Na deze tijd is een opdracht bij de printdienst niet meer te vertrouwen. */
728
+ const VERVAL_BEZIG_MIN = 30;
729
+ /** Een claim zonder verzending mag hooguit zo lang blijven hangen. */
730
+ const VERVAL_CLAIM_MIN = 10;
668
731
  /** Werkt de status bij van opdrachten die al naar een printer zijn gestuurd. */
669
732
  async function synchroniseerStatussen(db) {
670
733
  const { data } = await db
671
734
  .from("print_jobs")
672
- .select("id, extern_job_id")
735
+ .select("id, extern_job_id, pogingen, opgepakt_op")
673
736
  .eq("status", "bezig")
674
737
  .not("extern_job_id", "is", null)
675
738
  .limit(25);
@@ -691,14 +754,96 @@ async function synchroniseerStatussen(db) {
691
754
  bijgewerkt++;
692
755
  }
693
756
  }
694
- catch {
695
- // status opnieuw proberen bij de volgende ronde
757
+ catch (e) {
758
+ // Een onbereikbare printdienst mag niet stil blijven: tel hem als poging
759
+ // en leg vast waarom de status niet bevestigd kon worden.
760
+ const reden = e instanceof Error ? e.message : "Status kon niet worden opgehaald.";
761
+ await db
762
+ .from("print_jobs")
763
+ .update({
764
+ pogingen: Number(job.pogingen ?? 0) + 1,
765
+ foutmelding: `Status niet bevestigd: ${reden}`.slice(0, 500),
766
+ updated_at: new Date().toISOString(),
767
+ })
768
+ .eq("id", job.id);
769
+ await log(db, job.id, "status_onbekend", reden);
770
+ console.warn(`[printwachtrij] status ophalen mislukt voor job ${job.id}: ${reden}`);
696
771
  }
697
772
  }
698
773
  return bijgewerkt;
699
774
  }
700
- /** Eén ronde: wachtende opdrachten afdrukken en statussen bijwerken. */
775
+ /**
776
+ * Vangnet voor opdrachten die blijven hangen op 'bezig'. Twee gevallen:
777
+ * 1. wél naar de printdienst gestuurd, maar de status wordt niet meer
778
+ * bevestigd -> na 30 minuten definitief 'mislukt';
779
+ * 2. opgepakt maar nooit verstuurd (de ronde is onderweg gestorven of de
780
+ * runner draait niet meer) -> na 10 minuten terug naar 'wachtend'.
781
+ * Beide draaien in elke ronde, ook als er niets te claimen viel.
782
+ */
783
+ async function vervalControle(db) {
784
+ const nu = Date.now();
785
+ const grensVerstuurd = new Date(nu - VERVAL_BEZIG_MIN * 60000).toISOString();
786
+ const grensClaim = new Date(nu - VERVAL_CLAIM_MIN * 60000).toISOString();
787
+ let aantal = 0;
788
+ const { data: verstuurd } = await db
789
+ .from("print_jobs")
790
+ .select("id")
791
+ .eq("status", "bezig")
792
+ .not("extern_job_id", "is", null)
793
+ .or(`opgepakt_op.lt.${grensVerstuurd},and(opgepakt_op.is.null,updated_at.lt.${grensVerstuurd})`)
794
+ .limit(50);
795
+ for (const job of arr(verstuurd)) {
796
+ const reden = `De printdienst bevestigde de status van deze opdracht ${VERVAL_BEZIG_MIN} minuten ` +
797
+ "lang niet. Of hij is afgedrukt is niet vast te stellen; controleer de printer en print zo nodig opnieuw.";
798
+ await db
799
+ .from("print_jobs")
800
+ .update({
801
+ status: "mislukt",
802
+ foutmelding: reden.slice(0, 500),
803
+ voltooid_op: new Date().toISOString(),
804
+ updated_at: new Date().toISOString(),
805
+ })
806
+ .eq("id", job.id);
807
+ await log(db, job.id, "vervallen", reden);
808
+ aantal++;
809
+ }
810
+ const { data: vastgelopen } = await db
811
+ .from("print_jobs")
812
+ .select("id, pogingen")
813
+ .eq("status", "bezig")
814
+ .is("extern_job_id", null)
815
+ .or(`opgepakt_op.lt.${grensClaim},and(opgepakt_op.is.null,updated_at.lt.${grensClaim})`)
816
+ .limit(50);
817
+ for (const job of arr(vastgelopen)) {
818
+ const pogingen = Number(job.pogingen ?? 0);
819
+ const opnieuw = pogingen < MAX_POGINGEN;
820
+ await db
821
+ .from("print_jobs")
822
+ .update({
823
+ status: opnieuw ? "wachtend" : "mislukt",
824
+ pogingen: pogingen + 1,
825
+ foutmelding: opnieuw
826
+ ? "De vorige printronde is niet afgemaakt; deze opdracht staat weer in de wachtrij."
827
+ : "De printronde is meerdere keren niet afgemaakt. Controleer of de printdienst bereikbaar is.",
828
+ opgepakt_op: null,
829
+ voltooid_op: opnieuw ? null : new Date().toISOString(),
830
+ updated_at: new Date().toISOString(),
831
+ })
832
+ .eq("id", job.id);
833
+ await log(db, job.id, opnieuw ? "opnieuw_proberen" : "mislukt", "Vastgelopen ronde hersteld.");
834
+ aantal++;
835
+ }
836
+ return aantal;
837
+ }
838
+ /** Eén ronde: vastgelopen opdrachten opruimen, afdrukken en statussen bijwerken. */
701
839
  export async function verwerkWachtrij(db, max = 5) {
840
+ // Eerst het vangnet: dit draait ook als er niets te claimen valt.
841
+ try {
842
+ await vervalControle(db);
843
+ }
844
+ catch (e) {
845
+ console.warn(`[printwachtrij] vervalcontrole mislukt: ${e instanceof Error ? e.message : "onbekend"}`);
846
+ }
702
847
  const { data, error } = await db.rpc("claim_cloud_print_jobs", { _max: max });
703
848
  if (error)
704
849
  throw new Error(error.message);