@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
@@ -12,124 +12,40 @@ import { buildDimensionItems, carrierDimensions } from "./dimensionData.server";
12
12
  // ---------------------------------------------------------------------------
13
13
  // Buffer
14
14
  // ---------------------------------------------------------------------------
15
- function normalizeBufferPart(value) {
16
- if (value == null)
17
- return "";
18
- if (Array.isArray(value)) {
19
- for (const entry of value) {
20
- const normalized = normalizeBufferPart(entry);
21
- if (normalized)
22
- return normalized;
23
- }
24
- return "";
25
- }
26
- if (typeof value === "object") {
27
- const o = value;
28
- return normalizeBufferPart(o.gln ?? o.locationGln ?? o.warehouseId ?? o.id ?? o.name ?? o.location ?? o.address?.addressLine);
29
- }
30
- return String(value).trim().toLowerCase().replace(/\s+/g, " ");
31
- }
32
- function pickFirstString(...values) {
33
- for (const value of values) {
34
- const normalized = normalizeBufferPart(value);
35
- if (normalized)
36
- return normalized;
37
- }
38
- return "";
39
- }
40
- export function buildBufferKey(order, mode) {
41
- const cf = (order.custom_fields ?? {});
42
- const pickupDate = pickFirstString(cf.pickup_moment, cf.pickup_date, cf.pickup_datetime, order.delivery_date);
43
- const location = pickFirstString(cf.delivery_location_gln, cf.destination_gln, cf.region_gln, cf.location_gln, cf.delivery_location_id, cf.delivery_location);
44
- const customer = normalizeBufferPart(order.customer_id);
45
- let key;
46
- switch (mode) {
47
- case "same_pickup":
48
- key = `p|${pickupDate}`;
49
- break;
50
- case "same_location":
51
- key = `l|${location}`;
52
- break;
53
- case "same_customer":
54
- key = `c|${customer}|${pickupDate}`;
55
- break;
56
- case "never":
57
- key = `order-${order.id}`;
58
- break;
59
- case "same_pickup_and_location":
60
- default:
61
- key = `pl|${pickupDate}|${location}`;
62
- }
63
- return { key, pickupDate, location };
64
- }
15
+ // De bufferkeuze is uitsluitend van de database (logistics_apply_buffer_decision,
16
+ // via trigger trg_orders_apply_buffer_decision). Deze code leest die keuze
17
+ // alleen; ze berekent geen eigen sleutel en schrijft nooit orders.buffer_id.
65
18
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
66
19
  export async function resolveBufferForOrderInternal(sb, orderId) {
67
- const { data: order } = await sb
68
- .from("orders")
69
- .select("id, customer_id, delivery_date, custom_fields, buffer_id")
70
- .eq("id", orderId)
71
- .maybeSingle();
72
- if (!order)
20
+ const readBuffer = async () => {
21
+ const { data } = await sb.from("orders").select("id, buffer_id").eq("id", orderId).maybeSingle();
22
+ if (!data)
23
+ return { found: false, bufferId: null };
24
+ return { found: true, bufferId: data.buffer_id ?? null };
25
+ };
26
+ const first = await readBuffer();
27
+ if (!first.found)
28
+ return null;
29
+ if (first.bufferId)
30
+ return first.bufferId;
31
+ // Nog geen buffer: laat de database één keer beslissen, en lees opnieuw.
32
+ const { error } = await sb.rpc("logistics_apply_buffer_decision", { p_order_id: orderId });
33
+ if (error) {
34
+ console.warn(`[buffer] bufferbesluit voor order ${orderId} mislukt: ${error.message}`);
73
35
  return null;
74
- const { data: settings } = await sb
75
- .from("cart_planning_settings")
76
- .select("buffer_grouping_mode")
77
- .eq("id", 1)
78
- .maybeSingle();
79
- const mode = settings?.buffer_grouping_mode ?? "same_pickup_and_location";
80
- const cf = { ...(order.custom_fields ?? {}) };
81
- const locId = cf.delivery_location_id;
82
- if (typeof locId === "string" && UUID_RE.test(locId) && !cf.delivery_location_gln) {
83
- const { data: loc } = await sb
84
- .from("customer_delivery_locations")
85
- .select("gln, name")
86
- .eq("id", locId)
87
- .maybeSingle();
88
- if (loc?.gln)
89
- cf.delivery_location_gln = loc.gln;
90
- if (loc?.name)
91
- cf.delivery_location_name = loc.name;
92
- }
93
- const { key, pickupDate, location } = buildBufferKey({ ...order, custom_fields: cf }, mode);
94
- let bufferId = null;
95
- const { data: existing } = await sb
96
- .from("order_buffers")
97
- .select("id")
98
- .eq("buffer_key", key)
99
- .maybeSingle();
100
- if (existing?.id) {
101
- bufferId = existing.id;
102
- }
103
- else {
104
- const { data: inserted, error } = await sb
105
- .from("order_buffers")
106
- .insert({
107
- buffer_key: key,
108
- grouping_mode: mode,
109
- pickup_date: pickupDate || null,
110
- pickup_location: location || null,
111
- customer_id: mode === "same_customer" ? order.customer_id : null,
112
- })
113
- .select("id")
114
- .single();
115
- if (error) {
116
- // Race: iemand anders maakte dezelfde buffer aan.
117
- const { data: retry } = await sb
118
- .from("order_buffers")
119
- .select("id")
120
- .eq("buffer_key", key)
121
- .maybeSingle();
122
- bufferId = retry?.id ?? null;
123
- }
124
- else {
125
- bufferId = inserted?.id ?? null;
126
- }
127
36
  }
128
- if (bufferId && order.buffer_id !== bufferId) {
129
- await sb.from("orders").update({ buffer_id: bufferId }).eq("id", orderId);
37
+ const second = await readBuffer();
38
+ if (!second.bufferId) {
39
+ console.warn(`[buffer] order ${orderId} heeft geen buffer volgens de werkwijze (buffer uit of reservering uitgesloten); niet ingedeeld.`);
130
40
  }
131
- return bufferId;
41
+ return second.bufferId;
132
42
  }
43
+ /**
44
+ * Een belading die uit Floriday komt (per regel of, als noodgreep, order-breed) of
45
+ * die een mens bewust heeft vastgezet, is leidend: het aantal lagen per ladingdrager
46
+ * wordt dan exact gevolgd.
47
+ */
48
+ const isAuthoritative = (s) => s === "handmatig" || s === "floriday_line" || s === "order_items" || s === "floriday_packing";
133
49
  const positive = (v) => {
134
50
  const n = Number(v);
135
51
  return Number.isFinite(n) && n > 0 ? Math.round(n) : null;
@@ -151,17 +67,58 @@ export function beladingFromFloridayPacking(packing) {
151
67
  return null;
152
68
  return { plants_per_crate: ppc, crates_per_layer: cpl, layers_per_cart: lpc };
153
69
  }
154
- /** Beladingbronnen in volgorde: Floriday-verpakking > orderregel > product > geen. */
155
- function resolveBelading(row, packing) {
156
- if (packing)
157
- return { belading: packing, source: "floriday_packing" };
158
- const fromRow = positive(row.plants_per_crate) != null || positive(row.crates_per_layer) != null || positive(row.layers_per_cart) != null
159
- ? {
160
- plants_per_crate: positive(row.plants_per_crate) ?? 1,
161
- crates_per_layer: positive(row.crates_per_layer) ?? 1,
162
- layers_per_cart: positive(row.layers_per_cart) ?? 1,
163
- }
164
- : null;
70
+ /**
71
+ * Leest per artikel de Floriday-verpakking van de orderregel zelf uit
72
+ * `orders.custom_fields.floriday_lines`. Dit is de enige bron die per regel klopt;
73
+ * `custom_fields.floriday_packing` geldt voor de hele order.
74
+ */
75
+ export function beladingPerRegelUitOrder(customFields) {
76
+ const out = new Map();
77
+ const cf = (customFields ?? {});
78
+ const lines = Array.isArray(cf.floriday_lines) ? cf.floriday_lines : [];
79
+ for (const raw of lines) {
80
+ const line = (raw ?? {});
81
+ const tradeItemId = line.trade_item_id ? String(line.trade_item_id) : null;
82
+ if (!tradeItemId)
83
+ continue;
84
+ const belading = beladingFromFloridayPacking(line.packing);
85
+ if (belading)
86
+ out.set(tradeItemId, belading);
87
+ }
88
+ return out;
89
+ }
90
+ /** Waarden van de orderregel zelf, of null als de regel niets heeft. */
91
+ function beladingVanRegel(row) {
92
+ if (positive(row.plants_per_crate) == null &&
93
+ positive(row.crates_per_layer) == null &&
94
+ positive(row.layers_per_cart) == null) {
95
+ return null;
96
+ }
97
+ return {
98
+ plants_per_crate: positive(row.plants_per_crate) ?? 1,
99
+ crates_per_layer: positive(row.crates_per_layer) ?? 1,
100
+ layers_per_cart: positive(row.layers_per_cart) ?? 1,
101
+ };
102
+ }
103
+ /**
104
+ * Beladingbronnen in vaste volgorde:
105
+ * 1. handmatig vastgezette regel (packing_locked_at);
106
+ * 2. de Floriday-verpakking van díe regel (op artikel gekoppeld);
107
+ * 3. de eigen waarden van de bestelregel;
108
+ * 4. het product;
109
+ * 5. de order-brede Floriday-verpakking — uitsluitend als allerlaatste noodgreep
110
+ * voor een regel die van 1 t/m 4 niets heeft;
111
+ * 6. anders onbekend: er wordt niet gegokt, de indeling blokkeert.
112
+ */
113
+ function resolveBelading(row, packing, linePacking = null) {
114
+ const fromRow = beladingVanRegel(row);
115
+ // 1. Handmatig vastgezet: niemand overschrijft dat, ook Floriday niet.
116
+ if (row.packing_locked_at && fromRow)
117
+ return { belading: fromRow, source: "handmatig" };
118
+ // 2. De Floriday-verpakking van deze regel.
119
+ if (linePacking)
120
+ return { belading: linePacking, source: "floriday_line" };
121
+ // 3. De eigen waarden van de bestelregel.
165
122
  if (fromRow)
166
123
  return { belading: fromRow, source: "order_items" };
167
124
  const p = row.products ?? null;
@@ -172,8 +129,13 @@ function resolveBelading(row, packing) {
172
129
  layers_per_cart: positive(p.layers_per_cart) ?? 1,
173
130
  }
174
131
  : null;
132
+ // 4. Het product.
175
133
  if (fromProduct)
176
134
  return { belading: fromProduct, source: "product" };
135
+ // 5. Allerlaatste noodgreep: de order-brede Floriday-verpakking.
136
+ if (packing)
137
+ return { belading: packing, source: "floriday_packing" };
138
+ // 6. Onbekend.
177
139
  return { belading: { plants_per_crate: 1, crates_per_layer: 1, layers_per_cart: 1 }, source: "geen" };
178
140
  }
179
141
  export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
@@ -197,8 +159,15 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
197
159
  groupSet = new Set((groups ?? []).map((g) => g.id));
198
160
  }
199
161
  for (const row of rows) {
200
- const packing = row.order_id ? options.packingByOrder?.get(String(row.order_id)) ?? null : null;
201
- const { belading: base, source } = resolveBelading(row, packing);
162
+ const orderKey = row.order_id ? String(row.order_id) : null;
163
+ const packing = orderKey ? options.packingByOrder?.get(orderKey) ?? null : null;
164
+ const lineMap = orderKey ? options.packingByOrderLine?.get(orderKey) ?? null : null;
165
+ const linePacking = lineMap
166
+ ? lineMap.get(String(row.product_id ?? "")) ??
167
+ lineMap.get(String(row.products?.floriday_trade_item_id ?? "")) ??
168
+ null
169
+ : null;
170
+ const { belading: base, source } = resolveBelading(row, packing, linePacking);
202
171
  const plants = Math.max(0, Number(row.quantity ?? 0));
203
172
  const groupNameKey = String(row.product_name ?? "")
204
173
  .replace(/^\[Groep\]\s*/i, "")
@@ -218,7 +187,7 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
218
187
  product_name: row.product_name ?? "Onbekend",
219
188
  plants,
220
189
  ...base,
221
- packing_authoritative: source === "floriday_packing",
190
+ packing_authoritative: isAuthoritative(source),
222
191
  weight_per_crate: row.products?.weight != null ? Number(row.products.weight) : null,
223
192
  });
224
193
  continue;
@@ -241,7 +210,7 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
241
210
  product_name: row.product_name ?? "Onbekend",
242
211
  plants,
243
212
  ...base,
244
- packing_authoritative: source === "floriday_packing",
213
+ packing_authoritative: isAuthoritative(source),
245
214
  weight_per_crate: null,
246
215
  });
