@flowselections/layout-maker 1.36.7 → 1.45.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist-lib/components/AfleverbriefBuilderPage.d.ts +10 -0
  2. package/dist-lib/components/AfleverbriefBuilderPage.d.ts.map +1 -0
  3. package/dist-lib/components/AfleverbriefBuilderPage.js +101 -0
  4. package/dist-lib/components/BlokOntwerperPage.d.ts.map +1 -1
  5. package/dist-lib/components/BlokOntwerperPage.js +8 -3
  6. package/dist-lib/components/ComputersLijst.d.ts.map +1 -1
  7. package/dist-lib/components/ComputersLijst.js +1 -1
  8. package/dist-lib/components/FustKolomVolgorde.d.ts +11 -0
  9. package/dist-lib/components/FustKolomVolgorde.d.ts.map +1 -0
  10. package/dist-lib/components/FustKolomVolgorde.js +52 -0
  11. package/dist-lib/components/LayoutsPage.d.ts.map +1 -1
  12. package/dist-lib/components/LayoutsPage.js +10 -9
  13. package/dist-lib/components/PaginaDocument.d.ts +4 -1
  14. package/dist-lib/components/PaginaDocument.d.ts.map +1 -1
  15. package/dist-lib/components/PaginaDocument.js +10 -5
  16. package/dist-lib/components/PakbonBuilderPage.d.ts.map +1 -1
  17. package/dist-lib/components/PakbonBuilderPage.js +34 -14
  18. package/dist-lib/components/PakbonPreview.d.ts +4 -2
  19. package/dist-lib/components/PakbonPreview.d.ts.map +1 -1
  20. package/dist-lib/components/PakbonPreview.js +74 -24
  21. package/dist-lib/components/PakbonSectiesPanel.d.ts +5 -3
  22. package/dist-lib/components/PakbonSectiesPanel.d.ts.map +1 -1
  23. package/dist-lib/components/PakbonSectiesPanel.js +8 -5
  24. package/dist-lib/components/PdfMapToestemmingBanner.d.ts +2 -1
  25. package/dist-lib/components/PdfMapToestemmingBanner.d.ts.map +1 -1
  26. package/dist-lib/components/PdfMapToestemmingBanner.js +73 -14
  27. package/dist-lib/components/PdfVoorbeeld.d.ts +20 -0
  28. package/dist-lib/components/PdfVoorbeeld.d.ts.map +1 -0
  29. package/dist-lib/components/PdfVoorbeeld.js +95 -0
  30. package/dist-lib/components/PrintProgrammaUitleg.d.ts.map +1 -1
  31. package/dist-lib/components/PrintProgrammaUitleg.js +1 -1
  32. package/dist-lib/components/PrintQueuePage.d.ts.map +1 -1
  33. package/dist-lib/components/PrintQueuePage.js +81 -43
  34. package/dist-lib/components/PrintplekDetailPage.d.ts.map +1 -1
  35. package/dist-lib/components/PrintplekDetailPage.js +2 -2
  36. package/dist-lib/components/PrintplekWizard.d.ts.map +1 -1
  37. package/dist-lib/components/PrintplekWizard.js +6 -2
  38. package/dist-lib/components/ProductiebonBuilderPage.d.ts.map +1 -1
  39. package/dist-lib/components/ProductiebonBuilderPage.js +13 -7
  40. package/dist-lib/index.d.ts +16 -6
  41. package/dist-lib/index.d.ts.map +1 -1
  42. package/dist-lib/index.js +11 -5
  43. package/dist-lib/integrations/supabase/auth-middleware.d.ts +3585 -1267
  44. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  45. package/dist-lib/integrations/supabase/client.server.d.ts +3585 -1267
  46. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  47. package/dist-lib/integrations/supabase/moduleClient.d.ts +25898 -0
  48. package/dist-lib/integrations/supabase/moduleClient.d.ts.map +1 -0
  49. package/dist-lib/integrations/supabase/moduleClient.js +29 -0
  50. package/dist-lib/integrations/supabase/types.d.ts +3607 -1265
  51. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  52. package/dist-lib/integrations/supabase/types.js +1 -0
  53. package/dist-lib/lib/aiLayout.functions.d.ts +3585 -1267
  54. package/dist-lib/lib/aiLayout.functions.d.ts.map +1 -1
  55. package/dist-lib/lib/code128.d.ts +5 -0
  56. package/dist-lib/lib/code128.d.ts.map +1 -0
  57. package/dist-lib/lib/code128.js +51 -0
  58. package/dist-lib/lib/documentVoorbeeld.functions.d.ts +23 -0
  59. package/dist-lib/lib/documentVoorbeeld.functions.d.ts.map +1 -0
  60. package/dist-lib/lib/documentVoorbeeld.functions.js +37 -0
  61. package/dist-lib/lib/fontMetrics.d.ts +15 -0
  62. package/dist-lib/lib/fontMetrics.d.ts.map +1 -0
  63. package/dist-lib/lib/fontMetrics.js +118 -0
  64. package/dist-lib/lib/htmlNaarPdf.server.d.ts +2 -2
  65. package/dist-lib/lib/htmlNaarPdf.server.d.ts.map +1 -1
  66. package/dist-lib/lib/htmlNaarPdf.server.js +2 -1
  67. package/dist-lib/lib/htmlPdf.server.d.ts +11 -0
  68. package/dist-lib/lib/htmlPdf.server.d.ts.map +1 -1
  69. package/dist-lib/lib/htmlPdf.server.js +170 -9
  70. package/dist-lib/lib/layoutPreview.d.ts.map +1 -1
  71. package/dist-lib/lib/layoutPreview.js +6 -3
  72. package/dist-lib/lib/orderData.server.d.ts.map +1 -1
  73. package/dist-lib/lib/orderData.server.js +49 -16
  74. package/dist-lib/lib/pakbonBarcodeVerzoek.d.ts +39 -0
  75. package/dist-lib/lib/pakbonBarcodeVerzoek.d.ts.map +1 -0
  76. package/dist-lib/lib/pakbonBarcodeVerzoek.js +80 -0
  77. package/dist-lib/lib/pakbonBuilder.d.ts +90 -5
  78. package/dist-lib/lib/pakbonBuilder.d.ts.map +1 -1
  79. package/dist-lib/lib/pakbonBuilder.js +455 -58
  80. package/dist-lib/lib/pakbonContent.d.ts +41 -0
  81. package/dist-lib/lib/pakbonContent.d.ts.map +1 -0
  82. package/dist-lib/lib/pakbonContent.js +668 -0
  83. package/dist-lib/lib/pakbonDocument.d.ts +199 -0
  84. package/dist-lib/lib/pakbonDocument.d.ts.map +1 -0
  85. package/dist-lib/lib/pakbonDocument.js +735 -0
  86. package/dist-lib/lib/pakbonHtml.d.ts +21 -2
  87. package/dist-lib/lib/pakbonHtml.d.ts.map +1 -1
  88. package/dist-lib/lib/pakbonHtml.js +144 -36
  89. package/dist-lib/lib/pakbonPresentation.d.ts +8 -0
  90. package/dist-lib/lib/pakbonPresentation.d.ts.map +1 -0
  91. package/dist-lib/lib/pakbonPresentation.js +31 -0
  92. package/dist-lib/lib/pakbonScan.d.ts +44 -0
  93. package/dist-lib/lib/pakbonScan.d.ts.map +1 -0
  94. package/dist-lib/lib/pakbonScan.js +87 -0
  95. package/dist-lib/lib/pdfJobSaver.d.ts +10 -6
  96. package/dist-lib/lib/pdfJobSaver.d.ts.map +1 -1
  97. package/dist-lib/lib/pdfJobSaver.js +302 -99
  98. package/dist-lib/lib/pdfMap.d.ts +37 -2
  99. package/dist-lib/lib/pdfMap.d.ts.map +1 -1
  100. package/dist-lib/lib/pdfMap.js +120 -7
  101. package/dist-lib/lib/pdfOpslagStatus.d.ts +23 -0
  102. package/dist-lib/lib/pdfOpslagStatus.d.ts.map +1 -0
  103. package/dist-lib/lib/pdfOpslagStatus.js +74 -0
  104. package/dist-lib/lib/printApi.d.ts +31 -0
  105. package/dist-lib/lib/printApi.d.ts.map +1 -1
  106. package/dist-lib/lib/printApi.js +62 -1
  107. package/dist-lib/lib/printQueue.server.d.ts +13 -0
  108. package/dist-lib/lib/printQueue.server.d.ts.map +1 -1
  109. package/dist-lib/lib/printQueue.server.js +673 -96
  110. package/dist-lib/lib/printService.functions.d.ts +11840 -2568
  111. package/dist-lib/lib/printService.functions.d.ts.map +1 -1
  112. package/dist-lib/lib/profielHtml.d.ts.map +1 -1
  113. package/dist-lib/lib/profielHtml.js +8 -1
  114. package/dist-lib/lib/supabaseClient.d.ts +1 -3
  115. package/dist-lib/lib/supabaseClient.d.ts.map +1 -1
  116. package/dist-lib/lib/supabaseClient.js +4 -8
  117. package/dist-lib/lib/useDocumentData.d.ts +8 -0
  118. package/dist-lib/lib/useDocumentData.d.ts.map +1 -1
  119. package/dist-lib/lib/useDocumentData.js +1 -1
  120. package/dist-lib/lib/voorbeeldPrint.d.ts +3 -0
  121. package/dist-lib/lib/voorbeeldPrint.d.ts.map +1 -1
  122. package/dist-lib/lib/voorbeeldPrint.js +65 -17
  123. package/package.json +9 -2
