@flowselections/logistiek-engine 1.0.53 → 1.0.90

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 (168) hide show
  1. package/README.md +24 -24
  2. package/dist-lib/components/AfrondWachtrijCard.d.ts +2 -0
  3. package/dist-lib/components/AfrondWachtrijCard.d.ts.map +1 -0
  4. package/dist-lib/components/AfrondWachtrijCard.js +27 -0
  5. package/dist-lib/components/BeladingsafwijkingenCard.d.ts +2 -0
  6. package/dist-lib/components/BeladingsafwijkingenCard.d.ts.map +1 -0
  7. package/dist-lib/components/BeladingsafwijkingenCard.js +44 -0
  8. package/dist-lib/components/FlowCanvas.d.ts +6 -0
  9. package/dist-lib/components/FlowCanvas.d.ts.map +1 -1
  10. package/dist-lib/components/FlowCanvas.js +79 -18
  11. package/dist-lib/components/FlowOrders.d.ts.map +1 -1
  12. package/dist-lib/components/FlowOrders.js +59 -13
  13. package/dist-lib/components/FlowPage.d.ts.map +1 -1
  14. package/dist-lib/components/FlowPage.js +175 -6
  15. package/dist-lib/components/GeblokkeerdeOrdersCard.d.ts.map +1 -1
  16. package/dist-lib/components/GeblokkeerdeOrdersCard.js +21 -2
  17. package/dist-lib/components/KardekkingCard.d.ts +2 -0
  18. package/dist-lib/components/KardekkingCard.d.ts.map +1 -0
  19. package/dist-lib/components/KardekkingCard.js +29 -0
  20. package/dist-lib/components/LogistiekEnginePage.d.ts.map +1 -1
  21. package/dist-lib/components/LogistiekEnginePage.js +7 -1
  22. package/dist-lib/components/LogistiekEngineQueuePage.d.ts.map +1 -1
  23. package/dist-lib/components/LogistiekEngineQueuePage.js +4 -2
  24. package/dist-lib/components/NodeTracePanel.d.ts +4 -1
  25. package/dist-lib/components/NodeTracePanel.d.ts.map +1 -1
  26. package/dist-lib/components/NodeTracePanel.js +8 -2
  27. package/dist-lib/components/OrderStopBanner.d.ts +9 -2
  28. package/dist-lib/components/OrderStopBanner.d.ts.map +1 -1
  29. package/dist-lib/components/OrderStopBanner.js +27 -13
  30. package/dist-lib/components/OrderTraceDetails.d.ts.map +1 -1
  31. package/dist-lib/components/OrderTraceDetails.js +2 -1
  32. package/dist-lib/components/PlanWachtrijPage.d.ts +2 -0
  33. package/dist-lib/components/PlanWachtrijPage.d.ts.map +1 -0
  34. package/dist-lib/components/PlanWachtrijPage.js +99 -0
  35. package/dist-lib/components/PlannerDiffCard.d.ts +2 -0
  36. package/dist-lib/components/PlannerDiffCard.d.ts.map +1 -0
  37. package/dist-lib/components/PlannerDiffCard.js +25 -0
  38. package/dist-lib/components/RegiePage.d.ts.map +1 -1
  39. package/dist-lib/components/RegiePage.js +21 -13
  40. package/dist-lib/components/TaakGezondheidCard.d.ts +2 -0
  41. package/dist-lib/components/TaakGezondheidCard.d.ts.map +1 -0
  42. package/dist-lib/components/TaakGezondheidCard.js +31 -0
  43. package/dist-lib/hooks/useAfrondWachtrij.d.ts +28 -0
  44. package/dist-lib/hooks/useAfrondWachtrij.d.ts.map +1 -0
  45. package/dist-lib/hooks/useAfrondWachtrij.js +88 -0
  46. package/dist-lib/hooks/useBeladingsafwijkingen.d.ts +39 -0
  47. package/dist-lib/hooks/useBeladingsafwijkingen.d.ts.map +1 -0
  48. package/dist-lib/hooks/useBeladingsafwijkingen.js +119 -0
  49. package/dist-lib/hooks/useGeblokkeerdeOrders.d.ts +7 -0
  50. package/dist-lib/hooks/useGeblokkeerdeOrders.d.ts.map +1 -1
  51. package/dist-lib/hooks/useGeblokkeerdeOrders.js +28 -2
  52. package/dist-lib/hooks/useLogisticsTrace.js +1 -1
  53. package/dist-lib/hooks/useOrderTrace.js +1 -1
  54. package/dist-lib/hooks/useOrderVersies.d.ts.map +1 -1
  55. package/dist-lib/hooks/useOrderVersies.js +17 -3
  56. package/dist-lib/hooks/usePlanWachtrij.d.ts +33 -0
  57. package/dist-lib/hooks/usePlanWachtrij.d.ts.map +1 -0
  58. package/dist-lib/hooks/usePlanWachtrij.js +104 -0
  59. package/dist-lib/hooks/useRecalcJob.js +1 -1
  60. package/dist-lib/hooks/useShippingNoteDiagnose.d.ts +20 -0
  61. package/dist-lib/hooks/useShippingNoteDiagnose.d.ts.map +1 -0
  62. package/dist-lib/hooks/useShippingNoteDiagnose.js +52 -0
  63. package/dist-lib/hooks/useTaakGezondheid.d.ts +16 -0
  64. package/dist-lib/hooks/useTaakGezondheid.d.ts.map +1 -0
  65. package/dist-lib/hooks/useTaakGezondheid.js +43 -0
  66. package/dist-lib/hooks/useWerktijden.js +1 -1
  67. package/dist-lib/hooks/useWerkwijzeStatus.js +1 -1
  68. package/dist-lib/index.d.ts +10 -0
  69. package/dist-lib/index.d.ts.map +1 -1
  70. package/dist-lib/index.js +10 -0
  71. package/dist-lib/integrations/supabase/auth-middleware.d.ts +3147 -159
  72. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  73. package/dist-lib/integrations/supabase/client.server.d.ts +3147 -159
  74. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  75. package/dist-lib/integrations/supabase/client.server.js +1 -1
  76. package/dist-lib/integrations/supabase/moduleClient.d.ts +3147 -159
  77. package/dist-lib/integrations/supabase/moduleClient.d.ts.map +1 -1
  78. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +1 -1
  79. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -1
  80. package/dist-lib/integrations/supabase/previewAuthStorage.js +8 -1
  81. package/dist-lib/integrations/supabase/types.d.ts +3183 -161
  82. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  83. package/dist-lib/lib/attachAuthToken.js +1 -1
  84. package/dist-lib/lib/bufferDecision.test.d.ts +2 -0
  85. package/dist-lib/lib/bufferDecision.test.d.ts.map +1 -0
  86. package/dist-lib/lib/bufferDecision.test.js +104 -0
  87. package/dist-lib/lib/cartCoverage.functions.d.ts +30820 -0
  88. package/dist-lib/lib/cartCoverage.functions.d.ts.map +1 -0
  89. package/dist-lib/lib/cartCoverage.functions.js +20 -0
  90. package/dist-lib/lib/floridayDocuments.functions.d.ts +8979 -15
  91. package/dist-lib/lib/floridayDocuments.functions.d.ts.map +1 -1
  92. package/dist-lib/lib/floridayDocuments.functions.js +5 -5
  93. package/dist-lib/lib/floridayPush.functions.d.ts +3147 -159
  94. package/dist-lib/lib/floridayPush.functions.d.ts.map +1 -1
  95. package/dist-lib/lib/floridayPush.functions.js +3 -3
  96. package/dist-lib/lib/flowJourney.d.ts +40 -0
  97. package/dist-lib/lib/flowJourney.d.ts.map +1 -0
  98. package/dist-lib/lib/flowJourney.js +156 -0
  99. package/dist-lib/lib/flowNodes.d.ts +3 -0
  100. package/dist-lib/lib/flowNodes.d.ts.map +1 -1
  101. package/dist-lib/lib/flowNodes.js +6 -0
  102. package/dist-lib/lib/flowRoute.d.ts +11 -1
  103. package/dist-lib/lib/flowRoute.d.ts.map +1 -1
  104. package/dist-lib/lib/flowRoute.js +343 -40
  105. package/dist-lib/lib/flowTree.d.ts +86 -1
  106. package/dist-lib/lib/flowTree.d.ts.map +1 -1
  107. package/dist-lib/lib/flowTree.js +391 -2
  108. package/dist-lib/lib/foutTekst.d.ts +4 -0
  109. package/dist-lib/lib/foutTekst.d.ts.map +1 -0
  110. package/dist-lib/lib/foutTekst.js +43 -0
  111. package/dist-lib/lib/handlers/logistics-tick.server.d.ts +2 -0
  112. package/dist-lib/lib/handlers/logistics-tick.server.d.ts.map +1 -1
  113. package/dist-lib/lib/handlers/logistics-tick.server.js +56 -0
  114. package/dist-lib/lib/handlers/logistics-tick.test.d.ts +2 -0
  115. package/dist-lib/lib/handlers/logistics-tick.test.d.ts.map +1 -0
  116. package/dist-lib/lib/handlers/logistics-tick.test.js +63 -0
  117. package/dist-lib/lib/loadCarrierPlanning.functions.d.ts +14965 -25
  118. package/dist-lib/lib/loadCarrierPlanning.functions.d.ts.map +1 -1
  119. package/dist-lib/lib/loadCarrierPlanning.functions.js +7 -7
  120. package/dist-lib/lib/loadCarrierPlanning.server.d.ts +24 -12
  121. package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -1
  122. package/dist-lib/lib/loadCarrierPlanning.server.js +490 -211
  123. package/dist-lib/lib/logisticsRecalcJob.functions.d.ts +5986 -10
  124. package/dist-lib/lib/logisticsRecalcJob.functions.d.ts.map +1 -1
  125. package/dist-lib/lib/logisticsRecalcJob.functions.js +4 -4
  126. package/dist-lib/lib/logisticsRecalcJob.server.d.ts.map +1 -1
  127. package/dist-lib/lib/logisticsRecalcJob.server.js +17 -2
  128. package/dist-lib/lib/logisticsRecalculate.functions.d.ts +3147 -159
  129. package/dist-lib/lib/logisticsRecalculate.functions.d.ts.map +1 -1
  130. package/dist-lib/lib/logisticsRecalculate.functions.js +2 -2
  131. package/dist-lib/lib/modulePrompts.d.ts.map +1 -1
  132. package/dist-lib/lib/modulePrompts.js +3436 -3325
  133. package/dist-lib/lib/nodeTrace.d.ts +4 -0
  134. package/dist-lib/lib/nodeTrace.d.ts.map +1 -1
  135. package/dist-lib/lib/nodeTrace.js +6 -15
  136. package/dist-lib/lib/planQueue.server.d.ts.map +1 -1
  137. package/dist-lib/lib/planQueue.server.js +24 -3
  138. package/dist-lib/lib/plannerDiff.d.ts +37 -0
  139. package/dist-lib/lib/plannerDiff.d.ts.map +1 -0
  140. package/dist-lib/lib/plannerDiff.functions.d.ts +30821 -0
  141. package/dist-lib/lib/plannerDiff.functions.d.ts.map +1 -0
  142. package/dist-lib/lib/plannerDiff.functions.js +21 -0
  143. package/dist-lib/lib/plannerDiff.js +74 -0
  144. package/dist-lib/lib/plannerDiff.server.d.ts +5 -0
  145. package/dist-lib/lib/plannerDiff.server.d.ts.map +1 -0
  146. package/dist-lib/lib/plannerDiff.server.js +60 -0
  147. package/dist-lib/lib/plannerDiff.test.d.ts +2 -0
  148. package/dist-lib/lib/plannerDiff.test.d.ts.map +1 -0
  149. package/dist-lib/lib/plannerDiff.test.js +41 -0
  150. package/dist-lib/lib/resolveBuffer.test.d.ts +2 -0
  151. package/dist-lib/lib/resolveBuffer.test.d.ts.map +1 -0
  152. package/dist-lib/lib/resolveBuffer.test.js +38 -0
  153. package/dist-lib/lib/ruleEngineSchema.d.ts.map +1 -1
  154. package/dist-lib/lib/ruleEngineSchema.js +52 -0
  155. package/dist-lib/lib/stopReden.d.ts +43 -0
  156. package/dist-lib/lib/stopReden.d.ts.map +1 -0
  157. package/dist-lib/lib/stopReden.js +197 -0
  158. package/dist-lib/lib/taskCounts.d.ts +2 -0
  159. package/dist-lib/lib/taskCounts.d.ts.map +1 -0
  160. package/dist-lib/lib/taskCounts.js +15 -0
  161. package/dist-lib/lib/taskCounts.test.d.ts +2 -0
  162. package/dist-lib/lib/taskCounts.test.d.ts.map +1 -0
  163. package/dist-lib/lib/taskCounts.test.js +12 -0
  164. package/dist-lib/lib/trace.server.d.ts.map +1 -1
  165. package/dist-lib/lib/trace.server.js +3 -0
  166. package/dist-lib/lib/vakUitleg.d.ts.map +1 -1
  167. package/dist-lib/lib/vakUitleg.js +2 -1
  168. package/package.json +12 -9