247
216
  continue;
@@ -259,21 +228,36 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
259
228
  }
260
229
  const resolved = memberList.map((m) => {
261
230
  const p = prodByTrade.get(String(m.trade_item_id));
262
- // Een Floriday-verpakking geldt voor de hele orderregel, dus ook voor de leden.
263
- if (packing)
264
- return { m, p, belading: { ...base }, source: "floriday_packing" };
231
+ // Een handmatig vastgezette regel blijft leidend voor de hele groep.
232
+ if (source === "handmatig") {
233
+ return { m, p, belading: { ...base }, source };
234
+ }
235
+ // 1. De Floriday-verpakking van dít lid.
236
+ const eigenLijn = lineMap?.get(String(m.trade_item_id)) ?? (p ? lineMap?.get(String(p.id)) ?? null : null);
237
+ if (eigenLijn)
238
+ return { m, p, belading: { ...eigenLijn }, source: "floriday_line" };
239
+ // 2. Het artikel zelf. Nooit de belading van de groepsregel of van een ander lid.
265
240
  const known = p &&
266
241
  (positive(p.plants_per_crate) != null ||
267
242
  positive(p.crates_per_layer) != null ||
268
243
  positive(p.layers_per_cart) != null);
269
- const belading = known
270
- ? {
271
- plants_per_crate: positive(p.plants_per_crate) ?? 1,
272
- crates_per_layer: positive(p.crates_per_layer) ?? 1,
273
- layers_per_cart: positive(p.layers_per_cart) ?? 1,
274
- }
275
- : { ...base };
276
- return { m, p, belading, source: (known ? "product" : source) };
244
+ if (known) {
245
+ return {
246
+ m,
247
+ p,
248
+ belading: {
249
+ plants_per_crate: positive(p.plants_per_crate) ?? 1,
250
+ crates_per_layer: positive(p.crates_per_layer) ?? 1,
251
+ layers_per_cart: positive(p.layers_per_cart) ?? 1,
252
+ },
253
+ source: "product",
254
+ };
255
+ }
256
+ // 3. Allerlaatste noodgreep: de order-brede Floriday-verpakking van de order.
257
+ if (packing)
258
+ return { m, p, belading: { ...packing }, source: "floriday_packing" };
259
+ // 4. Onbekend: er wordt niet gegokt, de indeling blokkeert.
260
+ return { m, p, belading: { ...base }, source: "geen" };
277
261
  });
