@flowselections/logistiek-engine 1.0.34 → 1.0.89

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 (244) hide show
  1. package/dist-lib/components/AfrondWachtrijCard.d.ts +2 -0
  2. package/dist-lib/components/AfrondWachtrijCard.d.ts.map +1 -0
  3. package/dist-lib/components/AfrondWachtrijCard.js +27 -0
  4. package/dist-lib/components/BeladingsafwijkingenCard.d.ts +2 -0
  5. package/dist-lib/components/BeladingsafwijkingenCard.d.ts.map +1 -0
  6. package/dist-lib/components/BeladingsafwijkingenCard.js +44 -0
  7. package/dist-lib/components/FlowCanvas.d.ts +34 -0
  8. package/dist-lib/components/FlowCanvas.d.ts.map +1 -0
  9. package/dist-lib/components/FlowCanvas.js +490 -0
  10. package/dist-lib/components/FlowOrders.d.ts +8 -0
  11. package/dist-lib/components/FlowOrders.d.ts.map +1 -0
  12. package/dist-lib/components/FlowOrders.js +171 -0
  13. package/dist-lib/components/FlowPage.d.ts +2 -0
  14. package/dist-lib/components/FlowPage.d.ts.map +1 -0
  15. package/dist-lib/components/FlowPage.js +310 -0
  16. package/dist-lib/components/GeblokkeerdeOrdersCard.d.ts.map +1 -1
  17. package/dist-lib/components/GeblokkeerdeOrdersCard.js +21 -2
  18. package/dist-lib/components/InklapbaarVenster.d.ts +9 -0
  19. package/dist-lib/components/InklapbaarVenster.d.ts.map +1 -0
  20. package/dist-lib/components/InklapbaarVenster.js +10 -0
  21. package/dist-lib/components/KardekkingCard.d.ts +2 -0
  22. package/dist-lib/components/KardekkingCard.d.ts.map +1 -0
  23. package/dist-lib/components/KardekkingCard.js +29 -0
  24. package/dist-lib/components/LogistiekEnginePage.d.ts.map +1 -1
  25. package/dist-lib/components/LogistiekEnginePage.js +15 -3
  26. package/dist-lib/components/LogistiekEngineQueuePage.d.ts +2 -0
  27. package/dist-lib/components/LogistiekEngineQueuePage.d.ts.map +1 -0
  28. package/dist-lib/components/LogistiekEngineQueuePage.js +13 -0
  29. package/dist-lib/components/NodeTracePanel.d.ts +14 -0
  30. package/dist-lib/components/NodeTracePanel.d.ts.map +1 -0
  31. package/dist-lib/components/NodeTracePanel.js +46 -0
  32. package/dist-lib/components/OrderJourneyBar.d.ts +9 -0
  33. package/dist-lib/components/OrderJourneyBar.d.ts.map +1 -0
  34. package/dist-lib/components/OrderJourneyBar.js +26 -0
  35. package/dist-lib/components/OrderLogboek.d.ts +10 -0
  36. package/dist-lib/components/OrderLogboek.d.ts.map +1 -0
  37. package/dist-lib/components/OrderLogboek.js +92 -0
  38. package/dist-lib/components/OrderStopBanner.d.ts +12 -0
  39. package/dist-lib/components/OrderStopBanner.d.ts.map +1 -0
  40. package/dist-lib/components/OrderStopBanner.js +36 -0
  41. package/dist-lib/components/OrderTraceDetails.d.ts +22 -0
  42. package/dist-lib/components/OrderTraceDetails.d.ts.map +1 -0
  43. package/dist-lib/components/OrderTraceDetails.js +88 -0
  44. package/dist-lib/components/PlanWachtrijPage.d.ts +2 -0
  45. package/dist-lib/components/PlanWachtrijPage.d.ts.map +1 -0
  46. package/dist-lib/components/PlanWachtrijPage.js +99 -0
  47. package/dist-lib/components/PlannerDiffCard.d.ts +2 -0
  48. package/dist-lib/components/PlannerDiffCard.d.ts.map +1 -0
  49. package/dist-lib/components/PlannerDiffCard.js +25 -0
  50. package/dist-lib/components/RegiePage.d.ts.map +1 -1
  51. package/dist-lib/components/RegiePage.js +28 -78
  52. package/dist-lib/components/TaakGezondheidCard.d.ts +2 -0
  53. package/dist-lib/components/TaakGezondheidCard.d.ts.map +1 -0
  54. package/dist-lib/components/TaakGezondheidCard.js +31 -0
  55. package/dist-lib/components/WerktijdenCard.d.ts +2 -0
  56. package/dist-lib/components/WerktijdenCard.d.ts.map +1 -0
  57. package/dist-lib/components/WerktijdenCard.js +37 -0
  58. package/dist-lib/components/WerkwijzeHerindelingCard.d.ts.map +1 -1
  59. package/dist-lib/components/WerkwijzeHerindelingCard.js +100 -6
  60. package/dist-lib/hooks/useAfrondWachtrij.d.ts +28 -0
  61. package/dist-lib/hooks/useAfrondWachtrij.d.ts.map +1 -0
  62. package/dist-lib/hooks/useAfrondWachtrij.js +88 -0
  63. package/dist-lib/hooks/useBeladingsafwijkingen.d.ts +39 -0
  64. package/dist-lib/hooks/useBeladingsafwijkingen.d.ts.map +1 -0
  65. package/dist-lib/hooks/useBeladingsafwijkingen.js +119 -0
  66. package/dist-lib/hooks/useGeblokkeerdeOrders.d.ts +7 -0
  67. package/dist-lib/hooks/useGeblokkeerdeOrders.d.ts.map +1 -1
  68. package/dist-lib/hooks/useGeblokkeerdeOrders.js +28 -2
  69. package/dist-lib/hooks/useLogisticsTrace.d.ts +78 -0
  70. package/dist-lib/hooks/useLogisticsTrace.d.ts.map +1 -0
  71. package/dist-lib/hooks/useLogisticsTrace.js +152 -0
  72. package/dist-lib/hooks/useOrderNodeTrace.d.ts +11 -0
  73. package/dist-lib/hooks/useOrderNodeTrace.d.ts.map +1 -0
  74. package/dist-lib/hooks/useOrderNodeTrace.js +26 -0
  75. package/dist-lib/hooks/useOrderTrace.d.ts +74 -0
  76. package/dist-lib/hooks/useOrderTrace.d.ts.map +1 -0
  77. package/dist-lib/hooks/useOrderTrace.js +134 -0
  78. package/dist-lib/hooks/useOrderVersies.d.ts +47 -0
  79. package/dist-lib/hooks/useOrderVersies.d.ts.map +1 -0
  80. package/dist-lib/hooks/useOrderVersies.js +194 -0
  81. package/dist-lib/hooks/usePlanWachtrij.d.ts +33 -0
  82. package/dist-lib/hooks/usePlanWachtrij.d.ts.map +1 -0
  83. package/dist-lib/hooks/usePlanWachtrij.js +104 -0
  84. package/dist-lib/hooks/useRecalcJob.js +1 -1
  85. package/dist-lib/hooks/useRuleEngine.d.ts +1 -0
  86. package/dist-lib/hooks/useRuleEngine.d.ts.map +1 -1
  87. package/dist-lib/hooks/useRuleEngine.js +1 -0
  88. package/dist-lib/hooks/useShippingNoteDiagnose.d.ts +20 -0
  89. package/dist-lib/hooks/useShippingNoteDiagnose.d.ts.map +1 -0
  90. package/dist-lib/hooks/useShippingNoteDiagnose.js +52 -0
  91. package/dist-lib/hooks/useShippingNoteStatus.d.ts +29 -0
  92. package/dist-lib/hooks/useShippingNoteStatus.d.ts.map +1 -0
  93. package/dist-lib/hooks/useShippingNoteStatus.js +107 -0
  94. package/dist-lib/hooks/useTaakGezondheid.d.ts +16 -0
  95. package/dist-lib/hooks/useTaakGezondheid.d.ts.map +1 -0
  96. package/dist-lib/hooks/useTaakGezondheid.js +43 -0
  97. package/dist-lib/hooks/useWerktijden.d.ts +33 -0
  98. package/dist-lib/hooks/useWerktijden.d.ts.map +1 -0
  99. package/dist-lib/hooks/useWerktijden.js +88 -0
  100. package/dist-lib/hooks/useWerkwijzeStatus.d.ts +12 -1
  101. package/dist-lib/hooks/useWerkwijzeStatus.d.ts.map +1 -1
  102. package/dist-lib/hooks/useWerkwijzeStatus.js +61 -5
  103. package/dist-lib/index.d.ts +23 -0
  104. package/dist-lib/index.d.ts.map +1 -1
  105. package/dist-lib/index.js +23 -0
  106. package/dist-lib/integrations/supabase/auth-middleware.d.ts +4993 -459
  107. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  108. package/dist-lib/integrations/supabase/client.server.d.ts +4993 -459
  109. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  110. package/dist-lib/integrations/supabase/client.server.js +1 -1
  111. package/dist-lib/integrations/supabase/moduleClient.d.ts +4993 -459
  112. package/dist-lib/integrations/supabase/moduleClient.d.ts.map +1 -1
  113. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +1 -1
  114. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -1
  115. package/dist-lib/integrations/supabase/previewAuthStorage.js +8 -1
  116. package/dist-lib/integrations/supabase/types.d.ts +5022 -432
  117. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  118. package/dist-lib/lib/attachAuthToken.js +1 -1
  119. package/dist-lib/lib/bufferDecision.test.d.ts +2 -0
  120. package/dist-lib/lib/bufferDecision.test.d.ts.map +1 -0
  121. package/dist-lib/lib/bufferDecision.test.js +104 -0
  122. package/dist-lib/lib/cartCoverage.functions.d.ts +30820 -0
  123. package/dist-lib/lib/cartCoverage.functions.d.ts.map +1 -0
  124. package/dist-lib/lib/cartCoverage.functions.js +20 -0
  125. package/dist-lib/lib/cartPlanRequests.server.d.ts +1 -1
  126. package/dist-lib/lib/cartPlanRequests.server.d.ts.map +1 -1
  127. package/dist-lib/lib/cartPlanRequests.server.js +49 -8
  128. package/dist-lib/lib/floridayDocuments.functions.d.ts +13623 -21
  129. package/dist-lib/lib/floridayDocuments.functions.d.ts.map +1 -1
  130. package/dist-lib/lib/floridayDocuments.functions.js +5 -5
  131. package/dist-lib/lib/floridayDocuments.server.d.ts +14 -0
  132. package/dist-lib/lib/floridayDocuments.server.d.ts.map +1 -1
  133. package/dist-lib/lib/floridayDocuments.server.js +58 -0
  134. package/dist-lib/lib/floridayGuard.server.d.ts +1 -1
  135. package/dist-lib/lib/floridayGuard.server.d.ts.map +1 -1
  136. package/dist-lib/lib/floridayGuard.server.js +6 -3
  137. package/dist-lib/lib/floridayPush.functions.d.ts +4993 -459
  138. package/dist-lib/lib/floridayPush.functions.d.ts.map +1 -1
  139. package/dist-lib/lib/floridayPush.functions.js +3 -3
  140. package/dist-lib/lib/flowGraph.d.ts +43 -0
  141. package/dist-lib/lib/flowGraph.d.ts.map +1 -0
  142. package/dist-lib/lib/flowGraph.js +153 -0
  143. package/dist-lib/lib/flowJourney.d.ts +40 -0
  144. package/dist-lib/lib/flowJourney.d.ts.map +1 -0
  145. package/dist-lib/lib/flowJourney.js +156 -0
  146. package/dist-lib/lib/flowNodes.d.ts +26 -0
  147. package/dist-lib/lib/flowNodes.d.ts.map +1 -0
  148. package/dist-lib/lib/flowNodes.js +64 -0
  149. package/dist-lib/lib/flowRoute.d.ts +117 -0
  150. package/dist-lib/lib/flowRoute.d.ts.map +1 -0
  151. package/dist-lib/lib/flowRoute.js +583 -0
  152. package/dist-lib/lib/flowTree.d.ts +149 -0
  153. package/dist-lib/lib/flowTree.d.ts.map +1 -0
  154. package/dist-lib/lib/flowTree.js +908 -0
  155. package/dist-lib/lib/foutTekst.d.ts +4 -0
  156. package/dist-lib/lib/foutTekst.d.ts.map +1 -0
  157. package/dist-lib/lib/foutTekst.js +43 -0
  158. package/dist-lib/lib/handlers/index.d.ts +1 -0
  159. package/dist-lib/lib/handlers/index.d.ts.map +1 -1
  160. package/dist-lib/lib/handlers/index.js +1 -0
  161. package/dist-lib/lib/handlers/logistics-shipping-note-now.server.d.ts +3 -0
  162. package/dist-lib/lib/handlers/logistics-shipping-note-now.server.d.ts.map +1 -0
  163. package/dist-lib/lib/handlers/logistics-shipping-note-now.server.js +100 -0
  164. package/dist-lib/lib/handlers/logistics-tick.server.d.ts +2 -0
  165. package/dist-lib/lib/handlers/logistics-tick.server.d.ts.map +1 -1
  166. package/dist-lib/lib/handlers/logistics-tick.server.js +153 -2
  167. package/dist-lib/lib/handlers/logistics-tick.test.d.ts +2 -0
  168. package/dist-lib/lib/handlers/logistics-tick.test.d.ts.map +1 -0
  169. package/dist-lib/lib/handlers/logistics-tick.test.js +63 -0
  170. package/dist-lib/lib/loadCarrierPlanner.d.ts +2 -0
  171. package/dist-lib/lib/loadCarrierPlanner.d.ts.map +1 -1
  172. package/dist-lib/lib/loadCarrierPlanner.js +18 -0
  173. package/dist-lib/lib/loadCarrierPlanning.functions.d.ts +22705 -35
  174. package/dist-lib/lib/loadCarrierPlanning.functions.d.ts.map +1 -1
  175. package/dist-lib/lib/loadCarrierPlanning.functions.js +7 -7
  176. package/dist-lib/lib/loadCarrierPlanning.server.d.ts +33 -12
  177. package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -1
  178. package/dist-lib/lib/loadCarrierPlanning.server.js +534 -228
  179. package/dist-lib/lib/logisticsPlanner.d.ts.map +1 -1
  180. package/dist-lib/lib/logisticsPlanner.js +14 -2
  181. package/dist-lib/lib/logisticsRecalcJob.functions.d.ts +9082 -14
  182. package/dist-lib/lib/logisticsRecalcJob.functions.d.ts.map +1 -1
  183. package/dist-lib/lib/logisticsRecalcJob.functions.js +4 -4
  184. package/dist-lib/lib/logisticsRecalcJob.server.d.ts.map +1 -1
  185. package/dist-lib/lib/logisticsRecalcJob.server.js +17 -2
  186. package/dist-lib/lib/logisticsRecalculate.functions.d.ts +4993 -459
  187. package/dist-lib/lib/logisticsRecalculate.functions.d.ts.map +1 -1
  188. package/dist-lib/lib/logisticsRecalculate.functions.js +2 -2
  189. package/dist-lib/lib/modulePrompts.d.ts.map +1 -1
  190. package/dist-lib/lib/modulePrompts.js +893 -0
  191. package/dist-lib/lib/nodeTrace.d.ts +65 -0
  192. package/dist-lib/lib/nodeTrace.d.ts.map +1 -0
  193. package/dist-lib/lib/nodeTrace.js +182 -0
  194. package/dist-lib/lib/pickQueueCleanup.server.d.ts +14 -0
  195. package/dist-lib/lib/pickQueueCleanup.server.d.ts.map +1 -0
  196. package/dist-lib/lib/pickQueueCleanup.server.js +80 -0
  197. package/dist-lib/lib/planQueue.server.d.ts.map +1 -1
  198. package/dist-lib/lib/planQueue.server.js +164 -3
  199. package/dist-lib/lib/plannerDiff.d.ts +37 -0
  200. package/dist-lib/lib/plannerDiff.d.ts.map +1 -0
  201. package/dist-lib/lib/plannerDiff.functions.d.ts +30821 -0
  202. package/dist-lib/lib/plannerDiff.functions.d.ts.map +1 -0
  203. package/dist-lib/lib/plannerDiff.functions.js +21 -0
  204. package/dist-lib/lib/plannerDiff.js +74 -0
  205. package/dist-lib/lib/plannerDiff.server.d.ts +5 -0
  206. package/dist-lib/lib/plannerDiff.server.d.ts.map +1 -0
  207. package/dist-lib/lib/plannerDiff.server.js +60 -0
  208. package/dist-lib/lib/plannerDiff.test.d.ts +2 -0
  209. package/dist-lib/lib/plannerDiff.test.d.ts.map +1 -0
  210. package/dist-lib/lib/plannerDiff.test.js +41 -0
  211. package/dist-lib/lib/resolveBuffer.test.d.ts +2 -0
  212. package/dist-lib/lib/resolveBuffer.test.d.ts.map +1 -0
  213. package/dist-lib/lib/resolveBuffer.test.js +38 -0
  214. package/dist-lib/lib/ruleEngineImpact.d.ts.map +1 -1
  215. package/dist-lib/lib/ruleEngineImpact.js +11 -4
  216. package/dist-lib/lib/ruleEngineSchema.d.ts.map +1 -1
  217. package/dist-lib/lib/ruleEngineSchema.js +102 -6
  218. package/dist-lib/lib/ruleEngineWizard.d.ts.map +1 -1
  219. package/dist-lib/lib/ruleEngineWizard.js +3 -1
  220. package/dist-lib/lib/shippingNoteQueue.server.d.ts +18 -0
  221. package/dist-lib/lib/shippingNoteQueue.server.d.ts.map +1 -0
  222. package/dist-lib/lib/shippingNoteQueue.server.js +304 -0
  223. package/dist-lib/lib/stopReden.d.ts +43 -0
  224. package/dist-lib/lib/stopReden.d.ts.map +1 -0
  225. package/dist-lib/lib/stopReden.js +197 -0
  226. package/dist-lib/lib/taskCounts.d.ts +2 -0
  227. package/dist-lib/lib/taskCounts.d.ts.map +1 -0
  228. package/dist-lib/lib/taskCounts.js +15 -0
  229. package/dist-lib/lib/taskCounts.test.d.ts +2 -0
  230. package/dist-lib/lib/taskCounts.test.d.ts.map +1 -0
  231. package/dist-lib/lib/taskCounts.test.js +12 -0
  232. package/dist-lib/lib/trace.server.d.ts +75 -0
  233. package/dist-lib/lib/trace.server.d.ts.map +1 -0
  234. package/dist-lib/lib/trace.server.js +78 -0
  235. package/dist-lib/lib/vakUitleg.d.ts +19 -0
  236. package/dist-lib/lib/vakUitleg.d.ts.map +1 -0
  237. package/dist-lib/lib/vakUitleg.js +133 -0
  238. package/package.json +158 -131
  239. package/dist-lib/components/rule-engine/FloridayConnectionBanner.d.ts +0 -2
  240. package/dist-lib/components/rule-engine/FloridayConnectionBanner.d.ts.map +0 -1
  241. package/dist-lib/components/rule-engine/FloridayConnectionBanner.js +0 -41
  242. package/dist-lib/integrations/supabase/client.d.ts +0 -20293
  243. package/dist-lib/integrations/supabase/client.d.ts.map +0 -1
  244. package/dist-lib/integrations/supabase/client.js +0 -13