@@ -119,7 +119,7 @@ function weakest(a, b) {
119
119
  return rank[a] >= rank[b] ? a : b;
120
120
  }
121
121
  /** Bouwt de complete beslisboom van binnenkomst tot afgehandelde order. */
122
- export function buildFlowTree(settings) {
122
+ export function buildFlowTree(settings, overrides) {
123
123
  const veilig = settings && typeof settings === "object" ? settings : {};
124
124
  const nodes = [];
125
125
  const edges = [];
@@ -274,6 +274,32 @@ export function buildFlowTree(settings) {
274
274
  decisionIds,
275
275
  pendingRefs,
276
276
  };
277
+ // ── Klaar om te picken ───────────────────────────────────────────────────
278
+ // Vlak vóór het pickscherm: pas doorlopen als alle documenten die volgens
279
+ // de instellingen nodig zijn ook echt klaarstaan. Documenten die deze order
280
+ // niet nodig heeft, tellen meteen als klaar.
281
+ if (step.id === "pickstations") {
282
+ add({
283
+ id: "stap:klaar_om_te_picken",
284
+ kind: "step",
285
+ label: "Klaar om te picken",
286
+ sublabel: "staan alle benodigde documenten klaar?",
287
+ state: "active",
288
+ lines: [
289
+ "Pakbon: elke kar heeft een barcode",
290
+ "Verzendbrief: alleen als die samen met de pakbon geprint wordt",
291
+ "Stickers: alleen als deze order stickers heeft",
292
+ "Niet nodig telt meteen als klaar",
293
+ ],
294
+ help: [
295
+ "De order komt pas op het pickscherm als alle verplichte documenten van deze order klaarstaan.",
296
+ "Ontbreekt er iets, dan wacht de order hier met vermelding van wat er nog mist.",
297
+ "Kan de controle niet worden uitgevoerd, dan blijft de order bewust wachten.",
298
+ ],
299
+ });
300
+ link(tail, "stap:klaar_om_te_picken", "active");
301
+ tail = "stap:klaar_om_te_picken";
302
+ }
277
303
  const stepNode = add({
278
304
  id: `stap:${step.id}`,
279
305
  kind: "step",
@@ -293,6 +319,45 @@ export function buildFlowTree(settings) {
293
319
  fan(ctx, stepNode.id, "active", roots, [], stapMerge);
294
320
  nodes.push({ id: stapMerge, kind: "merge", label: "", state: "active", ancestors: [], help: [], w: NODE_SIZE.merge.w, h: NODE_SIZE.merge.h });
295
321
  tail = stapMerge;
322
+ // ── Terugmelding uit de pickstraat: geprint en afgemeld ──────────────────
323
+ // Twee eigen vakjes, zodat in één oogopslag zichtbaar is of de pakbonnen
324
+ // echt uit de printer zijn gekomen en of de karren zijn afgemeld.
325
+ if (step.id === "pickstations") {
326
+ add({
327
+ id: "stap:pakbon_geprint",
328
+ kind: "step",
329
+ label: "Pakbon geprint",
330
+ sublabel: "is de pakbon echt uit de printer gekomen?",
331
+ state: "active",
332
+ lines: [
333
+ "Ja: er staat een printmoment vastgelegd op de pakbonregel",
334
+ "Nee: er is nog niets geprint voor deze order",
335
+ "Printen verandert de status van de order niet",
336
+ ],
337
+ help: [
338
+ "Dit vakje laat alleen zien of de pakbonnen en verzendbrieven van deze order geprint zijn, en hoeveel van het totaal.",
339
+ "Het printmoment komt uit de pickstraat zelf; er wordt hier niets geprint en geen status gewijzigd.",
340
+ ],
341
+ });
342
+ link(tail, "stap:pakbon_geprint", "active");
343
+ add({
344
+ id: "stap:kar_afgemeld",
345
+ kind: "step",
346
+ label: "Kar afgemeld",
347
+ sublabel: "is de kar afgemeld op het pickscherm?",
348
+ state: "active",
349
+ lines: [
350
+ "Ja: alle pakbonregels van de order zijn afgerond",
351
+ "Nee: er staan nog karren open op het pickscherm",
352
+ ],
353
+ help: [
354
+ "Dit vakje laat zien of de karren van deze order zijn afgemeld, met de stand (bijvoorbeeld 0 van 2).",
355
+ "Zolang er nog een kar openstaat, wacht de order hier; er wordt niets afgemeld vanuit dit vakje.",
356
+ ],
357
+ });
358
+ link("stap:pakbon_geprint", "stap:kar_afgemeld", "active");
359
+ tail = "stap:kar_afgemeld";
360
+ }
296
361
  });
297
362
  // ── Einde ──────────────────────────────────────────────────────────────────
298
363
  add({
@@ -320,7 +385,210 @@ export function buildFlowTree(settings) {
320
385
  : undefined;
321
386
  link(from, ref.toNode, ref.state, label, ref.ancestors, "ref");
322
387
  }
323
- return { nodes, edges };
388
+ // Ruitjes met een eigen voorwaarde blijven staan: die hebben betekenis.
389
+ const behoud = new Set(Object.entries(overrides ?? {})
390
+ .filter(([, k]) => k && ((k.wanneer ?? "een") !== "een" || k.verwijderd))
391
+ .map(([id]) => id));
392
+ return vereenvoudigMerges({ nodes, edges }, behoud);
393
+ }
394
+ /** Standaard vangnet: na een half uur gaat de flow alsnog verder. */
395
+ export const MERGE_MAX_WACHT_MINUTEN = 30;
396
+ function lijnNaam(tree, edge) {
397
+ const n = tree.nodes.find((x) => x.id === edge.from);
398
+ const basis = n?.label ? n.label : n?.kind === "merge" ? "samenkomst" : edge.from;
399
+ return edge.label ? `${basis} — ${edge.label}` : basis;
400
+ }
401
+ /**
402
+ * De voorwaarde van één samenkomstpunt, met de lijnen die meetellen.
403
+ * Lijnen die volgens de huidige werkwijze uitgeschakeld zijn of op "Stopt hier"
404
+ * staan tellen nooit mee: een tak die nooit gelopen kan worden mag de flow niet
405
+ * tegenhouden.
406
+ */
407
+ export function mergeVoorwaarde(tree, mergeId, override) {
408
+ const standen = override?.lijnen ?? {};
409
+ const lijnen = (Array.isArray(tree?.edges) ? tree.edges : [])
410
+ .filter((e) => e.kind === "flow" && e.to === mergeId)
411
+ .map((e) => ({
412
+ id: e.id,
413
+ from: e.from,
414
+ naam: lijnNaam(tree, e),
415
+ telt: e.state !== "disabled" && standen[e.id] !== "stop",
416
+ }));
417
+ // Sluiten de meetellende lijnen elkaar uit? Dat is zo wanneer ze onder
418
+ // verschillende takken van dezelfde bovenliggende keuze (of verschillende
419
+ // herkomsten) hangen: geen enkele order loopt via meer dan één van die takken.
420
+ const meetellend = lijnen.filter((l) => l.telt);
421
+ let uitsluitend = false;
422
+ if (meetellend.length >= 2) {
423
+ const knopen = Array.isArray(tree?.nodes) ? tree.nodes : [];
424
+ const eigenaar = mergeId === "__bron_merge"
425
+ ? "__bron"
426
+ : mergeId.startsWith("samen:") && !mergeId.startsWith("samen:stap:")
427
+ ? (knopen.find((n) => n.kind === "decision" && n.mergeId === mergeId)?.id ?? null)
428
+ : null;
429
+ if (eigenaar) {
430
+ const naar = new Map();
431
+ for (const e of Array.isArray(tree?.edges) ? tree.edges : []) {
432
+ if (e.kind !== "flow")
433
+ continue;
434
+ const lijst = naar.get(e.to);
435
+ if (lijst)
436
+ lijst.push(e.from);
437
+ else
438
+ naar.set(e.to, [e.from]);
439
+ }
440
+ // Welke directe takken van de eigenaar kunnen dit vakje bereiken?
441
+ const takkenVoor = (van) => {
442
+ const takken = new Set();
443
+ const gezien = new Set([van]);
444
+ const rij = [van];
445
+ while (rij.length > 0) {
446
+ const cur = rij.shift();
447
+ for (const vorige of naar.get(cur) ?? []) {
448
+ if (vorige === eigenaar) {
449
+ takken.add(cur);
450
+ continue;
451
+ }
452
+ if (gezien.has(vorige))
453
+ continue;
454
+ gezien.add(vorige);
455
+ rij.push(vorige);
456
+ }
457
+ }
458
+ return takken;
459
+ };
460
+ const sets = meetellend.map((l) => takkenVoor(l.from));
461
+ // Alleen concluderen als elke lijn onder de eigenaar hangt.
462
+ if (sets.every((s) => s.size > 0)) {
463
+ const gemeenschappelijk = sets.reduce((a, b) => {
464
+ const over = new Set();
465
+ for (const x of a)
466
+ if (b.has(x))
467
+ over.add(x);
468
+ return over;
469
+ });
470
+ uitsluitend = gemeenschappelijk.size === 0;
471
+ }
472
+ }
473
+ }
474
+ const gevraagd = override?.wanneer ?? "een";
475
+ const verplicht = override?.verplichteLijn ?? null;
476
+ const geldig = verplicht ? lijnen.find((l) => l.id === verplicht && l.telt) : undefined;
477
+ const terugval = (gevraagd === "specifiek" && !geldig) || (gevraagd === "alle" && uitsluitend);
478
+ const wanneer = terugval ? "een" : gevraagd;
479
+ const minuten = Number(override?.maxWachtMinuten);
480
+ const maxWachtMinuten = Number.isFinite(minuten) && minuten > 0 ? Math.round(minuten) : MERGE_MAX_WACHT_MINUTEN;
481
+ const label = wanneer === "alle" ? "alle lijnen" : wanneer === "specifiek" ? (geldig?.naam ?? "één lijn") : "één lijn";
482
+ return {
483
+ wanneer,
484
+ lijnen,
485
+ verplichteLijn: wanneer === "specifiek" ? (geldig?.id ?? null) : null,
486
+ maxWachtMinuten,
487
+ terugval,
488
+ uitsluitend,
489
+ label,
490
+ };
491
+ }
492
+ /** Ruitjes die de kaart altijd nodig heeft om de route heel te houden. */
493
+ function vastRuitje(id) {
494
+ return id.startsWith("samen:stap:") || id === "__bron_merge";
495
+ }
496
+ /** Eén ruitje uit de kaart halen: inkomende en uitgaande lijnen aan elkaar knopen. */
497
+ function haalMergeWeg(tree, mergeId, lijnen) {
498
+ const node = tree.nodes.find((n) => n.id === mergeId);
499
+ if (!node || node.kind !== "merge")
500
+ return tree;
501
+ const inkomend = tree.edges.filter((e) => e.kind === "flow" && e.to === mergeId);
502
+ const uitgaand = tree.edges.filter((e) => e.kind === "flow" && e.from === mergeId);
503
+ const rest = tree.edges.filter((e) => e.from !== mergeId && e.to !== mergeId);
504
+ const nieuw = [];
505
+ for (const i of inkomend) {
506
+ if (lijnen[i.id] === "stop")
507
+ continue;
508
+ for (const u of uitgaand) {
509
+ const label = i.label ?? u.label;
510
+ const id = `flow:${i.from}->${u.to}:${label ?? ""}`;
511
+ if (nieuw.some((e) => e.id === id) || rest.some((e) => e.id === id))
512
+ continue;
513
+ nieuw.push({
514
+ id,
515
+ from: i.from,
516
+ to: u.to,
517
+ label,
518
+ state: weakest(i.state, u.state),
519
+ kind: "flow",
520
+ ancestors: i.ancestors,
521
+ });
522
+ }
523
+ }
524
+ const vervanger = uitgaand[0]?.to;
525
+ const nodes = tree.nodes
526
+ .filter((n) => n.id !== mergeId)
527
+ .map((n) => (n.mergeId === mergeId ? { ...n, mergeId: vervanger } : n));
528
+ return { nodes, edges: [...rest, ...nieuw] };
529
+ }
530
+ /**
531
+ * Overbodige ruitjes wegrekenen: een ruitje met één lijn erin en één lijn eruit
532
+ * voegt niets toe, en een ruitje waar niets meer op uitkomt hoort er niet te
533
+ * staan. De vaste verzamelpunten per stap blijven altijd staan.
534
+ */
535
+ export function vereenvoudigMerges(tree, behoud) {
536
+ let huidig = { nodes: [...(tree?.nodes ?? [])], edges: [...(tree?.edges ?? [])] };
537
+ for (let ronde = 0; ronde < 20; ronde++) {
538
+ const kandidaat = huidig.nodes.find((n) => {
539
+ if (n.kind !== "merge" || vastRuitje(n.id) || behoud?.has(n.id))
540
+ return false;
541
+ const inkomend = huidig.edges.filter((e) => e.kind === "flow" && e.to === n.id).length;
542
+ const uitgaand = huidig.edges.filter((e) => e.kind === "flow" && e.from === n.id).length;
543
+ return inkomend <= 1 && uitgaand <= 1;
544
+ });
545
+ if (!kandidaat)
546
+ break;
547
+ huidig = haalMergeWeg(huidig, kandidaat.id, {});
548
+ }
549
+ return huidig;
550
+ }
551
+ /** Handmatige keuzes van de gebruiker op de ruitjes toepassen. */
552
+ export function pasMergeOverridesToe(tree, overrides) {
553
+ if (!overrides || typeof overrides !== "object")
554
+ return tree;
555
+ let huidig = { nodes: [...(tree?.nodes ?? [])], edges: [...(tree?.edges ?? [])] };
556
+ for (const [mergeId, keuze] of Object.entries(overrides)) {
557
+ if (!keuze || typeof keuze !== "object")
558
+ continue;
559
+ const lijnen = keuze.lijnen ?? {};
560
+ if (keuze.verwijderd) {
561
+ if (vastRuitje(mergeId))
562
+ continue;
563
+ huidig = haalMergeWeg(huidig, mergeId, lijnen);
564
+ continue;
565
+ }
566
+ // Ruitje blijft staan: gesloten lijnen lopen niet door.
567
+ huidig = {
568
+ nodes: huidig.nodes,
569
+ edges: huidig.edges.map((e) => e.to === mergeId && lijnen[e.id] === "stop" ? { ...e, state: "disabled" } : e),
570
+ };
571
+ }
572
+ // Voorwaarde per ruitje op de knoop zetten, zodat de kaart en de route met
573
+ // dezelfde gegevens werken.
574
+ const metWacht = huidig.nodes.map((n) => {
575
+ if (n.kind !== "merge")
576
+ return n;
577
+ const keuze = overrides[n.id];
578
+ if (!keuze || (keuze.wanneer ?? "een") === "een")
579
+ return n.wacht ? { ...n, wacht: undefined } : n;
580
+ const v = mergeVoorwaarde(huidig, n.id, keuze);
581
+ if (v.wanneer === "een")
582
+ return n.wacht ? { ...n, wacht: undefined } : n;
583
+ const wacht = {
584
+ wanneer: v.wanneer,
585
+ verplichteLijn: v.verplichteLijn,
586
+ maxWachtMinuten: v.maxWachtMinuten,
587
+ label: v.label,
588
+ };
589
+ return { ...n, wacht };
590
+ });
591
+ return { nodes: metWacht, edges: huidig.edges };
324
592
  }
325
593
  /**
326
594
  * Waaiert een lijst velden uit vanaf `from`: elke beslissing wordt een eigen arm
@@ -517,3 +785,124 @@ function fan(ctx, from, fromState, items, ancestors, mergeId) {
517
785
  if (armen === 0)
518
786
  push({ from, to: mergeId, state: fromState, ancestors });
519
787
  }
788
+ /**
789
+ * Zonder geselecteerde order: vakjes waar de route ophoudt krijgen geen
790
+ * doorlopende lijnen meer.
791
+ *
792
+ * Eindstation is:
793
+ * - een vakje waarvan élk vervolg uitgeschakeld is, en
794
+ * - een uitkomst-tegel (`option`): daar hangt per definitie niets meer onder.
795
+ *
796
+ * Het samenkomstpunt van een beslissing waar daarna niets meer op uitkomt,
797
+ * verdwijnt mee. De doorgaande route tussen de stappen blijft altijd heel.
798
+ */
799
+ export function verbergEindnodeLijnen(tree) {
800
+ const nodes = Array.isArray(tree?.nodes) ? tree.nodes : [];
801
+ const edges = Array.isArray(tree?.edges) ? tree.edges : [];
802
+ const byId = new Map(nodes.map((n) => [n.id, n]));
803
+ const uit = new Map();
804
+ for (const e of edges) {
805
+ if (e.kind !== "flow")
806
+ continue;
807
+ const doel = byId.get(e.to);
808
+ const open = e.state !== "disabled" && doel !== undefined && doel.state !== "disabled";
809
+ const lijst = uit.get(e.from);
810
+ if (lijst)
811
+ lijst.push({ edge: e, open });
812
+ else
813
+ uit.set(e.from, [{ edge: e, open }]);
814
+ }
815
+ const eindstations = new Set();
816
+ for (const n of nodes) {
817
+ if (n.kind === "option")
818
+ eindstations.add(n.id);
819
+ }
820
+ for (const [id, lijst] of uit) {
821
+ const bron = byId.get(id);
822
+ if (!bron || bron.state === "disabled")
823
+ continue;
824
+ if (lijst.length > 0 && lijst.every((l) => !l.open))
825
+ eindstations.add(id);
826
+ }
827
+ if (eindstations.size === 0)
828
+ return { nodes, edges };
829
+ const weg = new Set();
830
+ for (const e of edges) {
831
+ if (e.kind === "flow" && eindstations.has(e.from))
832
+ weg.add(e.id);
833
+ }
834
+ // Beslissingspunten waar niets meer op uitkomt, lopen ook niet meer verder.
835
+ const beslisMerge = (id) => id.startsWith("samen:") && !id.startsWith("samen:stap:");
836
+ for (let ronde = 0; ronde < 10; ronde++) {
837
+ const binnen = new Set();
838
+ for (const e of edges) {
839
+ if (e.kind === "flow" && !weg.has(e.id))
840
+ binnen.add(e.to);
841
+ }
842
+ let gewijzigd = false;
843
+ for (const e of edges) {
844
+ if (e.kind !== "flow" || weg.has(e.id))
845
+ continue;
846
+ if (beslisMerge(e.from) && !binnen.has(e.from)) {
847
+ weg.add(e.id);
848
+ gewijzigd = true;
849
+ }
850
+ }
851
+ if (!gewijzigd)
852
+ break;
853
+ }
854
+ // Waarborg: elk stap-samenkomstpunt houdt minstens één inkomende lijn, zodat
855
+ // de doorgaande route tussen de stappen nooit onderbroken raakt.
856
+ for (const n of nodes) {
857
+ if (!n.id.startsWith("samen:stap:"))
858
+ continue;
859
+ const inkomend = edges.filter((e) => e.kind === "flow" && e.to === n.id);
860
+ if (inkomend.length === 0 || inkomend.some((e) => !weg.has(e.id)))
861
+ continue;
862
+ const herstel = inkomend.filter((e) => e.state === "active");
863
+ for (const e of herstel.length > 0 ? herstel : inkomend)
864
+ weg.delete(e.id);
865
+ }
866
+ const nieuweEdges = edges.filter((e) => !weg.has(e.id));
867
+ const gebruikt = new Set();
868
+ for (const e of nieuweEdges) {
869
+ gebruikt.add(e.from);
870
+ gebruikt.add(e.to);
871
+ }
872
+ return {
873
+ nodes: nodes.filter((n) => !(beslisMerge(n.id) && !gebruikt.has(n.id))),
874
+ edges: nieuweEdges,
875
+ };
876
+ }
877
+ /**
878
+ * Het vakje "Verzendbrief" per herkomst juist tonen.
879
+ *
880
+ * Bij een Floriday-order geldt uitsluitend het officiele Floriday-document;
881
+ * de regel over de eigen layout hoort daar niet te staan. Alleen tekst: de
882
+ * kaartstructuur, instellingen en route blijven ongewijzigd.
883
+ */
884
+ export function verduidelijkVerzendbrief(tree, source) {
885
+ const nodes = Array.isArray(tree?.nodes) ? tree.nodes : [];
886
+ const edges = Array.isArray(tree?.edges) ? tree.edges : [];
887
+ const isFloriday = source === "floriday";
888
+ const aangepast = nodes.map((n) => {
889
+ if (!n.id.includes(":verzendbrief:") && !n.id.endsWith(":verzendbrief"))
890
+ return n;
891
+ const lines = Array.isArray(n.lines) ? n.lines : [];
892
+ const eigenLayout = (regel) => regel.toLowerCase().startsWith("verzendbrief voor bvo");
893
+ const nieuweLines = isFloriday
894
+ ? [
895
+ ...lines.filter((r) => !eigenLayout(r)),
896
+ "Floriday-order: officiele Floriday-afleverbrief (aanvragen en ophalen)",
897
+ ]
898
+ : lines.map((r) => (eigenLayout(r) ? `Alleen niet-Floriday orders — ${r}` : r));
899
+ if (nieuweLines.length === lines.length && nieuweLines.every((r, i) => r === lines[i]))
900
+ return n;
901
+ const help = [...(Array.isArray(n.help) ? n.help : [])];
902
+ help.push(isFloriday
903
+ ? "Deze order komt uit Floriday: er wordt uitsluitend de officiele Floriday-afleverbrief gebruikt. De eigen layout geldt hier niet."
904
+ : "De eigen verzendbrieflayout geldt alleen voor orders die niet uit Floriday komen.");
905
+ return { ...n, lines: nieuweLines, help };
906
+ });
907
+ return { nodes: aangepast, edges };
908
+ }
@@ -0,0 +1,4 @@
1
+ export declare function foutTekst(ruw: string | null | undefined): string | null;
2
+ /** Of de melding een technische stukloop is, of "alleen" een tijdslimiet/regel. */
3
+ export declare function foutSoort(ruw: string | null | undefined): string | null;
4
+ //# sourceMappingURL=foutTekst.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foutTekst.d.ts","sourceRoot":"","sources":["../../src/lib/foutTekst.ts"],"names":[],"mappings":"AAQA,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAwBvE;AAED,mFAAmF;AACnF,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAMvE"}
@@ -0,0 +1,43 @@
1
+ // ============================================================================
2
+ // src/lib/foutTekst.ts — technische foutmeldingen in gewone taal
3
+ // ============================================================================
4
+ // Dezelfde vertaling als in de database (logistics_plan_error_text), zodat ook
5
+ // oudere, al vastgelegde meldingen leesbaar op het scherm komen. De ruwe tekst
6
+ // blijft altijd beschikbaar onder "Technische details".
7
+ // ============================================================================
8
+ export function foutTekst(ruw) {
9
+ const t = typeof ruw === "string" ? ruw.trim() : "";
10
+ if (!t)
11
+ return null;
12
+ const l = t.toLowerCase();
13
+ if (l === "deadline") {
14
+ return "De planronde was vol voordat deze order aan de beurt was. De order schuift door naar de volgende ronde.";
15
+ }
16
+ if (l.includes("statement timeout")) {
17
+ return "Het opslaan van de karrenindeling duurde te lang en is door de database afgebroken. Er is niets half opgeslagen; de order wordt opnieuw geprobeerd.";
18
+ }
19
+ if (l.includes("order_cart_items_cart_id_fkey")) {
20
+ return "Een kar uit deze indeling bestond niet meer toen de inhoud werd weggeschreven. De indeling wordt opnieuw gemaakt.";
21
+ }
22
+ if (l.includes("order_carts_buffer_id_fkey")) {
23
+ return "De buffer waarop deze karren stonden bestaat niet meer. De order wordt los ingedeeld.";
24
+ }
25
+ if (l.includes("foreign key constraint")) {
26
+ return "Een gegeven waar de indeling naar verwees bestond niet meer op het moment van opslaan. De indeling wordt opnieuw gemaakt.";
27
+ }
28
+ if (l.includes("deadlock")) {
29
+ return "Twee bewerkingen wachtten op elkaar bij het opslaan. De order wordt opnieuw geprobeerd.";
30
+ }
31
+ return t;
32
+ }
33
+ /** Of de melding een technische stukloop is, of "alleen" een tijdslimiet/regel. */
34
+ export function foutSoort(ruw) {
35
+ const l = (ruw ?? "").toLowerCase();
36
+ if (!l.trim())
37
+ return null;
38
+ if (l === "deadline" || l.includes("statement timeout") || l.includes("deadlock"))
39
+ return "regel";
40
+ if (l.includes("grens van") || l.includes("geen enkele kar"))
41
+ return "configuratie";
42
+ return "technisch";
43
+ }
@@ -1,3 +1,5 @@
1
1
  import type { SupabaseClient } from "@supabase/supabase-js";
2
2
  export declare function handleLogisticsTick(request: Request, supabase: SupabaseClient): Promise<Response>;
3
+ /** Vaste sleutel van het rondeslot; gelijk aan de check in logistics_plan_kick(). */
4
+ export declare const TICK_ROUND_LOCK_KEY = "tick:ronde";
3
5
  //# sourceMappingURL=logistics-tick.server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"logistics-tick.server.d.ts","sourceRoot":"","sources":["../../../src/lib/handlers/logistics-tick.server.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAQ5D,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,cAAc,GACvB,OAAO,CAAC,QAAQ,CAAC,CAuRnB"}
1
+ {"version":3,"file":"logistics-tick.server.d.ts","sourceRoot":"","sources":["../../../src/lib/handlers/logistics-tick.server.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAQ5D,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,cAAc,GACvB,OAAO,CAAC,QAAQ,CAAC,CA2EnB;AAED,qFAAqF;AACrF,eAAO,MAAM,mBAAmB,eAAe,CAAC"}
@@ -39,6 +39,51 @@ export async function handleLogisticsTick(request, supabase) {
39
39
  catch {
40
40
  // Een ontbrekende hartslag mag de verwerking nooit tegenhouden.
41
41
  }
42
+ // Rondeslot: nooit twee tick-rondes tegelijk (minuut-cron én wek-aanroep
43
+ // komen hier allebei binnen). Hergebruikt het bestaande planslot met
44
+ // vervaltijd. Fail-closed: bij twijfel wordt de ronde overgeslagen; de
45
+ // volgende minuut-ronde is het vangnet.
46
+ const roundId = crypto.randomUUID();
47
+ let held = false;
48
+ let lockUnknown = false;
49
+ try {
50
+ const { data, error } = await supabase.rpc("logistics_plan_lock_acquire", {
51
+ _key: TICK_ROUND_LOCK_KEY,
52
+ _owner: roundId,
53
+ _ttl_seconds: 120,
54
+ });
55
+ if (error)
56
+ lockUnknown = true;
57
+ held = !error && data === true;
58
+ }
59
+ catch {
60
+ lockUnknown = true;
61
+ held = false;
62
+ }
63
+ if (!held) {
64
+ if (lockUnknown) {
65
+ console.warn("[logistics-tick] rondeslot kon niet worden vastgesteld; ronde overgeslagen");
66
+ }
67
+ return Response.json({ ok: true, skipped: lockUnknown ? "slot_onbekend" : "ronde_loopt_al" }, { status: 200, headers: CORS_HEADERS });
68
+ }
69
+ try {
70
+ return await runTickRound(supabase, tickJob);
71
+ }
72
+ finally {
73
+ try {
74
+ await supabase.rpc("logistics_plan_lock_release", {
75
+ _key: TICK_ROUND_LOCK_KEY,
76
+ _owner: roundId,
77
+ });
78
+ }
79
+ catch {
80
+ // Vrijgeven mag de uitkomst niet omgooien; de lease verloopt vanzelf.
81
+ }
82
+ }
83
+ }
84
+ /** Vaste sleutel van het rondeslot; gelijk aan de check in logistics_plan_kick(). */
85
+ export const TICK_ROUND_LOCK_KEY = "tick:ronde";
86
+ async function runTickRound(supabase, tickJob) {
42
87
  // Meerdere batches per aanroep, met een ruime marge tot de time-out.
43
88
  const deadline = Date.now() + 8000;
44
89
  let last = null;
@@ -72,6 +117,16 @@ export async function handleLogisticsTick(request, supabase) {
72
117
  catch (e) {
73
118
  orphans = { error: String(e?.message ?? e) };
74
119
  }
120
+ // Vangnet: recente orders die nooit in de planwachtrij kwamen alsnog
121
+ // aanmelden, zodat ze in deze zelfde ronde nog worden ingedeeld.
122
+ let reconciledOrders = null;
123
+ try {
124
+ const { data, error } = await supabase.rpc("logistics_reconcile_missing_plan_queue");
125
+ reconciledOrders = error ? { error: error.message } : data;
126
+ }
127
+ catch (e) {
128
+ reconciledOrders = { error: String(e?.message ?? e) };
129
+ }
75
130
  // Planwachtrij: nieuwe/gewijzigde orders incrementeel indelen.
76
131
  let plan = null;
77
132
  try {
@@ -243,6 +298,7 @@ export async function handleLogisticsTick(request, supabase) {
243
298
  status: last?.status ?? "idle",
244
299
  phase: last?.phase ?? null,
245
300
  processed: last?.processed ?? 0,
301
+ reconciledOrders,
246
302
  plan,
247
303
  orphans,
248
304
  bufferQueue,
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=logistics-tick.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logistics-tick.test.d.ts","sourceRoot":"","sources":["../../../src/lib/handlers/logistics-tick.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ const tickJob = vi.fn(async () => null);
3
+ const runPlanQueue = vi.fn(async () => ({}));
4
+ vi.mock("../logisticsRecalcJob.server.js", () => ({ tickJob }));
5
+ vi.mock("../planQueue.server.js", () => ({ runPlanQueue, prunePlanQueue: vi.fn() }));
6
+ import { handleLogisticsTick, TICK_ROUND_LOCK_KEY } from "./logistics-tick.server";
7
+ const KEY = "test-key";
8
+ function makeClient(acquire) {
9
+ const calls = [];
10
+ const rpc = vi.fn(async (fn, args) => {
11
+ calls.push({ fn, args });
12
+ if (fn === "logistics_plan_lock_acquire")
13
+ return acquire();
14
+ return { data: null, error: null };
15
+ });
16
+ return { client: { rpc }, calls };
17
+ }
18
+ const req = () => new Request("http://x/api/public/hooks/logistics-recalc-tick", {
19
+ method: "POST",
20
+ headers: { apikey: KEY },
21
+ });
22
+ beforeEach(() => {
23
+ process.env["SUPABASE_PUBLISHABLE_KEY"] = KEY;
24
+ tickJob.mockClear();
25
+ runPlanQueue.mockClear();
26
+ });
27
+ describe("tick-rondeslot", () => {
28
+ it("slaat een tweede ronde over als er al een ronde loopt", async () => {
29
+ const { client, calls } = makeClient(async () => ({ data: false, error: null }));
30
+ const res = await handleLogisticsTick(req(), client);
31
+ expect(await res.json()).toMatchObject({ skipped: "ronde_loopt_al" });
32
+ expect(tickJob).not.toHaveBeenCalled();
33
+ expect(runPlanQueue).not.toHaveBeenCalled();
34
+ const fns = calls.map((c) => c.fn);
35
+ expect(fns).not.toContain("logistics_buffer_detach_late_orders");
36
+ expect(fns).not.toContain("logistics_sync_pick_queue");
37
+ expect(fns).not.toContain("logistics_plan_lock_release");
38
+ });
39
+ it("slaat over (fail-closed) als het slot niet kan worden vastgesteld", async () => {
40
+ const { client } = makeClient(async () => ({ data: null, error: { message: "boom" } }));
41
+ const res = await handleLogisticsTick(req(), client);
42
+ expect(await res.json()).toMatchObject({ skipped: "slot_onbekend" });
43
+ expect(tickJob).not.toHaveBeenCalled();
44
+ expect(runPlanQueue).not.toHaveBeenCalled();
45
+ });
46
+ it("draait de ronde met slot en geeft het slot daarna vrij, ook bij een fout", async () => {
47
+ tickJob.mockImplementationOnce(async () => {
48
+ throw new Error("stap faalt");
49
+ });
50
+ const { client, calls } = makeClient(async () => ({ data: true, error: null }));
51
+ await expect(handleLogisticsTick(req(), client)).rejects.toThrow("stap faalt");
52
+ const acq = calls.find((c) => c.fn === "logistics_plan_lock_acquire");
53
+ const rel = calls.find((c) => c.fn === "logistics_plan_lock_release");
54
+ expect(rel).toBeTruthy();
55
+ expect(rel.args._key).toBe(TICK_ROUND_LOCK_KEY);
56
+ expect(rel.args._owner).toBe(acq.args._owner);
57
+ });
58
+ it("voert de planwachtrij uit als het slot is verkregen", async () => {
59
+ const { client } = makeClient(async () => ({ data: true, error: null }));
60
+ await handleLogisticsTick(req(), client);
61
+ expect(runPlanQueue).toHaveBeenCalledTimes(1);
62
+ });
63
+ });