278
262
  const weightSum = resolved.reduce((s, r) => s + Number(r.m.quantity ?? 1), 0) || resolved.length;
279
263
  const shares = [];
@@ -302,7 +286,7 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
302
286
  product_name: `${row.product_name ?? "Groep"} — ${r.p?.product ?? r.m.trade_item_id}`,
303
287
  plants: shares[idx],
304
288
  ...r.belading,
305
- packing_authoritative: r.source === "floriday_packing",
289
+ packing_authoritative: isAuthoritative(r.source),
306
290
  weight_per_crate: r.p?.weight != null ? Number(r.p.weight) : null,
307
291
  });
308
292
  });
@@ -347,10 +331,29 @@ export async function loadBeladingRules(sb) {
347
331
  maxCustomers: k.fill_customers_as_much_as_fits ? null : numOrNull(k.max_customers),
348
332
  maxLayers: k.fill_layers_as_much_as_fits ? null : numOrNull(k.max_layers),
349
333
  maxBoxes: k.fill_boxes_as_much_as_fits ? null : numOrNull(k.max_boxes_per_pallet),
334
+ maxCustomersPerLayer: numOrNull(k.max_customers_per_layer),
350
335
  optimalFill: k.optimal_fill !== false,
351
336
  layoutMethod: k.cart_layout_method === "afmetingen" ? "afmetingen" : "belading",
352
337
  };
353
338
  }
339
+ /**
340
+ * Wanneer ligt de karrenindeling vast?
341
+ * - "per_cart": alleen de geprinte/gescande/gecontroleerde kar zelf.
342
+ * - "first_packing_slip": zodra de eerste pakbon van de order of buffer is
343
+ * geprint, blijft de hele bestaande indeling van die order/buffer staan.
344
+ * Ontbreekt de instelling, dan geldt "per_cart".
345
+ */
346
+ export async function loadCartPlanFreeze(sb) {
347
+ const { data } = await sb
348
+ .from("logistics_rule_configs")
349
+ .select("settings")
350
+ .eq("is_active", true)
351
+ .order("updated_at", { ascending: false })
352
+ .limit(1);
353
+ const settings = Array.isArray(data) && data.length > 0 ? data[0]?.settings : null;
354
+ const k = settings?.karrenindeling?.fields ?? settings?.karrenindeling;
355
+ return k?.cart_plan_freeze === "first_packing_slip" ? "first_packing_slip" : "per_cart";
356
+ }
354
357
  /** Past de grenzen uit de werkwijze toe op het capaciteitsprofiel van een ladingdrager. */