@@ -0,0 +1,908 @@
1
+ // ============================================================================
2
+ // src/lib/flowTree.ts — vertakkende beslisboom van de Logistiek Engine
3
+ // ============================================================================
4
+ // Bouwt uit WIZARD_STEPS en de opgeslagen werkwijze een boom waarin ELK
5
+ // beslismoment al zijn uitkomsten laat zien. De vertakkingen komen uit de
6
+ // `dependsOn`-regels van het schema: een instelling die alleen geldt bij een
7
+ // bepaalde keuze hangt onder de pijl van die keuze. De actieve route wordt met
8
+ // dezelfde `fieldVisible`-logica bepaald die de engine en het instelscherm
9
+ // gebruiken. Er staat hier geen aparte flowlogica.
10
+ // ============================================================================
11
+ import { fieldVisible, optionLabel, } from "./ruleEngineSchema";
12
+ import { WIZARD_STEPS, fieldHelp } from "./ruleEngineWizard";
13
+ export const NODE_SIZE = {
14
+ start: { w: 240, h: 60 },
15
+ end: { w: 240, h: 60 },
16
+ source: { w: 260, h: 96 },
17
+ step: { w: 300, h: 88 },
18
+ decision: { w: 280, h: 118 },
19
+ setting: { w: 240, h: 66 },
20
+ option: { w: 150, h: 30 },
21
+ merge: { w: 14, h: 14 },
22
+ };
23
+ const DAY_SHORT = {
24
+ mon: "ma",
25
+ tue: "di",
26
+ wed: "wo",
27
+ thu: "do",
28
+ fri: "vr",
29
+ sat: "za",
30
+ sun: "zo",
31
+ };
32
+ function isDecision(field) {
33
+ if (field.type === "toggle")
34
+ return true;
35
+ if (field.type === "select" || field.type === "radio") {
36
+ return Array.isArray(field.options) && field.options.length >= 2;
37
+ }
38
+ return false;
39
+ }
40
+ function rulesOf(field) {
41
+ if (!field.dependsOn)
42
+ return [];
43
+ return Array.isArray(field.dependsOn) ? field.dependsOn : [field.dependsOn];
44
+ }
45
+ function optionsOf(field) {
46
+ if (field.type === "toggle") {
47
+ return [
48
+ { value: "true", label: "ja" },
49
+ { value: "false", label: "nee" },
50
+ ];
51
+ }
52
+ return (field.options ?? []).map((o) => ({ value: o.value, label: o.label }));
53
+ }
54
+ function currentOf(field, value) {
55
+ if (field.type === "toggle")
56
+ return value === true ? "true" : "false";
57
+ return String(value ?? "");
58
+ }
59
+ /**
60
+ * Zoekt de schemavelden op die bij een vakje horen. Gebruikt dezelfde
61
+ * WIZARD_STEPS als het instelscherm, zodat er geen tweede definitie ontstaat.
62
+ */
63
+ export function fieldsFor(storeId, keys) {
64
+ const veiligeKeys = Array.isArray(keys) ? keys : [];
65
+ if (!storeId || !veiligeKeys.length)
66
+ return [];
67
+ const stappen = Array.isArray(WIZARD_STEPS) ? WIZARD_STEPS : [];
68
+ const alle = [];
69
+ for (const stap of stappen) {
70
+ if (stap.storeId !== storeId)
71
+ continue;
72
+ for (const group of Array.isArray(stap.groups) ? stap.groups : []) {
73
+ for (const field of Array.isArray(group.fields) ? group.fields : [])
74
+ alle.push(field);
75
+ }
76
+ }
77
+ return veiligeKeys
78
+ .map((k) => alle.find((f) => f.key === k))
79
+ .filter((f) => Boolean(f));
80
+ }
81
+ export function valueText(field, value) {
82
+ if (value === undefined || value === null || value === "")
83
+ return "niet ingesteld";
84
+ switch (field.type) {
85
+ case "toggle":
86
+ return value === true ? "aan" : "uit";
87
+ case "select":
88
+ case "radio":
89
+ return optionLabel(field, value);
90
+ case "days": {
91
+ const days = Array.isArray(value) ? value : [];
92
+ return days.length ? days.map((d) => DAY_SHORT[d] ?? d).join(", ") : "geen";
93
+ }
94
+ case "number":
95
+ return `${String(value)}${field.unit ? ` ${field.unit}` : ""}`;
96
+ case "priority": {
97
+ const entries = Array.isArray(value) ? value : [];
98
+ return `${entries.filter((e) => e?.enabled).length} actief`;
99
+ }
100
+ case "textarea":
101
+ case "text": {
102
+ const t = String(value);
103
+ return t.length > 40 ? `${t.slice(0, 39)}…` : t;
104
+ }
105
+ default:
106
+ return String(value);
107
+ }
108
+ }
109
+ /** Toestand van een tak-pijl uit een beslissing. */
110
+ function edgeState(decision, isCurrent) {
111
+ if (decision === "active")
112
+ return isCurrent ? "active" : "possible";
113
+ if (decision === "possible")
114
+ return isCurrent ? "possible" : "disabled";
115
+ return "disabled";
116
+ }
117
+ function weakest(a, b) {
118
+ const rank = { active: 0, possible: 1, disabled: 2 };
119
+ return rank[a] >= rank[b] ? a : b;
120
+ }
121
+ /** Bouwt de complete beslisboom van binnenkomst tot afgehandelde order. */
122
+ export function buildFlowTree(settings, overrides) {
123
+ const veilig = settings && typeof settings === "object" ? settings : {};
124
+ const nodes = [];
125
+ const edges = [];
126
+ const decisionIds = new Map();
127
+ const pendingRefs = [];
128
+ const add = (node) => {
129
+ const size = NODE_SIZE[node.kind];
130
+ const n = { ...node, ancestors: node.ancestors ?? [], w: size.w, h: size.h };
131
+ nodes.push(n);
132
+ return n;
133
+ };
134
+ const link = (from, to, state, label, ancestors = [], kind = "flow") => {
135
+ edges.push({ id: `${kind}:${from}->${to}:${label ?? ""}`, from, to, label, state, kind, ancestors });
136
+ };
137
+ // ── Start en herkomst ──────────────────────────────────────────────────────
138
+ add({
139
+ id: "__start",
140
+ kind: "start",
141
+ label: "Order komt binnen",
142
+ sublabel: "Floriday of eigen verkoop",
143
+ state: "active",
144
+ help: [
145
+ "Hier begint de route. Elke order die binnenkomt start bovenaan deze kaart en loopt via alle stappen naar 'Order afgehandeld'.",
146
+ "Gekleurde vakjes en dikke pijlen zijn de route die nu geldt. Lichte vakjes zijn keuzes die je ook kunt maken; grijze, gestippelde takken zijn op dit moment niet bereikbaar door een keuze eerder in de route.",
147
+ ],
148
+ });
149
+ add({
150
+ id: "__bron",
151
+ kind: "source",
152
+ label: "Waar komt de order vandaan?",
153
+ sublabel: "vaste regel — geen instelling",
154
+ state: "active",
155
+ help: [
156
+ "De engine herkent zelf waar een order vandaan komt: Floriday, de mailbox voor bestellingen, de actiemail of handmatige invoer. Dit is geen instelling; alle vier de routes bestaan altijd.",
157
+ "Het verschil zit vooral bij de verzendbrief: een Floriday-order krijgt uitsluitend de officiële Floriday-afleverbrief, de overige orders de eigen verzendbrieflayout uit de Layout maker.",
158
+ "Verder doorlopen alle orders dezelfde stappen en dezelfde regels hieronder.",
159
+ ],
160
+ });
161
+ link("__start", "__bron", "active");
162
+ add({ id: "__bron_merge", kind: "merge", label: "", state: "active", help: [] });
163
+ const bronnen = [
164
+ {
165
+ id: "__bron_floriday",
166
+ label: "Bestellingen uit Floriday",
167
+ sub: "order_source: floriday",
168
+ help: [
169
+ "Floriday levert per artikel een eigen verkooporder aan. Eén bestelling van één klant komt daardoor binnen als losse regels met elk een eigen FO-nummer.",
170
+ "Daarom volgt hierna eerst de stap 'Bestelregels samenvoegen'.",
171
+ ],
172
+ },
173
+ {
174
+ id: "__bron_mail",
175
+ label: "Bestellingen uit de mail",
176
+ sub: "mailbox voor bestellingen",
177
+ help: [
178
+ "Bestellingen die per e-mail binnenkomen en via de mailboxmodule als ordervoorstel worden goedgekeurd.",
179
+ "Een goedgekeurd voorstel wordt één order en loopt daarna dezelfde route als alle andere orders.",
180
+ ],
181
+ },
182
+ {
183
+ id: "__bron_actiemail",
184
+ label: "Bestellingen uit de actiemail",
185
+ sub: "actiemail kwekers",
186
+ help: [
187
+ "Bestellingen die voortkomen uit een actiemail aan kwekers/klanten.",
188
+ "Ook deze orders lopen daarna dezelfde route als alle andere orders.",
189
+ ],
190
+ },
191
+ {
192
+ id: "__bron_handmatig",
193
+ label: "Handmatige bestellingen",
194
+ sub: "ingevoerd in verkoop",
195
+ help: [
196
+ "Orders die een medewerker zelf aanmaakt in het verkoopscherm.",
197
+ "Deze orders zijn al één bestelling met één ordernummer en worden niet samengevoegd.",
198
+ ],
199
+ },
200
+ ];
201
+ for (const b of bronnen) {
202
+ add({ id: b.id, kind: "source", label: b.label, sublabel: b.sub, state: "active", help: b.help });
203
+ link("__bron", b.id, "active");
204
+ }
205
+ // ── Floriday: bestelregels eerst samenvoegen tot één bestelling ────────────
206
+ const ovFields = (veilig["order_verwerking"]?.fields ?? {});
207
+ const samenvoegenAan = ovFields["floriday_merge_lines"] !== false;
208
+ add({
209
+ id: "stap:bestelregels_samenvoegen",
210
+ kind: "step",
211
+ label: "Bestelregels samenvoegen",
212
+ sublabel: samenvoegenAan
213
+ ? "aan — regels van dezelfde klant en levermoment worden één bestelling"
214
+ : "uit — elke Floriday-regel blijft een eigen order",
215
+ state: samenvoegenAan ? "active" : "disabled",
216
+ storeId: "order_verwerking",
217
+ fieldKeys: ["floriday_merge_lines"],
218
+ lines: [
219
+ samenvoegenAan
220
+ ? "Samenvoegen: aan (zelfde klant, levermoment en afleverlocatie)"
221
+ : "Samenvoegen: uit",
222
+ "Laagste FO-nummer blijft het ordernummer",
223
+ "Al ingedeelde, geprinte of gecontroleerde orders blijven ongewijzigd",
224
+ ],
225
+ help: [
226
+ "Floriday stuurt elke bestelregel als een eigen verkooporder. Deze stap bundelt de regels van dezelfde klant, hetzelfde levermoment en dezelfde afleverlocatie tot één bestelling met één ordernummer.",
227
+ "Het laagste FO-nummer van de groep blijft staan; de andere nummers worden als herkomst bewaard, zodat documenten en terugkoppeling naar Floriday blijven kloppen.",
228
+ "Orders die al vastliggen (ingedeeld, geprint, gescand of afgehandeld) worden nooit alsnog samengevoegd.",
229
+ ],
230
+ });
231
+ link("__bron_floriday", "stap:bestelregels_samenvoegen", samenvoegenAan ? "active" : "possible");
232
+ link("stap:bestelregels_samenvoegen", "__bron_merge", samenvoegenAan ? "active" : "possible");
233
+ link("__bron_mail", "__bron_merge", "active");
234
+ link("__bron_actiemail", "__bron_merge", "active");
235
+ link("__bron_handmatig", "__bron_merge", "active");
236
+ let tail = "__bron_merge";
237
+ // ── Stappen ────────────────────────────────────────────────────────────────
238
+ WIZARD_STEPS.forEach((step, index) => {
239
+ const values = (veilig?.[step.storeId]?.fields ?? {});
240
+ const groups = Array.isArray(step.groups) ? step.groups : [];
241
+ const fieldsInOrder = [];
242
+ for (const group of groups) {
243
+ for (const field of Array.isArray(group.fields) ? group.fields : []) {
244
+ if (field.type === "guidance")
245
+ continue;
246
+ fieldsInOrder.push({ field, group });
247
+ }
248
+ }
249
+ const keysHere = new Set(fieldsInOrder.map((f) => f.field.key));
250
+ const order = new Map(fieldsInOrder.map((f, i) => [f.field.key, i]));
251
+ // Elk veld hangt onder de "diepste" beslissing uit dezelfde stap waar het
252
+ // van afhangt (de regel waarvan het bronveld het laatst in de volgorde staat).
253
+ const childrenOf = new Map();
254
+ const roots = [];
255
+ for (const item of fieldsInOrder) {
256
+ const local = rulesOf(item.field).filter((r) => !r.step && keysHere.has(r.key));
257
+ if (!local.length) {
258
+ roots.push(item);
259
+ continue;
260
+ }
261
+ const parentRule = local.reduce((best, r) => (order.get(r.key) ?? -1) > (order.get(best.key) ?? -1) ? r : best);
262
+ const list = childrenOf.get(parentRule.key) ?? [];
263
+ list.push({ ...item, rule: parentRule });
264
+ childrenOf.set(parentRule.key, list);
265
+ }
266
+ const ctx = {
267
+ nodes,
268
+ edges,
269
+ settings: veilig,
270
+ step,
271
+ values,
272
+ fieldsInOrder,
273
+ childrenOf,
274
+ decisionIds,
275
+ pendingRefs,
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
+ }
303
+ const stepNode = add({
304
+ id: `stap:${step.id}`,
305
+ kind: "step",
306
+ label: `${index + 1}. ${step.label}`,
307
+ sublabel: step.description,
308
+ stepId: step.id,
309
+ storeId: step.storeId,
310
+ state: "active",
311
+ help: [
312
+ step.description,
313
+ `Achter deze stap staan ${fieldsInOrder.length} instellingen. Klik op het vakje om ze allemaal in het zijpaneel te openen.`,
314
+ "De vertakkingen hieronder laten zien welke instellingen alleen gelden bij een bepaalde keuze.",
315
+ ],
316
+ });
317
+ link(tail, stepNode.id, "active");
318
+ const stapMerge = `samen:stap:${step.id}`;
319
+ fan(ctx, stepNode.id, "active", roots, [], stapMerge);
320
+ nodes.push({ id: stapMerge, kind: "merge", label: "", state: "active", ancestors: [], help: [], w: NODE_SIZE.merge.w, h: NODE_SIZE.merge.h });
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
+ }
361
+ });
362
+ // ── Einde ──────────────────────────────────────────────────────────────────
363
+ add({
364
+ id: "__eind",
365
+ kind: "end",
366
+ label: "Order afgehandeld",
367
+ sublabel: "gecontroleerd en afgemeld",
368
+ state: "active",
369
+ help: [
370
+ "Dit is het einde van de route: de order is gepickt, gecontroleerd en afgemeld.",
371
+ "Pas als alle stappen hierboven doorlopen zijn, komt een order hier terecht.",
372
+ ],
373
+ });
374
+ link(tail, "__eind", "active");
375
+ // ── Verwijspijlen tussen stappen ───────────────────────────────────────────
376
+ for (const ref of pendingRefs) {
377
+ const from = decisionIds.get(`${ref.storeId}:${ref.key}`);
378
+ if (!from)
379
+ continue;
380
+ const fromNode = nodes.find((n) => n.id === from);
381
+ const label = fromNode
382
+ ? ref.values
383
+ .map((v) => fromNode.options?.find((o) => o.value === String(v))?.label ?? String(v))
384
+ .join(" / ")
385
+ : undefined;
386
+ link(from, ref.toNode, ref.state, label, ref.ancestors, "ref");
387
+ }
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 };
592
+ }
593
+ /**
594
+ * Waaiert een lijst velden uit vanaf `from`: elke beslissing wordt een eigen arm
595
+ * met al haar keuzes; losse instellingen worden per blok gebundeld in één
596
+ * vakje. Alle armen komen samen in `mergeId`.
597
+ */
598
+ function fan(ctx, from, fromState, items, ancestors, mergeId) {
599
+ const size = NODE_SIZE;
600
+ // `from` is uniek per tak (stap, of het tussenpunt van een keuze).
601
+ const takSleutel = from.replace(/[^a-z0-9_]+/gi, "_");
602
+ const push = (e) => {
603
+ ctx.edges.push({
604
+ id: `${e.kind ?? "flow"}:${e.from}->${e.to}:${e.suffix ?? e.label ?? ""}`,
605
+ from: e.from,
606
+ to: e.to,
607
+ label: e.label,
608
+ state: e.state,
609
+ kind: e.kind ?? "flow",
610
+ ancestors: e.ancestors,
611
+ });
612
+ };
613
+ const uitlegVoor = (field, group, state) => {
614
+ const help = [];
615
+ const uitleg = fieldHelp(field);
616
+ if (uitleg)
617
+ help.push(uitleg);
618
+ help.push(`Onderdeel van stap "${ctx.step.label}", blok "${group.label}".`);
619
+ help.push(`Nu ingesteld op: ${valueText(field, ctx.values[field.key])}.`);
620
+ if (ancestors.length) {
621
+ help.push(`Deze tak geldt alleen onder: ${ancestors
622
+ .map((a) => ctx.nodes.find((n) => n.id === a)?.label ?? a)
623
+ .join(" → ")}.`);
624
+ }
625
+ if (state === "disabled") {
626
+ help.push("Deze tak is op dit moment niet bereikbaar door een keuze eerder in de route.");
627
+ }
628
+ else if (state === "possible") {
629
+ help.push("Deze tak is mogelijk, maar niet de keuze die nu geldt.");
630
+ }
631
+ return help;
632
+ };
633
+ const verwijzingen = (field, toNode, state, help) => {
634
+ for (const rule of rulesOf(field)) {
635
+ if (!rule.step)
636
+ continue;
637
+ ctx.pendingRefs.push({ toNode, storeId: rule.step, key: rule.key, values: rule.in, state, ancestors });
638
+ help.push("De gestippelde pijl laat zien van welke keuze uit een andere stap dit vakje afhangt.");
639
+ }
640
+ };
641
+ const stateVan = (field) => fieldVisible(field, ctx.values, ctx.settings) ? fromState : weakest(fromState, "disabled");
642
+ let armen = 0;
643
+ // ── Losse instellingen: per blok één vakje ────────────────────────────────
644
+ const losse = items.filter((i) => !isDecision(i.field));
645
+ const perBlok = new Map();
646
+ for (const { field, group } of losse) {
647
+ const b = perBlok.get(group.id) ?? { group, fields: [] };
648
+ b.fields.push(field);
649
+ perBlok.set(group.id, b);
650
+ }
651
+ for (const { group, fields } of perBlok.values()) {
652
+ const id = `blok:${ctx.step.storeId}:${group.id}:${takSleutel}`;
653
+ const states = fields.map(stateVan);
654
+ const state = states.some((s) => s !== "disabled") ? fromState : weakest(fromState, "disabled");
655
+ const lines = fields.map((f) => `${f.label}: ${valueText(f, ctx.values[f.key])}`);
656
+ const help = [];
657
+ if (group.description)
658
+ help.push(group.description);
659
+ help.push(`Vaste instellingen in blok "${group.label}" van stap "${ctx.step.label}":`);
660
+ help.push(...lines);
661
+ help.push("Dubbelklik op het vakje om deze instellingen hier meteen aan te passen.");
662
+ for (const f of fields)
663
+ verwijzingen(f, id, state, help);
664
+ const h = Math.min(6, lines.length) * 16 + 44;
665
+ ctx.nodes.push({
666
+ id,
667
+ kind: "setting",
668
+ label: group.label,
669
+ sublabel: `${fields.length} instelling${fields.length === 1 ? "" : "en"}`,
670
+ lines,
671
+ fieldKeys: fields.map((f) => f.key),
672
+ state,
673
+ stepId: ctx.step.id,
674
+ storeId: ctx.step.storeId,
675
+ ancestors,
676
+ help,
677
+ w: size.setting.w,
678
+ h,
679
+ });
680
+ push({ from, to: id, state, ancestors });
681
+ push({ from: id, to: mergeId, state, ancestors });
682
+ armen++;
683
+ }
684
+ // ── Beslissingen: elke keuze een eigen tak ────────────────────────────────
685
+ for (const { field, group } of items) {
686
+ if (!isDecision(field))
687
+ continue;
688
+ const state = stateVan(field);
689
+ const id = `keuze:${ctx.step.storeId}:${field.key}`;
690
+ const value = ctx.values[field.key];
691
+ const options = optionsOf(field);
692
+ const current = currentOf(field, value);
693
+ const eigenMerge = `samen:${ctx.step.storeId}:${field.key}`;
694
+ const help = uitlegVoor(field, group, state);
695
+ help.push(`Mogelijke keuzes: ${options.map((o) => o.label).join(" · ")}. Kies je hier iets anders, dan wordt meteen een andere tak actief én werkt de engine daarna volgens die keuze.`);
696
+ verwijzingen(field, id, state, help);
697
+ ctx.nodes.push({
698
+ id,
699
+ kind: "decision",
700
+ label: field.label,
701
+ sublabel: group.label,
702
+ state,
703
+ stepId: ctx.step.id,
704
+ storeId: ctx.step.storeId,
705
+ fieldKey: field.key,
706
+ fieldKeys: [field.key],
707
+ options,
708
+ currentValue: current,
709
+ isBoolean: field.type === "toggle",
710
+ mergeId: eigenMerge,
711
+ ancestors,
712
+ help,
713
+ w: size.decision.w,
714
+ h: size.decision.h,
715
+ });
716
+ ctx.decisionIds.set(`${ctx.step.storeId}:${field.key}`, id);
717
+ push({ from, to: id, state, ancestors });
718
+ const kinderen = ctx.childrenOf.get(field.key) ?? [];
719
+ const takAncestors = [...ancestors, id];
720
+ // Takken: één per verzameling keuzes waar dezelfde kinderen onder hangen,
721
+ // zodat een instelling die bij "scanner" én "scan_only" geldt één vakje blijft.
722
+ const takken = new Map();
723
+ for (const k of kinderen) {
724
+ const waarden = options.map((o) => o.value).filter((v) => k.rule.in.some((r) => String(r) === v));
725
+ if (!waarden.length)
726
+ continue;
727
+ const sleutel = waarden.join("|");
728
+ const tak = takken.get(sleutel) ?? { waarden, kinderen: [] };
729
+ tak.kinderen.push(k);
730
+ takken.set(sleutel, tak);
731
+ }
732
+ const gedekt = new Set([...takken.values()].flatMap((t) => t.waarden));
733
+ const labelVan = (waarden) => waarden.map((v) => options.find((o) => o.value === v)?.label ?? v).join(" / ");
734
+ for (const option of options) {
735
+ if (gedekt.has(option.value))
736
+ continue;
737
+ // Keuze zonder eigen vervolg: een klein label-vakje, zodat alle uitkomsten
738
+ // naast elkaar zichtbaar blijven.
739
+ const optState = edgeState(state, option.value === current);
740
+ const optId = `optie:${ctx.step.storeId}:${field.key}:${option.value}`;
741
+ ctx.nodes.push({
742
+ id: optId,
743
+ kind: "option",
744
+ label: option.label,
745
+ state: optState,
746
+ stepId: ctx.step.id,
747
+ storeId: ctx.step.storeId,
748
+ fieldKey: field.key,
749
+ ancestors: takAncestors,
750
+ help: [
751
+ `Uitkomst "${option.label}" van "${field.label}".`,
752
+ optState === "active"
753
+ ? "Dit is de keuze die nu geldt."
754
+ : "Deze keuze geldt nu niet; kies hem in de beslistegel om deze tak actief te maken.",
755
+ "Er hangen geen extra instellingen onder deze uitkomst; de route gaat direct verder.",
756
+ ],
757
+ w: size.option.w,
758
+ h: size.option.h,
759
+ });
760
+ push({ from: id, to: optId, state: optState, ancestors: takAncestors });
761
+ push({ from: optId, to: eigenMerge, state: optState, ancestors: takAncestors });
762
+ }
763
+ for (const [sleutel, tak] of takken) {
764
+ const takState = edgeState(state, tak.waarden.includes(current));
765
+ // Klein tussenpunt met het keuzelabel; daarachter waaieren de kinderen uit.
766
+ const takId = `tak:${ctx.step.storeId}:${field.key}:${sleutel}`;
767
+ ctx.nodes.push({
768
+ id: takId,
769
+ kind: "merge",
770
+ label: "",
771
+ state: takState,
772
+ ancestors: takAncestors,
773
+ help: [],
774
+ w: size.merge.w,
775
+ h: size.merge.h,
776
+ });
777
+ push({ from: id, to: takId, label: labelVan(tak.waarden), suffix: sleutel, state: takState, ancestors: takAncestors });
778
+ const sub = { ...ctx };
779
+ fan(sub, takId, takState, tak.kinderen, takAncestors, eigenMerge);
780
+ }
781
+ ctx.nodes.push({ id: eigenMerge, kind: "merge", label: "", state, ancestors, help: [], w: size.merge.w, h: size.merge.h });
782
+ push({ from: eigenMerge, to: mergeId, state, ancestors });
783
+ armen++;
784
+ }
785
+ if (armen === 0)
786
+ push({ from, to: mergeId, state: fromState, ancestors });
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
+ }