@@ -75,8 +75,134 @@ export const FUST_VELD_OPTIES = [
75
75
  { value: "locatie", label: "Locatie" },
76
76
  { value: "fustsoort", label: "Tray/fustsoort" },
77
77
  { value: "planten_per_fust", label: "Planten per fust" },
78
+ { value: "fust_aantal", label: "Aantal fusten" },
78
79
  { value: "aantal", label: "Totaal planten" },
79
80
  ];
81
+ /**
82
+ * Alle velden die op een fustregel van de afleverbrief kunnen staan. De
83
+ * afleverbrief beschrijft één kar, dus kar- en laagnummer horen er ook bij.
84
+ */
85
+ export const FUSTREGEL_VELD_OPTIES = [
86
+ { value: "fustsoort", label: "Fustsoort" },
87
+ { value: "fusthoogte", label: "Fusthoogte" },
88
+ { value: "belading", label: "Belading" },
89
+ { value: "karnummer", label: "Karnummer" },
90
+ { value: "laagnummer", label: "Laag" },
91
+ { value: "artikelnummer", label: "Artikelnummer" },
92
+ { value: "artikelnaam", label: "Omschrijving" },
93
+ { value: "potmaat", label: "Potmaat" },
94
+ { value: "kleur", label: "Kleur" },
95
+ { value: "productgroep", label: "Productgroep" },
96
+ { value: "locatie", label: "Locatie" },
97
+ { value: "planten_per_fust", label: "Planten per fust" },
98
+ { value: "fust_aantal", label: "Aantal fusten" },
99
+ { value: "aantal", label: "Totaal planten" },
100
+ { value: "gewicht", label: "Gewicht" },
101
+ ];
102
+ /** Labels van de fustregelvelden, gedeeld door scherm en afdruk. */
103
+ export const FUSTREGEL_LABELS = Object.fromEntries(FUSTREGEL_VELD_OPTIES.map((o) => [o.value, o.label]));
104
+ /** Velden die rechts uitgelijnd horen te staan. */
105
+ export const FUSTREGEL_RECHTS = new Set([
106
+ "planten_per_fust",
107
+ "fust_aantal",
108
+ "aantal",
109
+ "gewicht",
110
+ ]);
111
+ const tekst = (waarde) => String(waarde ?? "").trim();
112
+ /**
113
+ * Zet de karren van een levering om naar losse fustregels. Met `comprimeren`
114
+ * komen identieke fusten (zelfde fustsoort, artikel, potmaat, kleur en aantal
115
+ * planten per fust) op één regel te staan; het aantal fusten wordt opgeteld.
116
+ * Zonder `comprimeren` krijgt elke losse fust een eigen regel.
117
+ */
118
+ export function fustRegels(karren, opties = {}) {
119
+ const lijst = Array.isArray(karren) ? karren : [];
120
+ const regels = [];
121
+ for (const kar of lijst) {
122
+ const karnummer = tekst(kar?.karnummer ?? kar?.cart_label);
123
+ for (const laag of Array.isArray(kar?.lagen) ? kar.lagen : []) {
124
+ const fusten = Array.isArray(laag?.fusten) ? laag.fusten : [];
125
+ const fustInfo = (soort) => fusten.find((f) => tekst(f?.fusttype ?? f?.fustsoort).toLowerCase() === soort.toLowerCase());
126
+ for (const pr of Array.isArray(laag?.producten) ? laag.producten : []) {
127
+ const fustsoort = tekst(pr?.fustsoort) || "Onbekend";
128
+ const info = fustInfo(fustsoort);
129
+ regels.push({
130
+ fustsoort,
131
+ fusthoogte: tekst(info?.hoogte),
132
+ belading: tekst(pr?.belading ?? info?.belading ?? laag?.belading),
133
+ karnummer,
134
+ laagnummer: tekst(laag?.laagnummer),
135
+ artikelnummer: tekst(pr?.artikelnummer),
136
+ artikelnaam: tekst(pr?.artikelnaam),
137
+ potmaat: tekst(pr?.potmaat ?? laag?.potmaat),
138
+ kleur: tekst(pr?.kleur),
139
+ productgroep: tekst(pr?.productgroep),
140
+ locatie: tekst(pr?.locatie),
141
+ planten_per_fust: Number(pr?.planten_per_fust ?? 0) || 0,
142
+ fust_aantal: Number(pr?.fust_aantal ?? 0) || 0,
143
+ aantal: Number(pr?.aantal ?? 0) || 0,
144
+ gewicht: tekst(pr?.gewicht),
145
+ barcode: tekst(pr?.barcode ?? info?.barcode),
146
+ });
147
+ }
148
+ }
149
+ }
150
+ let uit = regels;
151
+ if (opties.comprimeren !== false) {
152
+ const map = new Map();
153
+ for (const r of regels) {
154
+ const sleutel = [
155
+ r.fustsoort,
156
+ r.fusthoogte,
157
+ r.artikelnummer,
158
+ r.artikelnaam,
159
+ r.potmaat,
160
+ r.kleur,
161
+ r.planten_per_fust,
162
+ ]
163
+ .join("|")
164
+ .toLowerCase();
165
+ const bestaand = map.get(sleutel);
166
+ if (bestaand) {
167
+ bestaand.fust_aantal += r.fust_aantal;
168
+ bestaand.aantal += r.aantal;
169
+ // Kar- en laagnummers van samengevoegde regels blijven zichtbaar.
170
+ const lagen = new Set([...bestaand.laagnummer.split(", "), r.laagnummer].filter(Boolean));
171
+ bestaand.laagnummer = [...lagen].join(", ");
172
+ if (!bestaand.locatie)
173
+ bestaand.locatie = r.locatie;
174
+ }
175
+ else {
176
+ map.set(sleutel, { ...r });
177
+ }
178
+ }
179
+ uit = [...map.values()];
180
+ }
181
+ else {
182
+ // Zonder samenvoegen krijgt elke losse fust een eigen regel, zodat de
183
+ // schakelaar zichtbaar effect heeft: 8 dezelfde fusten worden 8 regels
184
+ // van 1 fust in plaats van één regel met 8.
185
+ const los = [];
186
+ for (const r of regels) {
187
+ const n = r.fust_aantal;
188
+ if (n > 1 && n <= 500) {
189
+ const perFust = r.planten_per_fust > 0 ? r.planten_per_fust : Math.round(r.aantal / n);
190
+ for (let i = 0; i < n; i += 1) {
191
+ los.push({ ...r, fust_aantal: 1, aantal: perFust });
192
+ }
193
+ }
194
+ else {
195
+ los.push(r);
196
+ }
197
+ }
198
+ uit = los;
199
+ }
200
+ const sortering = opties.sortering ?? "fustsoort";
201
+ if (sortering !== "eigen") {
202
+ uit = [...uit].sort((a, b) => String(a[sortering] ?? "").localeCompare(String(b[sortering] ?? ""), "nl", { numeric: true }));
203
+ }
204
+ return uit;
205
+ }
80
206
  const groepering = {
81
207
  key: "groepering",
82
208
  label: "Groeperen op",
@@ -209,10 +335,56 @@ export const SECTIE_REGISTER = [
209
335
  },
210
336
  ],