355
358
  export function applyRulesToProfile(profile, rules) {
356
359
  if (!rules)
@@ -550,6 +553,53 @@ export function dimensionCartsToPlannedUnits(carts) {
550
553
  // ---------------------------------------------------------------------------
551
554
  /** Maximaal aantal ladingdragers dat één orderbuffer mag opleveren. */
552
555
  const MAX_CARTS_PER_BUFFER = 500;
556
+ /** Statussen waarbij een order niet meer ingedeeld hoeft te worden. */
557
+ const TERMINALE_STATUSSEN = new Set([
558
+ "verzonden",
559
+ "afgehandeld",
560
+ "geannuleerd",
561
+ "cancelled",
562
+ "geleverd",
563
+ "afgerond",
564
+ ]);
565
+ /** Reservering of al afgehandelde order: niet plannen, ook niet blokkeren. */
566
+ function isReservationOrTerminal(order) {
567
+ if (!order)
568
+ return true;
569
+ if (order.is_reservation === true)
570
+ return true;
571
+ const status = String(order.status ?? "").trim().toLowerCase();
572
+ return TERMINALE_STATUSSEN.has(status);
573
+ }
574
+ /**
575
+ * Duidelijk teveel identieke bestelregels wijst op een synchronisatie die
576
+ * regels toevoegt in plaats van bijwerkt. Dan niet rekenen: een indeling op
577
+ * opgeblazen aantallen is fout én loopt in een timeout.
578
+ */
579
+ const DUPES_MIN_REGELS = 50;
580
+ const DUPES_FACTOR = 3;
581
+ function telDubbeleRegels(rows) {
582
+ const groepen = new Map();
583
+ const producten = new Set();
584
+ for (const r of rows) {
585
+ const product = String(r["product_id"] ?? r["product_name"] ?? "");
586
+ producten.add(product);
587
+ const sleutel = [
588
+ product,
589
+ r["quantity"] ?? 0,
590
+ r["plants_per_crate"] ?? 0,
591
+ r["crates_per_layer"] ?? 0,
592
+ r["layers_per_cart"] ?? 0,
593
+ ].join("|");
594
+ groepen.set(sleutel, (groepen.get(sleutel) ?? 0) + 1);
595
+ }
596
+ return {
597
+ regels: rows.length,
598
+ uniek: groepen.size,
599
+ kopieen: rows.length - groepen.size,
600
+ producten: producten.size,
601
+ };
602
+ }
553
603
  /**
554
604
  * Bepaalt de definitieve, persistente identiteit van iedere fysieke kar.
555
605
  * Bij een bufferorder met meerdere orders zijn alle karren gedeelde
@@ -643,7 +693,62 @@ export async function bufferOrdersWithoutCarts(sb, bufferId, orderIds) {
643
693
  }
644
694
  return ids.filter((id) => !withCarts.has(id));
645
695
  }
696
+ /**
697
+ * Auditpunt 2: één buffer wordt nooit door twee processen tegelijk herschreven.
698
+ *
699
+ * De lees-verwijder-herschrijf-cyclus hieronder loopt over veel losse aanroepen
700
+ * en past dus niet in één databasetransactie; een advisory xact lock zou meteen
701
+ * na de eerste aanroep weer vrijvallen. Daarom een kortlopende lease met
702
+ * vervaltijd (logistics_plan_lock_acquire), die wél geldt over de planwachtrij,
703
+ * de massa-herberekening en een openstaand scherm heen.
704
+ *
705
+ * Lukt de vergrendeling niet, dan volgt een nette "wordt al bijgewerkt"-uitkomst
706
+ * in plaats van een tweede, botsende herberekening.
707
+ */
646
708
  export async function replanBufferInternal(sb, bufferId, options = {}) {
709
+ // Een proefberekening schrijft niets weg en hoeft dus niets te vergrendelen.
710
+ if (options.dryRun)
711
+ return replanBufferUnlocked(sb, bufferId, options);
712
+ const lockKey = `buffer:${bufferId}`;
713
+ let held = false;
714
+ try {
715
+ const { data, error } = await sb.rpc("logistics_plan_lock_acquire", {
716
+ _key: lockKey,
717
+ _owner: "logistiek-engine",
718
+ _ttl_seconds: 600,
719
+ });
720
+ held = !error && data === true;
721
+ }
722
+ catch {
723
+ held = false;
724
+ }
725
+ if (!held) {
726
+ return {
727
+ ok: true,
728
+ bufferId,
729
+ orderIds: [],
730
+ cartsCreated: 0,
731
+ replacedManualCount: 0,
732
+ usedFloridayPacking: false,
733
+ warnings: [BUFFER_BUSY_MESSAGE],
734
+ skipped: "locked",
735
+ };
736
+ }
737
+ try {
738
+ return await replanBufferUnlocked(sb, bufferId, options);
739
+ }
740
+ finally {
741
+ try {
742
+ await sb.rpc("logistics_plan_lock_release", { _key: lockKey, _owner: "logistiek-engine" });
743
+ }
744
+ catch {
745
+ // Vrijgeven mag nooit de uitkomst omgooien; de lease verloopt vanzelf.
746
+ }
747
+ }
748
+ }
749
+ /** Herbruikbare melding als een buffer al door een ander proces wordt bijgewerkt. */
750
+ export const BUFFER_BUSY_MESSAGE = "Deze orderbuffer wordt op dit moment al bijgewerkt; probeer het zo opnieuw.";
751
+ async function replanBufferUnlocked(sb, bufferId, options = {}) {
647
752
  const warnings = [];
648
753
  // Bevroren karren blijven staan; al het overige van de héle buffer wordt
649
754
  // samen ingedeeld, zodat orders van de buffer op dezelfde karren komen.
@@ -651,11 +756,17 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
651
756
  const isSupplement = keepCartIds.length > 0;
652
757
  const { data: orders } = await sb
653
758
  .from("orders")
654
- .select("id, order_number, customer_id, custom_fields, plan_locked_at")
759
+ .select("id, order_number, customer_id, custom_fields, plan_locked_at, status, is_reservation")
655
760
  .eq("buffer_id", bufferId)
656
761
  .is("deleted_at", null);
657
762
  const allOrderRows = Array.isArray(orders) ? orders : [];
658
- const orderRows = allOrderRows;
763
+ // Reserveringen en al afgeronde orders horen niet in de indeling thuis. Ze
764
+ // leveren nooit karren op en zouden anders de hele buffer laten blokkeren.
765
+ const orderRows = allOrderRows.filter((o) => !isReservationOrTerminal(o));
766
+ const overgeslagen = allOrderRows.length - orderRows.length;
767
+ if (overgeslagen > 0) {
768
+ warnings.push(`${overgeslagen} order(s) in deze buffer zijn overgeslagen: reservering of al afgehandeld.`);
769
+ }
659
770
  const orderIds = orderRows.map((o) => o.id);
660
771
  // Binnen een bufferorder mogen orders en klanten altijd samen op een kar.
661
772
  const isBufferOrder = orderRows.length > 1;
@@ -688,8 +799,10 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
688
799
  };
689
800
  }
690
801
  // Een eenmaal geprinte, gescande of gecontroleerde fysieke kar heeft een
691
- // blijvende identiteit. Ook vóór de formele vrijgavetijd mag een replan die
692
- // id, het nummer of total_carts daarom niet meer vervangen.
802
+ // blijvende identiteit. Let op: een klaargezette barcode is nog géén bewijs
803
+ // van printen, en een wachtende, mislukte of geannuleerde printopdracht ook
804
+ // niet. Alleen een voltooide print of een echte scan-/controlehandeling
805
+ // bevriest een kar.
693
806
  if (!isSupplement) {
694
807
  const { data: existingCartRows, error: existingCartError } = await sb
695
808
  .from("order_carts")
@@ -702,12 +815,15 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
702
815
  .map((cart) => String(cart.id ?? ""))
703
816
  .filter(Boolean);
704
817
  if (existingCartIds.length > 0) {
705
- const [controlResult, barcodeResult, printResult] = await Promise.all([
818
+ const [controlResult, printResult] = await Promise.all([
706
819
  sb.from("cart_control_items").select("cart_number_id").in("cart_number_id", existingCartIds),
707
- sb.from("pakbon_barcode_inhoud").select("cart_id").in("cart_id", existingCartIds),
708
- sb.from("print_jobs").select("id, payload").in("payload->>cart_id", existingCartIds),
820
+ sb
821
+ .from("print_jobs")
822
+ .select("id, payload")
823
+ .eq("status", "voltooid")
824
+ .in("payload->>cart_id", existingCartIds),
709
825
  ]);
710
- const lockCheckError = controlResult.error ?? barcodeResult.error ?? printResult.error;
826
+ const lockCheckError = controlResult.error ?? printResult.error;
711
827
  if (lockCheckError) {
712
828
  throw new Error(`Karvergrendeling kon niet veilig worden gecontroleerd: ${lockCheckError.message}`);
713
829
  }
@@ -717,11 +833,6 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
717
833
  if (id)
718
834
  frozen.add(String(id));
719
835
  }
720
- for (const r of barcodeResult.data ?? []) {
721
- const id = r?.cart_id;
722
- if (id)
723
- frozen.add(String(id));
724
- }
725
836
  for (const r of printResult.data ?? []) {
726
837
  const id = r?.payload?.cart_id;
727
838
  if (id)
@@ -729,19 +840,26 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
729
840
  }
730
841
  const frozenIds = existingCartIds.filter((id) => frozen.has(id));
731
842
  if (frozenIds.length > 0) {
732
- // Geprinte, gescande of gecontroleerde karren blijven exact zoals ze zijn.
733
- // Al het overige van de buffer wordt in één ronde samen ingedeeld, zodat
734
- // de resterende fusten van alle orders op dezelfde karren komen.
843
+ // Instelling uit de werkwijze: alleen de geprinte kar vastzetten, of de
844
+ // hele order/buffer zodra de eerste pakbon is geprint.
845
+ const freezeMode = await loadCartPlanFreeze(sb);
846
+ const keep = freezeMode === "first_packing_slip" ? existingCartIds : frozenIds;
847
+ // Bevroren karren blijven exact zoals ze zijn. Al het overige wordt in
848
+ // één ronde samen ingedeeld, zodat de resterende fusten van alle orders
849
+ // op dezelfde karren komen.
735
850
  const supplement = await replanBufferInternal(sb, bufferId, {
736
851
  ...options,
737
- keepCartIds: frozenIds,
852
+ keepCartIds: keep,
738
853
  });
739
854
  return {
740
855
  ...supplement,
741
856
  warnings: [
742
857
  ...(supplement.warnings ?? []),
743
- `${frozenIds.length} reeds geprinte of gecontroleerde kar(ren) zijn ongewijzigd gebleven; ` +
744
- "de overige fusten van de buffer zijn samen opnieuw ingedeeld.",
858
+ freezeMode === "first_packing_slip"
859
+ ? `De eerste pakbon van deze order of buffer is al geprint; alle ${keep.length} bestaande kar(ren) blijven ongewijzigd. ` +
860
+ "Alleen fusten die nog nergens liggen zijn als extra kar toegevoegd."
861
+ : `${keep.length} reeds geprinte of gecontroleerde kar(ren) zijn ongewijzigd gebleven; ` +
862
+ "de overige fusten van de buffer zijn samen opnieuw ingedeeld.",
745
863
  ],
746
864
  };
747
865
  }
@@ -751,22 +869,86 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
751
869
  const { data: items } = await sb
752
870
  .from("order_items")
753
871
  .select("id, order_id, product_id, product_name, quantity, plants_per_crate, crates_per_layer, layers_per_cart, " +
754
- "products(plants_per_crate, crates_per_layer, layers_per_cart, weight)")
872
+ "packing_locked_at, packing_locked_by, " +
873
+ "products(plants_per_crate, crates_per_layer, layers_per_cart, weight, floriday_trade_item_id)")
755
874
  .in("order_id", idList);
875
+ // Duidelijk teveel identieke bestelregels: niet rekenen maar blokkeren.
876
+ // Een indeling op opgeblazen aantallen is fout en loopt in een timeout.
877
+ {
878
+ const perOrder = new Map();
879
+ for (const it of Array.isArray(items) ? items : []) {
880
+ const id = String(it.order_id ?? "");
881
+ const lijst = perOrder.get(id) ?? [];
882
+ lijst.push(it);
883
+ perOrder.set(id, lijst);
884
+ }
885
+ const verdacht = [];
886
+ for (const [id, rijen] of perOrder) {
887
+ const telling = telDubbeleRegels(rijen);
888
+ if (telling.regels >= DUPES_MIN_REGELS && telling.regels > telling.uniek * DUPES_FACTOR) {
889
+ const order = orderRows.find((o) => String(o.id) === id);
890
+ verdacht.push({ orderId: id, orderRef: String(order?.order_number ?? id), telling });
891
+ }
892
+ }
893
+ if (verdacht.length > 0) {
894
+ const reason = "Dubbele bestelregels gevonden: " +
895
+ verdacht
896
+ .map((v) => `${v.orderRef} heeft ${v.telling.regels} bestelregels voor ${v.telling.producten} product(en) ` +
897
+ `(${v.telling.kopieen} kopie(ën))`)
898
+ .join("; ") +
899
+ ". Er is niets ingedeeld; de kopieën moeten eerst worden opgeruimd en de bron ervan hersteld.";
900
+ console.error(`[logistiek-engine] buffer ${bufferId} geblokkeerd: ${reason}`);
901
+ if (!options.dryRun) {
902
+ const { error: blockError } = await sb.rpc("logistics_plan_block", {
903
+ p_order_ids: orderIds,
904
+ p_reason: reason,
905
+ p_details: {
906
+ reason_code: "dubbele_regels",
907
+ bufferId,
908
+ path: "typescript",
909
+ orders: verdacht.map((v) => ({ orderRef: v.orderRef, ...v.telling })),
910
+ },
911
+ });
912
+ if (blockError)
913
+ warnings.push(`Blokkade kon niet worden vastgelegd: ${blockError.message}`);
914
+ }
915
+ return {
916
+ ok: false,
917
+ bufferId,
918
+ orderIds,
919
+ cartsCreated: 0,
920
+ replacedManualCount: 0,
921
+ usedFloridayPacking: false,
922
+ warnings: [...warnings, reason],
923
+ skipped: "blocked",
924
+ blockedReason: reason,
925
+ };
926
+ }
927
+ }
756
928
  const customerByOrder = new Map(orderRows.map((o) => [o.id, o.customer_id ?? null]));
757
929
  const rawRows = (Array.isArray(items) ? items : []).map((it) => ({
758
930
  ...it,
759
931
  customer_id: customerByOrder.get(it.order_id) ?? null,
760
932
  }));
761
- // Floriday-verpakking per order is de eerste beladingbron.
933
+ // Order-brede Floriday-verpakking: alleen nog als allerlaatste noodgreep.
762
934
  const packingByOrder = new Map();
935
+ // Floriday-verpakking per orderregel, gekoppeld op artikel. Dit is de bron die
936
+ // per regel klopt en gaat voor op de order-brede waarde.
937
+ const packingByOrderLine = new Map();
763
938
  for (const o of orderRows) {
764
939
  const belading = beladingFromFloridayPacking(o.custom_fields?.floriday_packing);
765
940
  if (belading)
766
941
  packingByOrder.set(String(o.id), belading);
942
+ const perRegel = beladingPerRegelUitOrder(o.custom_fields);
943
+ if (perRegel.size > 0)
944
+ packingByOrderLine.set(String(o.id), perRegel);
767
945
  }
768
946
  const sources = new Map();
769
- let inputs = await expandOrderItemsForPlanner(sb, rawRows, { packingByOrder, sources });
947
+ let inputs = await expandOrderItemsForPlanner(sb, rawRows, {
948
+ packingByOrder,
949
+ packingByOrderLine,
950
+ sources,
951
+ });
770
952
  // Wat al op een bevroren kar ligt, wordt niet opnieuw ingedeeld.
771
953
  let frozenCartCount = 0;
772
954
  let cartNumberStart = 1;
@@ -896,8 +1078,17 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
896
1078
  // daarom verwijderd.)
897
1079
  const solo = orderRows.length === 1 ? orderRows[0] : null;
898
1080
  const soloPacking = solo ? beladingFromFloridayPacking(solo.custom_fields?.floriday_packing) : null;
1081
+ // Het overname-pad legt één verpakking over de hele order. Dat mag alleen als
1082
+ // elke regel toch al exact die verpakking heeft; anders rekent de gewone
1083
+ // planner, die per regel met de eigen belading werkt.
1084
+ const uniformeBelading = inputs.length > 0 &&
1085
+ soloPacking != null &&
1086
+ inputs.every((i) => i.plants_per_crate === soloPacking.plants_per_crate &&
1087
+ i.crates_per_layer === soloPacking.crates_per_layer &&
1088
+ i.layers_per_cart === soloPacking.layers_per_cart);
899
1089
  const takeoverProfile = profileFor(profiles, options.carrierType ?? defaultProfile.code);
900
- const takeoverAllowed = takeoverProfile.capacity_model === "layered" || takeoverProfile.capacity_model === "stacked";
1090
+ const takeoverAllowed = (takeoverProfile.capacity_model === "layered" || takeoverProfile.capacity_model === "stacked") &&
1091
+ uniformeBelading;
901
1092
  if (useDimensions) {
902
1093
  // Globale methode: indelen op basis van fysieke afmetingen.
903
1094
  const carrierRow = profiles.find((p) => p.code === (options.carrierType ?? defaultProfile.code)) ?? null;
@@ -1008,6 +1199,7 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
1008
1199
  maxOrders: isBufferOrder ? null : rules?.maxOrders ?? null,
1009
1200
  maxCustomers: isBufferOrder ? null : rules?.maxCustomers ?? settings?.max_customers_per_cart ?? null,
1010
1201
  oneCustomerPerLayer: Boolean(settings?.max_one_customer_per_layer),
1202
+ maxCustomersPerLayer: rules?.maxCustomersPerLayer ?? null,
1011
1203
  ownCarrierFromLayers: settings?.own_cart_from_layers ?? null,
1012
1204
  sortOrder: "none",
1013
1205
  });
@@ -1020,8 +1212,13 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
1020
1212
  }
1021
1213
  // Veiligheidsgrens: een onrealistisch grote uitkomst wordt niet afgekapt maar
1022
1214
  // geblokkeerd. Een half plan laat planten stilzwijgend uit de pickstroom vallen.
1023
- if (plannedCarts.length > MAX_CARTS_PER_BUFFER) {
1024
- const reason = `Deze orderbuffer levert ${plannedCarts.length} ladingdragers op, meer dan de grens van ` +
1215
+ // Auditpunt 2: de grens telt cumulatief. Bij aanvullen na bevroren karren
1216
+ // tellen die bevroren karren mee, zodat de grens niet in kleine stapjes
1217
+ // omzeild kan worden.
1218
+ const totalCartsAfterPlan = plannedCarts.length + frozenCartCount;
1219
+ if (totalCartsAfterPlan > MAX_CARTS_PER_BUFFER) {
1220
+ const reason = `Deze orderbuffer komt uit op ${totalCartsAfterPlan} ladingdragers ` +
1221
+ `(${frozenCartCount} al vastgelegd + ${plannedCarts.length} nieuw), meer dan de grens van ` +
1025
1222
  `${MAX_CARTS_PER_BUFFER}. Er is niets ingedeeld; controleer de aantallen of de belading van de producten.`;
1026
1223
  console.error(`[logistiek-engine] buffer ${bufferId} geblokkeerd: ${reason}`);
1027
1224
  if (!options.dryRun) {
@@ -1032,7 +1229,9 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
1032
1229
  reason_code: "kargrens_overschreden",
1033
1230
  bufferId,
1034
1231
  path: "typescript",
1035
- carts: plannedCarts.length,
1232
+ carts: totalCartsAfterPlan,
1233
+ newCarts: plannedCarts.length,
1234
+ frozenCarts: frozenCartCount,
1036
1235
  limit: MAX_CARTS_PER_BUFFER,
1037
1236
  beladingSources,
1038
1237
  },
@@ -1139,37 +1338,167 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
1139
1338
  await del().in("order_id", ids);
1140
1339
  }
1141
1340
  };
1142
- await clearExistingCarts();
1143
- // In blokken wegschrijven: per ladingdrager een aparte insert is bij grote
1144
- // orders veel te traag.
1145
- let created = 0;
1146
- const CHUNK = 100;
1147
- for (let start = 0; start < plannedCarts.length; start += CHUNK) {
1148
- const chunk = plannedCarts.slice(start, start + CHUNK);
1149
- const cartRows = chunk.map((cart, chunkIndex) => {
1150
- const cartIndex = start + chunkIndex;
1151
- const identity = persistentIdentities[cartIndex];
1152
- if (!identity) {
1153
- throw new Error(`Karrenindeling ongeldig: identiteit ontbreekt voor kar ${cartIndex + 1}.`);
1341
+ // ---------------------------------------------------------------------------
1342
+ // Vergelijken in plaats van wissen. Een kar die exact hetzelfde blijft, blijft
1343
+ // staan: zelfde rij, zelfde id, zelfde barcode. Alleen echte verschillen worden
1344
+ // geschreven. Dit voorkomt dat elke herberekening honderdduizenden karren en
1345
+ // logboekregels produceert.
1346
+ // ---------------------------------------------------------------------------
1347
+ const getal = (v) => v === null || v === undefined || v === "" ? "" : Number.isFinite(Number(v)) ? Number(v).toFixed(3) : String(v);
1348
+ const itemSig = (r) => [
1349
+ String(r["order_item_id"] ?? ""),
1350
+ String(r["product_id"] ?? ""),
1351
+ getal(r["crates"]),
1352
+ getal(r["boxes"]),
1353
+ getal(r["weight_kg"]),
1354
+ getal(r["plants_per_crate"]),
1355
+ getal(r["crates_per_layer"]),
1356
+ getal(r["layers_per_cart"]),
1357
+ getal(r["layer_fraction"]),
1358
+ getal(r["layer_share_pct"]),
1359
+ getal(r["layer_index"]),
1360
+ getal(r["crates_in_layer"]),
1361
+ ].join("|");
1362
+ const karSig = (row, items) => [
1363
+ String(row["order_id"] ?? "gedeeld"),
1364
+ getal(row["cart_number"]),
1365
+ getal(row["total_carts"]),
1366
+ String(row["carrier_type"] ?? ""),
1367
+ String(row["carrier_type_id"] ?? ""),
1368
+ getal(row["max_layers"]),
1369
+ getal(row["total_layers_used"]),
1370
+ getal(row["occupancy_pct"]),
1371
+ getal(row["weight_kg"]),
1372
+ String(row["layout_method"] ?? ""),
1373
+ getal(row["algorithm_version"]),
1374
+ String(row["is_complete"] ?? ""),
1375
+ String(row["source"] ?? ""),
1376
+ ].join("|") +
1377
+ "#" +
1378
+ items.map(itemSig).sort().join(";");
1379
+ const gewenst = plannedCarts.map((cart, cartIndex) => {
1380
+ const identity = persistentIdentities[cartIndex];
1381
+ if (!identity) {
1382
+ throw new Error(`Karrenindeling ongeldig: identiteit ontbreekt voor kar ${cartIndex + 1}.`);
1383
+ }
1384
+ const row = {
1385
+ order_id: identity.ownerOrderId,
1386
+ buffer_id: bufferId,
1387
+ cart_number: identity.cartNumber,
1388
+ total_carts: identity.totalCarts,
1389
+ is_auto: true,
1390
+ is_complete: cart.is_complete,
1391
+ source: usedFloridayPacking ? "floriday" : "auto",
1392
+ occupancy_pct: cart.occupancy_pct,
1393
+ total_layers_used: cart.total_layers_used,
1394
+ carrier_type: cart.carrier_type,
1395
+ carrier_type_id: cart.carrier_type_id,
1396
+ max_layers: cart.max_layers,
1397
+ weight_kg: cart.weight_kg,
1398
+ algorithm_version: useDimensions ? DIMENSION_PLANNER_VERSION : LOAD_CARRIER_PLANNER_VERSION,
1399
+ layout_method: useDimensions ? "afmetingen" : "belading",
1400
+ };
1401
+ const items = cart.items.map((i) => ({
1402
+ order_item_id: i.order_item_id.includes("::") ? i.order_item_id.split("::")[0] : i.order_item_id,
1403
+ product_id: i.product_id,
1404
+ product_name: i.product_name,
1405
+ crates: Math.round(i.crates),
1406
+ boxes: i.boxes ?? null,
1407
+ weight_kg: i.weight_kg ?? null,
1408
+ plants_per_crate: i.plants_per_crate,
1409
+ crates_per_layer: i.crates_per_layer,
1410
+ layers_per_cart: i.layers_per_cart,
1411
+ layer_fraction: i.layer_fraction,
1412
+ layer_share_pct: i.layer_share_pct,
1413
+ layer_index: i.layer_index ?? null,
1414
+ crates_in_layer: i.crates_in_layer ?? null,
1415
+ }));
1416
+ return { planIndex: cartIndex, row, items, sig: karSig(row, items) };
1417
+ });
1418
+ // Bestaande (niet-bevroren) karren binnen dezelfde scope inlezen.
1419
+ const scopeOr = orderIds.length > 0
1420
+ ? `buffer_id.eq.${bufferId},order_id.in.(${orderIds.join(",")})`
1421
+ : `buffer_id.eq.${bufferId}`;
1422
+ const { data: huidigeKarren, error: huidigeError } = await sb
1423
+ .from("order_carts")
1424
+ .select("id, order_id, buffer_id, cart_number, total_carts, is_complete, source, occupancy_pct, " +
1425
+ "total_layers_used, carrier_type, carrier_type_id, max_layers, weight_kg, algorithm_version, layout_method")
1426
+ .or(scopeOr);
1427
+ if (huidigeError) {
1428
+ throw new Error(`Bestaande karren konden niet worden vergeleken: ${huidigeError.message}`);
1429
+ }
1430
+ const bevroren = new Set(keepCartIds.map(String));
1431
+ const vergelijkbaar = (Array.isArray(huidigeKarren) ? huidigeKarren : []).filter((c) => !bevroren.has(String(c.id)));
1432
+ const huidigeIds = vergelijkbaar.map((c) => String(c.id));
1433
+ const huidigeItems = new Map();
1434
+ for (let s = 0; s < huidigeIds.length; s += 200) {
1435
+ const slice = huidigeIds.slice(s, s + 200);
1436
+ if (slice.length === 0)
1437
+ continue;
1438
+ const { data: rijen, error: itemsError } = await sb
1439
+ .from("order_cart_items")
1440
+ .select("cart_id, order_item_id, product_id, crates, boxes, weight_kg, plants_per_crate, " +
1441
+ "crates_per_layer, layers_per_cart, layer_fraction, layer_share_pct, layer_index, crates_in_layer")
1442
+ .in("cart_id", slice);
1443
+ if (itemsError) {
1444
+ throw new Error(`Inhoud van bestaande karren kon niet worden gelezen: ${itemsError.message}`);
1445
+ }
1446
+ for (const r of Array.isArray(rijen) ? rijen : []) {
1447
+ const id = String(r.cart_id);
1448
+ const lijst = huidigeItems.get(id) ?? [];
1449
+ lijst.push(r);
1450
+ huidigeItems.set(id, lijst);
1451
+ }
1452
+ }
1453
+ const beschikbaarPerSig = new Map();
1454
+ for (const c of vergelijkbaar) {
1455
+ const id = String(c.id);
1456
+ const sig = karSig(c, huidigeItems.get(id) ?? []);
1457
+ const lijst = beschikbaarPerSig.get(sig) ?? [];
1458
+ lijst.push(id);
1459
+ beschikbaarPerSig.set(sig, lijst);
1460
+ }
1461
+ const behouden = new Map(); // planIndex -> bestaande cart id
1462
+ const nieuw = [];
1463
+ for (const g of gewenst) {
1464
+ const kandidaten = beschikbaarPerSig.get(g.sig);
1465
+ const hergebruik = kandidaten && kandidaten.length > 0 ? kandidaten.shift() : undefined;
1466
+ if (hergebruik)
1467
+ behouden.set(g.planIndex, hergebruik);
1468
+ else
1469
+ nieuw.push(g);
1470
+ }
1471
+ const teVerwijderen = [];
1472
+ for (const lijst of beschikbaarPerSig.values())
1473
+ teVerwijderen.push(...lijst);
1474
+ if (teVerwijderen.length > 0) {
1475
+ for (let s = 0; s < teVerwijderen.length; s += 200) {
1476
+ const slice = teVerwijderen.slice(s, s + 200);
1477
+ // Alleen voor karren die echt verdwijnen: openstaande printopdrachten en barcodes vervallen.
1478
+ await sb
1479
+ .from("print_jobs")
1480
+ .update({ status: "geannuleerd", foutmelding: "Vervallen: kar verwijderd bij herindeling", updated_at: new Date().toISOString() })
1481
+ .in("status", ["wachtend", "bezig"])
1482
+ .in("payload->>cart_id", slice);
1483
+ await sb.from("pakbon_barcode_inhoud").delete().in("cart_id", slice);
1484
+ await sb.from("order_cart_layer_positions").delete().in("cart_id", slice);
1485
+ const { error: delError } = await sb.from("order_carts").delete().in("id", slice);
1486
+ if (delError) {
1487
+ throw new Error(`Verouderde karren konden niet worden opgeruimd: ${delError.message}`);
1154
1488
  }
1155
- return {
1156
- order_id: identity.ownerOrderId,
1157
- buffer_id: bufferId,
1158
- cart_number: identity.cartNumber,
1159
- total_carts: identity.totalCarts,
1160
- is_auto: true,
1161
- is_complete: cart.is_complete,
1162
- source: usedFloridayPacking ? "floriday" : "auto",
1163
- occupancy_pct: cart.occupancy_pct,
1164
- total_layers_used: cart.total_layers_used,
1165
- carrier_type: cart.carrier_type,
1166
- carrier_type_id: cart.carrier_type_id,
1167
- max_layers: cart.max_layers,
1168
- weight_kg: cart.weight_kg,
1169
- algorithm_version: useDimensions ? DIMENSION_PLANNER_VERSION : LOAD_CARRIER_PLANNER_VERSION,
1170
- layout_method: useDimensions ? "afmetingen" : "belading",
1171
- };
1172
- });
1489
+ }
1490
+ }
1491
+ let created = behouden.size;
1492
+ const idByPlanIndex = new Map(behouden);
1493
+ if (nieuw.length === 0) {
1494
+ console.log(`[logistiek-engine] buffer ${bufferId}: indeling ongewijzigd, ${behouden.size} kar(ren) behouden, ` +
1495
+ `${teVerwijderen.length} verouderde kar(ren) verwijderd.`);
1496
+ }
1497
+ // Alleen de echt nieuwe karren wegschrijven, in blokken.
1498
+ const CHUNK = 40;
1499
+ for (let start = 0; start < nieuw.length; start += CHUNK) {
1500
+ const chunk = nieuw.slice(start, start + CHUNK);
1501
+ const cartRows = chunk.map((g) => g.row);
1173
1502
  const { data: insertedCarts, error } = await sb
1174
1503
  .from("order_carts")
1175
1504
  .insert(cartRows)
@@ -1184,33 +1513,13 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
1184
1513
  const identityKey = (orderId, cartNumber) => `${orderId == null ? "shared" : String(orderId)}:${Number(cartNumber)}`;
1185
1514
  const idByIdentity = new Map(insertedCarts.map((c) => [identityKey(c.order_id, c.cart_number), String(c.id)]));
1186
1515
  const itemRows = [];
1187
- for (let chunkIndex = 0; chunkIndex < chunk.length; chunkIndex++) {
1188
- const cart = chunk[chunkIndex];
1189
- const identity = persistentIdentities[start + chunkIndex];
1190
- const cartId = identity
1191
- ? idByIdentity.get(identityKey(identity.ownerOrderId, identity.cartNumber))
1192
- : undefined;
1516
+ for (const g of chunk) {
1517
+ const cartId = idByIdentity.get(identityKey(g.row["order_id"], g.row["cart_number"]));
1193
1518
  if (!cartId)
1194
1519
  continue;
1195
- for (const i of cart.items) {
1196
- itemRows.push({
1197
- cart_id: cartId,
1198
- // Samengestelde sleutels van groepsleden bestaan niet als orderregel.
1199
- order_item_id: i.order_item_id.includes("::") ? i.order_item_id.split("::")[0] : i.order_item_id,
1200
- product_id: i.product_id,
1201
- product_name: i.product_name,
1202
- crates: Math.round(i.crates),
1203
- boxes: i.boxes ?? null,
1204
- weight_kg: i.weight_kg ?? null,
1205
- plants_per_crate: i.plants_per_crate,
1206
- crates_per_layer: i.crates_per_layer,
1207
- layers_per_cart: i.layers_per_cart,
1208
- layer_fraction: i.layer_fraction,
1209
- layer_share_pct: i.layer_share_pct,
1210
- layer_index: i.layer_index ?? null,
1211
- crates_in_layer: i.crates_in_layer ?? null,
1212
- });
1213
- }
1520
+ idByPlanIndex.set(g.planIndex, cartId);
1521
+ for (const i of g.items)
1522
+ itemRows.push({ ...i, cart_id: cartId });
1214
1523
  }
1215
1524
  for (let s = 0; s < itemRows.length; s += 500) {
1216
1525
  const slice = itemRows.slice(s, s + 500);
@@ -1231,14 +1540,11 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
1231
1540
  // Fysieke laagindeling per kar. Alleen de afmetingenmethode levert posities;
1232
1541
  // bij belading blijft de indeling leeg ("nog geen laagindeling aangeleverd").
1233
1542
  if (dimensionPlan) {
1234
- for (let chunkIndex = 0; chunkIndex < chunk.length; chunkIndex++) {
1235
- const cart = chunk[chunkIndex];
1236
- const identity = persistentIdentities[start + chunkIndex];
1237
- const cartId = identity
1238
- ? idByIdentity.get(identityKey(identity.ownerOrderId, identity.cartNumber))
1239
- : undefined;
1240
- const dimCart = dimensionPlan.get(start + chunkIndex);
1241
- if (!cartId || !dimCart)
1543
+ for (const g of chunk) {
1544
+ const cartId = idByPlanIndex.get(g.planIndex);
1545
+ const dimCart = dimensionPlan.get(g.planIndex);
1546
+ const cart = plannedCarts[g.planIndex];
1547
+ if (!cartId || !dimCart || !cart)
1242
1548
  continue;
1243
1549
  const rows = layerPositionRows(cartId, dimCart);
1244
1550
  // Atomair vervangen: eerst de vorige indeling van deze kar weg.