211
337
  },
338
+ {
339
+ type: "fustregels",
340
+ label: "Fustregels",
341
+ omschrijving: "Alle fusten van deze kar als regels, met de fustgegevens en het aantal fusten per regel.",
342
+ opties: [
343
+ {
344
+ key: "velden",
345
+ label: "Zichtbare fustgegevens",
346
+ type: "meerkeuze",
347
+ standaard: [
348
+ "fustsoort",
349
+ "artikelnummer",
350
+ "artikelnaam",
351
+ "potmaat",
352
+ "planten_per_fust",
353
+ "fust_aantal",
354
+ "aantal",
355
+ ],
356
+ opties: FUSTREGEL_VELD_OPTIES,
357
+ },
358
+ {
359
+ key: "comprimeren",
360
+ label: "Identieke fusten samenvoegen tot één regel",
361
+ type: "schakelaar",
362
+ standaard: true,
363
+ hint: "Twee dezelfde fusten op de kar komen dan op één regel te staan, met 2 bij het aantal fusten.",
364
+ },
365
+ { key: "toonKoppen", label: "Kolomkoppen tonen", type: "schakelaar", standaard: true },
366
+ { key: "zebra", label: "Om-en-om arcering", type: "schakelaar", standaard: false },
367
+ { key: "randen", label: "Randen om cellen", type: "schakelaar", standaard: true },
368
+ { key: "toonTotaal", label: "Totaalregel tonen", type: "schakelaar", standaard: true },
369
+ {
370
+ key: "sortering",
371
+ label: "Sorteren op",
372
+ type: "keuze",
373
+ standaard: "fustsoort",
374
+ opties: [
375
+ { value: "eigen", label: "Eigen volgorde" },
376
+ { value: "fustsoort", label: "Fustsoort" },
377
+ { value: "artikelnummer", label: "Artikelnummer" },
378
+ { value: "artikelnaam", label: "Omschrijving" },
379
+ { value: "locatie", label: "Locatie" },
380
+ ],
381
+ },
382
+ ],
383
+ },
212
384
  {
213
385
  type: "pickmethode",
214
386
  label: "Pickmethode",
215
- omschrijving: "Bepaalt de opbouw van de pakbon: per kar (met karren en lagen) of per fust (verzamellijst).",
387
+ omschrijving: "Bepaalt of orders per kar, per fust of op beide manieren worden gepickt.",
216
388
  opties: [
217
389
  {
218
390
  key: "methode",
@@ -222,8 +394,9 @@ export const SECTIE_REGISTER = [
222
394
  opties: [
223
395
  { value: "kar", label: "Order picken per kar" },
224
396
  { value: "fusten", label: "Order picken per fusten (zelf karindeling maken)" },
397
+ { value: "beide", label: "Order picken per kar én per fusten (karren + verzamellijst)" },
225
398
  ],
226
- hint: "Bij picken per fusten vervallen de blokken Karren, Lagen, Producten en Fusten en toont de pakbon een fustverzamellijst.",
399
+ hint: "Bij picken per fusten vervallen de blokken Karren, Lagen, Producten en Fusten. Bij de gecombineerde keuze bepaalt de Engine per opdracht: full_cart toont de karindeling, partial_order alleen de te verzamelen fusten.",
227
400
  },
228
401
  {
229
402
  key: "kop",
@@ -243,12 +416,21 @@ export const SECTIE_REGISTER = [
243
416
  "locatie",
244
417
  "fustsoort",
245
418
  "planten_per_fust",
419
+ "fust_aantal",
246
420
  "aantal",
247
421
  ],
248
422
  opties: FUST_VELD_OPTIES,
249
423
  },
250
424
  { key: "toonFusthoogte", label: "Fusthoogte en belading tonen", type: "schakelaar", standaard: true },
251
425
  { key: "toonTotaal", label: "Totaal aantal fusten tonen", type: "schakelaar", standaard: true },
426
+ {
427
+ key: "toonBarcode",
428
+ label: "Barcode voor afleverbrief tonen",
429
+ type: "schakelaar",
430
+ standaard: true,
431
+ hint: "De barcode wordt uitsluitend overgenomen uit de Engine-opdracht.",
432
+ },
433
+ { key: "barcodeHoogte", label: "Barcodehoogte (mm)", type: "getal", standaard: 15 },
252
434
  ],
253
435
  },
254
436
  {
@@ -474,6 +656,13 @@ export const SECTIE_REGISTER = [
474
656
  { key: "label", label: "Bijschrift", type: "tekst", standaard: "Voor ontvangst getekend" },
475
657
  { key: "toonNaam", label: "Naamregel", type: "schakelaar", standaard: true },
476
658
  { key: "toonDatum", label: "Datumregel", type: "schakelaar", standaard: true },
659
+ {
660
+ key: "toonBarcode",
661
+ label: "Barcode onderaan tonen",
662
+ type: "schakelaar",
663
+ standaard: true,
664
+ hint: "De barcode wordt uitsluitend overgenomen uit de Engine-opdracht.",
665
+ },
477
666
  ],
478
667
  },
479
668
  {
@@ -498,9 +687,9 @@ export const SECTIE_REGISTER = [
498
687
  {
499
688
  type: "barcode",
500
689
  label: "Barcode",
501
- omschrijving: "Barcode met orderwaarde.",
690
+ omschrijving: "Scanbare barcode. Bij Engine-pakbonnen komt de waarde uit de opdracht.",
502
691
  opties: [
503
- { key: "waarde", label: "Inhoud", type: "tekst", standaard: "{{order.nummer}}" },
692
+ { key: "waarde", label: "Inhoud", type: "tekst", standaard: "{{barcode}}" },
504
693
  { key: "hoogte", label: "Hoogte (mm)", type: "getal", standaard: 15 },
505
694
  ],
506
695
  },
@@ -555,28 +744,94 @@ export function standaardPakbonConfig() {
555
744
  export function leesPakbonConfig(config) {
556
745
  // Accepteert zowel de layout-config ({ pakbon: {...} }) als een kale
557
746
  // PakbonConfig. Zonder deze tolerantie viel de afdruk terug op de
558
- // standaardsecties en verdween o.a. het documentlogo.
747
+ // standaardsecties en verdween o.a. het documentlogo. De sleutel
748
+ // "afleverbrief" wordt meegelezen zodat de printpijplijn ook
749
+ // afleverbrief-layouts met hun eigen secties afdrukt.
559
750
  const raw = config;
560
751
  const c = Array.isArray(raw?.secties)
561
752
  ? raw
562
- : raw?.pakbon;
753
+ : (raw?.pakbon ?? raw?.afleverbrief);
563
754
  const secties = Array.isArray(c?.secties) ? c.secties : null;
564
755
  const logo = c?.logo ?? null;
565
756
  const basis = secties && secties.length ? { secties } : standaardPakbonConfig();
566
757
  return { ...basis, secties: migreerPickmethode(basis.secties), logo };
567
758
  }
759
+ /** Standaardindeling van een afleverbrief: geen kar-, laag- of fustblokken. */
760
+ export function standaardAfleverbriefConfig() {
761
+ return {
762
+ secties: [
763
+ "logo",
764
+ "bedrijfsgegevens",
765
+ "klantgegevens",
766
+ "afleveradres",
767
+ "orderinformatie",
768
+ "transportinformatie",
769
+ "fustregels",
770
+ "totalen",
771
+ "opmerkingen",
772
+ "handtekening",
773
+ ].map(nieuweSectie),
774
+ };
775
+ }
776
+ /**
777
+ * Blokken die op een afleverbrief thuishoren. Een afleverbrief beschrijft
778
+ * altijd precies één kar, dus kar-, laag- en pickmethodeblokken vervallen.
779
+ */
780
+ export const AFLEVERBRIEF_SECTIE_TYPES = [
781
+ "logo",
782
+ "bedrijfsgegevens",
783
+ "klantgegevens",
784
+ "afleveradres",
785
+ "factuuradres",
786
+ "orderinformatie",
787
+ "transportinformatie",
788
+ "fustregels",
789
+ "totalen",
790
+ "opmerkingen",
791
+ "handtekening",
792
+ "tekstblok",
793
+ "qrcode",
794
+ "barcode",
795
+ "afbeelding",
796
+ ];
797
+ /** Leest de sectie-opbouw van een afleverbrief-layout. */
798
+ export function leesAfleverbriefConfig(config) {
799
+ // Eerst de eigen sleutel; oudere afleverbrief-layouts zijn per abuis via de
800
+ // pakbon-opbouw onder "pakbon" opgeslagen en worden hier meegelezen.
801
+ const raw = config;
802
+ const c = Array.isArray(raw?.secties)
803
+ ? raw
804
+ : (raw?.afleverbrief ?? raw?.pakbon);
805
+ const secties = Array.isArray(c?.secties) ? c.secties : null;
806
+ const logo = c?.logo ?? null;
807
+ const basis = secties && secties.length ? { secties } : standaardAfleverbriefConfig();
808
+ return { ...basis, secties: basis.secties, logo };
809
+ }
568
810
  /** Zet de oude karren-optie "pickmethode" om naar een eigen pickmethode-blok. */
569
811
  function migreerPickmethode(secties) {
812
+ // Alleen relevant voor pakbonnen met een karren-blok; een afleverbrief
813
+ // krijgt nooit een pickmethode-kop.
570
814
  if (secties.some((s) => s.type === "pickmethode"))
571
815
  return secties;
816
+ if (!secties.some((s) => s.type === "karren"))
817
+ return secties;
572
818
  const karIndex = secties.findIndex((s) => s.type === "karren");
573
819
  const oud = karIndex >= 0 ? String(secties[karIndex].opties?.["pickmethode"] ?? "kar") : "kar";
574
820
  const blok = nieuweSectie("pickmethode");
575
- blok.opties["methode"] = oud === "fusten" ? "fusten" : "kar";
821
+ blok.opties["methode"] = oud === "fusten" || oud === "beide" ? oud : "kar";
576
822
  const kopie = [...secties];
577
823
  kopie.splice(karIndex >= 0 ? karIndex : kopie.length, 0, blok);
578
824
  return kopie;
579
825
  }
826
+ /** Normaliseert alleen de twee varianten uit het gedeelde Engine-contract. */
827
+ export function pakbonLayoutVariant(value) {
828
+ const variant = String(value ?? "").trim().toLowerCase();
829
+ if (variant === "full_cart")
830
+ return "full_cart";
831
+ if (variant === "partial_order")
832
+ return "partial_order";
833
+ return null;
834
+ }
580
835
  /** Actieve pickmethode van een pakbonconfiguratie. */
581
836
  export function pickMethode(secties) {
582
837
  const lijst = Array.isArray(secties) ? secties : [];
@@ -584,13 +839,31 @@ export function pickMethode(secties) {
584
839
  const waarde = blok
585
840
  ? String(blok.opties?.["methode"] ?? "kar")
586
841
  : String(lijst.find((s) => s.type === "karren")?.opties?.["pickmethode"] ?? "kar");
587
- return waarde === "fusten" ? "fusten" : "kar";
842
+ if (waarde === "fusten")
843
+ return "fusten";
844
+ if (waarde === "beide")
845
+ return "beide";
846
+ return "kar";
588
847
  }
589
848
  const KAR_SECTIES = ["karren", "lagen", "producten", "fusten"];
590
849
  /** Blokken die bij de gekozen pickmethode niet van toepassing zijn. */
591
850
  export function sectieOnderdrukt(type, methode) {
592
851
  return methode === "fusten" && KAR_SECTIES.includes(type);
593
852
  }
853
+ /**
854
+ * Blokken die de Engine-variant weglaat. Zonder variant blijft de editor/het
855
+ * losse voorbeeld compleet; een echte Engine-opdracht wordt in de wachtrij
856
+ * geblokkeerd totdat de variant aanwezig is.
857
+ */
858
+ export function sectieOnderdruktVoorVariant(type, methode, variant) {
859
+ if (sectieOnderdrukt(type, methode))
860
+ return true;
861
+ if (methode !== "beide" || !variant)
862
+ return false;
863
+ if (variant === "full_cart")
864
+ return type === "pickmethode";
865
+ return type === "pickmethode" ? false : [...KAR_SECTIES, "orderregels"].includes(type);
866
+ }
594
867
  const p = (nr, naam, potmaat, aantal, locatie, groep, kleur) => ({
595
868
  artikelnummer: nr,
596
869
  artikelnaam: naam,
@@ -651,6 +924,7 @@ export const DEMO_ORDER = {
651
924
  karren: [
652
925
  {
653
926
  karnummer: 5,
927
+ barcode: "VOORBEELD-KAR-05",
654
928
  route: "Route Zuid",
655
929
  chauffeur: "J. Bakker",
656
930
  gewicht: "142 kg",
@@ -687,6 +961,7 @@ export const DEMO_ORDER = {
687
961
  // ze samengevoegd tot één blok met "Deze kar 3× pakken".
688
962
  ...[6, 7, 8].map((nr) => ({
689
963
  karnummer: nr,
964
+ barcode: `VOORBEELD-KAR-${String(nr).padStart(2, "0")}`,
690
965
  route: "Route Zuid",
691
966
  chauffeur: "J. Bakker",
692
967
  gewicht: "118 kg",
@@ -813,8 +1088,10 @@ function laagVingerafdruk(l) {
813
1088
  return [l.belading, l.potmaat, l.gewicht, l.opmerking, producten, fusten].join("#");
814
1089
  }
815
1090
  function karVingerafdruk(k) {
816
- // karnummer bewust niet meegenomen: alleen de indeling telt
817
- return [k.route, k.chauffeur, k.gewicht, k.gedeeld ? "gedeeld" : "", (k.lagen ?? []).map(laagVingerafdruk).join("~")].join("#");
1091
+ // karnummer bewust niet meegenomen: alleen de indeling telt. Een kar met een
1092
+ // eigen barcode of cart_id van de Engine is wél uniek en wordt nooit gebundeld.
1093
+ const uniek = String(k.barcode ?? "") || String(k.cart_id ?? "") || String(k.cart_label ?? "");
1094
+ return [uniek, k.route, k.chauffeur, k.gewicht, k.gedeeld ? "gedeeld" : "", (k.lagen ?? []).map(laagVingerafdruk).join("~")].join("#");
818
1095
  }
819
1096
  /** Groepeert identieke karren. Bij `aan = false` blijft elke kar los. */
820
1097
  export function groepeerIdentiekeKarren(karren, aan) {
@@ -862,69 +1139,189 @@ export function karBundelTekst(aantal) {
862
1139
  export function laagBundelTekst(aantal) {
863
1140
  return `Deze laag ${aantal}×`;
864
1141
  }
1142
+ const fustSleutel = (waarde) => String(waarde ?? "")
1143
+ .trim()
1144
+ .replace(/\s+/g, " ")
1145
+ .toLowerCase();
865
1146
  /**
866
1147
  * Verzamellijst met alle fusten van een order, opgeteld per fusttype,
867
1148
  * inclusief de producten die in dat fusttype gepickt moeten worden.
868
- * Gebruikt wanneer er per fust gepickt wordt en de gebruiker zelf de
869
- * karindeling maakt.
1149
+ * Een product hoort uitsluitend bij zijn eigen fustsoort — nooit bij alle
1150
+ * fustgroepen van de laag.
870
1151
  */
871
1152
  export function fustVerzamellijst(karren) {
872
1153
  const lijst = Array.isArray(karren) ? karren : [];
873
1154
  const map = new Map();
1155
+ /** Expliciet door de Engine opgegeven fustaantallen per fusttype. */
1156
+ const opgegevenAantallen = new Map();
1157
+ const zoekGroep = (type, f) => {
1158
+ const sleutel = fustSleutel(type) || "onbekend";
1159
+ let groep = map.get(sleutel);
1160
+ if (!groep) {
1161
+ groep = {
1162
+ fusttype: String(type ?? "").trim() || "Onbekend",
1163
+ aantal: 0,
1164
+ hoogte: String(f?.["hoogte"] ?? ""),
1165
+ belading: String(f?.["belading"] ?? ""),
1166
+ producten: [],
1167
+ };
1168
+ map.set(sleutel, groep);
1169
+ }
1170
+ else if (f) {
1171
+ if (!groep.hoogte && f["hoogte"])
1172
+ groep.hoogte = String(f["hoogte"]);
1173
+ if (!groep.belading && f["belading"])
1174
+ groep.belading = String(f["belading"]);
1175
+ }
1176
+ return groep;
1177
+ };
874
1178
  for (const kar of lijst) {
875
1179
  for (const laag of Array.isArray(kar?.lagen) ? kar.lagen : []) {
876
1180
  const producten = Array.isArray(laag?.producten) ? laag.producten : [];
877
- // Zonder aparte fustlijst: fusten afleiden uit de producten, zodat een
878
- // module die alleen orderregels aanlevert toch een verzamellijst krijgt.
879
1181
  const opgegeven = Array.isArray(laag?.fusten) ? laag.fusten : [];
880
- const afgeleid = new Map();
881
- if (!opgegeven.length) {
882
- for (const pr of producten) {
883
- const soort = String(pr?.fustsoort ?? "").trim() || "Onbekend";
884
- const aantal = Number(pr?.fust_aantal ?? 0) || 0;
885
- if (aantal > 0)
886
- afgeleid.set(soort, (afgeleid.get(soort) ?? 0) + aantal);
887
- }
888
- }
889
- const fusten = opgegeven.length
890
- ? opgegeven
891
- : Array.from(afgeleid.entries()).map(([fusttype, aantal]) => ({ fusttype, aantal }));
892
- for (const f of fusten) {
1182
+ // Fustmetadata (hoogte/belading) en expliciete aantallen vastleggen.
1183
+ for (const f of opgegeven) {
893
1184
  const type = String(f?.fusttype ?? f?.fustsoort ?? "Onbekend");
894
- let groep = map.get(type);
895
- if (!groep) {
896
- groep = {
897
- fusttype: type,
898
- aantal: 0,
899
- hoogte: String(f?.hoogte ?? ""),
900
- belading: String(f?.belading ?? ""),
901
- producten: [],
902
- };
903
- map.set(type, groep);
1185
+ zoekGroep(type, f);
1186
+ const sleutel = fustSleutel(type) || "onbekend";
1187
+ opgegevenAantallen.set(sleutel, (opgegevenAantallen.get(sleutel) ?? 0) + (Number(f?.aantal ?? 0) || 0));
1188
+ }
1189
+ // Producten bij hun eigen fustsoort plaatsen.
1190
+ for (const pr of producten) {
1191
+ const type = String(pr?.fustsoort ?? "").trim() || "Onbekend";
1192
+ const groep = zoekGroep(type);
1193
+ const nr = String(pr?.artikelnummer ?? "");
1194
+ const naam = String(pr?.artikelnaam ?? "");
1195
+ const bestaand = groep.producten.find((x) => x.artikelnummer === nr && x.artikelnaam === naam);
1196
+ const aantal = Number(pr?.aantal ?? 0) || 0;
1197
+ const fustAantal = Number(pr?.fust_aantal ?? 0) || 0;
1198
+ if (bestaand) {
1199
+ bestaand.aantal += aantal;
1200
+ bestaand.fust_aantal += fustAantal;
904
1201
  }
905
- groep.aantal += Number(f?.aantal ?? 0);
906
- for (const pr of producten) {
907
- const nr = String(pr?.artikelnummer ?? "");
908
- const bestaand = groep.producten.find((x) => x.artikelnummer === nr);
909
- if (bestaand) {
910
- bestaand.aantal += Number(pr?.aantal ?? 0);
911
- }
912
- else {
913
- groep.producten.push({
914
- artikelnummer: nr,
915
- artikelnaam: String(pr?.artikelnaam ?? ""),
916
- potmaat: String(pr?.potmaat ?? ""),
917
- kleur: String(pr?.kleur ?? ""),
918
- productgroep: String(pr?.productgroep ?? ""),
919
- locatie: String(pr?.locatie ?? ""),
920
- fustsoort: String(pr?.fustsoort ?? ""),
921
- planten_per_fust: Number(pr?.planten_per_fust ?? 0),
922
- aantal: Number(pr?.aantal ?? 0),
923
- });
924
- }
1202
+ else {
1203
+ groep.producten.push({
1204
+ artikelnummer: nr,
1205
+ artikelnaam: naam,
1206
+ potmaat: String(pr?.potmaat ?? ""),
1207
+ kleur: String(pr?.kleur ?? ""),
1208
+ productgroep: String(pr?.productgroep ?? ""),
1209
+ locatie: String(pr?.locatie ?? ""),
1210
+ fustsoort: String(pr?.fustsoort ?? ""),
1211
+ planten_per_fust: Number(pr?.planten_per_fust ?? 0) || 0,
1212
+ fust_aantal: fustAantal,
1213
+ aantal,
1214
+ });
925
1215
  }
926
1216
  }
927
1217
  }
928
1218
  }
929
- return [...map.values()].sort((a, b) => a.fusttype.localeCompare(b.fusttype));
1219
+ const groepen = [...map.entries()].map(([sleutel, groep]) => {
1220
+ const uitProducten = groep.producten.reduce((n, p) => n + p.fust_aantal, 0);
1221
+ const expliciet = opgegevenAantallen.get(sleutel) ?? 0;
1222
+ // Engine-opgave is leidend; anders tellen we de fusten van de producten op.
1223
+ groep.aantal = expliciet > 0 ? expliciet : uitProducten;
1224
+ return groep;
1225
+ });
1226
+ return groepen
1227
+ .filter((g) => g.producten.length > 0 || g.aantal > 0)
1228
+ .sort((a, b) => a.fusttype.localeCompare(b.fusttype));
1229
+ }
1230
+ /**
1231
+ * Voorbeeld van een deelkar (niet-volle kar): alleen een deel van de fusten,
1232
+ * zonder karindeling. Uitsluitend bedoeld voor het voorbeeld in de builder —
1233
+ * echte opdrachten krijgen hun gegevens van de Logistics Engine.
1234
+ */
1235
+ export function deelkarVoorbeeldOrder(order = DEMO_ORDER) {
1236
+ const eerste = order.karren[0];
1237
+ const laag = eerste?.lagen?.[0];
1238
+ if (!eerste || !laag)
1239
+ return order;
1240
+ return {
1241
+ ...order,
1242
+ karren: [
1243
+ {
1244
+ ...eerste,
1245
+ gedeeld: true,
1246
+ barcode: "VOORBEELD-DEELKAR",
1247
+ lagen: [{ ...laag, producten: laag.producten.slice(0, 1), fusten: laag.fusten?.slice(0, 1) ?? [] }],
1248
+ },
1249
+ ],
1250
+ };
1251
+ }
1252
+ // --- restpakbon (deelkar) ---------------------------------------------------
1253
+ /** Restregels zoals de Logistics Engine ze meestuurt onder `cart.cart_items`. */
1254
+ export function deelkarRegels(order) {
1255
+ const o = (order && typeof order === "object" ? order : {});
1256
+ const cart = (o["cart"] && typeof o["cart"] === "object" ? o["cart"] : {});
1257
+ const lijst = cart["cart_items"] ?? cart["items"];
1258
+ return Array.isArray(lijst) ? lijst : [];
1259
+ }
1260
+ const tekstWaarde = (waarde) => waarde === undefined || waarde === null ? "" : String(waarde).trim();
1261
+ /**
1262
+ * Beperkt een order tot de restregels van één deelkar. Alleen wat de Engine
1263
+ * als `cart.cart_items` meestuurt komt op de restpakbon te staan; nooit de
1264
+ * volledige karinhoud. Wordt gedeeld door voorbeeld, PDF en print, zodat de
1265
+ * drie weergaven niet uiteen kunnen lopen.
1266
+ */
1267
+ export function beperkTotDeelkar(order) {
1268
+ const regels = deelkarRegels(order);
1269
+ if (!regels.length)
1270
+ return order;
1271
+ const o = (order && typeof order === "object" ? order : {});
1272
+ const cart = (o["cart"] && typeof o["cart"] === "object" ? o["cart"] : {});
1273
+ const karren = Array.isArray(o["karren"]) ? o["karren"] : [];
1274
+ const cartId = tekstWaarde(cart["cart_id"]);
1275
+ const basisKar = (cartId ? karren.find((k) => tekstWaarde(k["cart_id"]) === cartId) : undefined) ??
1276
+ karren[0] ??
1277
+ {};
1278
+ const producten = regels.map((raw) => {
1279
+ const item = raw;
1280
+ return {
1281
+ artikelnummer: tekstWaarde(item["artikelnummer"] ?? item["artikelcode"]),
1282
+ artikelnaam: tekstWaarde(item["artikelnaam"] ?? item["product_name"] ?? item["omschrijving"]),
1283
+ potmaat: tekstWaarde(item["potmaat"]),
1284
+ aantal: Number(item["aantal"] ?? item["quantity"] ?? 0) || 0,
1285
+ belading: tekstWaarde(item["belading"]),
1286
+ barcode: tekstWaarde(item["barcode"]),
1287
+ opmerking: tekstWaarde(item["opmerking"]),
1288
+ locatie: tekstWaarde(item["locatie"]),
1289
+ gewicht: tekstWaarde(item["gewicht"]),
1290
+ productgroep: tekstWaarde(item["productgroep"]),
1291
+ kleur: tekstWaarde(item["kleur"]),
1292
+ fust_aantal: Number(item["fust_aantal"] ?? item["aantal_fusten"] ?? 0) || 0,
1293
+ fustsoort: tekstWaarde(item["fustsoort"] ?? item["fusttype"]),
1294
+ planten_per_fust: Number(item["planten_per_fust"] ?? 0) || 0,
1295
+ };
1296
+ });
1297
+ // Fusten per soort optellen uit dezelfde restregels, zodat de verzamellijst
1298
+ // en de totalen nooit meer tonen dan er nog verzameld moet worden.
1299
+ const fustMap = new Map();
1300
+ for (const pr of producten) {
1301
+ const sleutel = pr.fustsoort.toLowerCase() || "onbekend";
1302
+ const bestaand = fustMap.get(sleutel);
1303
+ if (bestaand)
1304
+ bestaand.aantal += pr.fust_aantal;
1305
+ else
1306
+ fustMap.set(sleutel, {
1307
+ fusttype: pr.fustsoort || "Onbekend",
1308
+ aantal: pr.fust_aantal,
1309
+ hoogte: "",
1310
+ belading: pr.belading,
1311
+ barcode: "",
1312
+ });
1313
+ }
1314
+ const laag = {
1315
+ laagnummer: 1,
1316
+ belading: "",
1317
+ potmaat: "",
1318
+ gewicht: "",
1319
+ opmerking: "",
1320
+ producten,
1321
+ fusten: [...fustMap.values()].filter((f) => f.aantal > 0),
1322
+ };
1323
+ return {
1324
+ ...o,
1325
+ karren: [{ ...basisKar, lagen: [laag] }],
1326
+ };
930
1327
  }