@flowselections/logistiek-engine 1.0.53 → 1.0.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +24 -24
  2. package/dist-lib/components/AfrondWachtrijCard.d.ts +2 -0
  3. package/dist-lib/components/AfrondWachtrijCard.d.ts.map +1 -0
  4. package/dist-lib/components/AfrondWachtrijCard.js +27 -0
  5. package/dist-lib/components/BeladingsafwijkingenCard.d.ts +2 -0
  6. package/dist-lib/components/BeladingsafwijkingenCard.d.ts.map +1 -0
  7. package/dist-lib/components/BeladingsafwijkingenCard.js +44 -0
  8. package/dist-lib/components/FlowCanvas.d.ts +6 -0
  9. package/dist-lib/components/FlowCanvas.d.ts.map +1 -1
  10. package/dist-lib/components/FlowCanvas.js +79 -18
  11. package/dist-lib/components/FlowOrders.d.ts.map +1 -1
  12. package/dist-lib/components/FlowOrders.js +59 -13
  13. package/dist-lib/components/FlowPage.d.ts.map +1 -1
  14. package/dist-lib/components/FlowPage.js +175 -6
  15. package/dist-lib/components/GeblokkeerdeOrdersCard.d.ts.map +1 -1
  16. package/dist-lib/components/GeblokkeerdeOrdersCard.js +21 -2
  17. package/dist-lib/components/KardekkingCard.d.ts +2 -0
  18. package/dist-lib/components/KardekkingCard.d.ts.map +1 -0
  19. package/dist-lib/components/KardekkingCard.js +29 -0
  20. package/dist-lib/components/LogistiekEnginePage.d.ts.map +1 -1
  21. package/dist-lib/components/LogistiekEnginePage.js +7 -1
  22. package/dist-lib/components/LogistiekEngineQueuePage.d.ts.map +1 -1
  23. package/dist-lib/components/LogistiekEngineQueuePage.js +4 -2
  24. package/dist-lib/components/NodeTracePanel.d.ts +4 -1
  25. package/dist-lib/components/NodeTracePanel.d.ts.map +1 -1
  26. package/dist-lib/components/NodeTracePanel.js +8 -2
  27. package/dist-lib/components/OrderStopBanner.d.ts +9 -2
  28. package/dist-lib/components/OrderStopBanner.d.ts.map +1 -1
  29. package/dist-lib/components/OrderStopBanner.js +27 -13
  30. package/dist-lib/components/OrderTraceDetails.d.ts.map +1 -1
  31. package/dist-lib/components/OrderTraceDetails.js +2 -1
  32. package/dist-lib/components/PlanWachtrijPage.d.ts +2 -0
  33. package/dist-lib/components/PlanWachtrijPage.d.ts.map +1 -0
  34. package/dist-lib/components/PlanWachtrijPage.js +99 -0
  35. package/dist-lib/components/PlannerDiffCard.d.ts +2 -0
  36. package/dist-lib/components/PlannerDiffCard.d.ts.map +1 -0
  37. package/dist-lib/components/PlannerDiffCard.js +25 -0
  38. package/dist-lib/components/RegiePage.d.ts.map +1 -1
  39. package/dist-lib/components/RegiePage.js +21 -13
  40. package/dist-lib/components/TaakGezondheidCard.d.ts +2 -0
  41. package/dist-lib/components/TaakGezondheidCard.d.ts.map +1 -0
  42. package/dist-lib/components/TaakGezondheidCard.js +31 -0
  43. package/dist-lib/hooks/useAfrondWachtrij.d.ts +28 -0
  44. package/dist-lib/hooks/useAfrondWachtrij.d.ts.map +1 -0
  45. package/dist-lib/hooks/useAfrondWachtrij.js +88 -0
  46. package/dist-lib/hooks/useBeladingsafwijkingen.d.ts +39 -0
  47. package/dist-lib/hooks/useBeladingsafwijkingen.d.ts.map +1 -0
  48. package/dist-lib/hooks/useBeladingsafwijkingen.js +119 -0
  49. package/dist-lib/hooks/useGeblokkeerdeOrders.d.ts +7 -0
  50. package/dist-lib/hooks/useGeblokkeerdeOrders.d.ts.map +1 -1
  51. package/dist-lib/hooks/useGeblokkeerdeOrders.js +28 -2
  52. package/dist-lib/hooks/useLogisticsTrace.js +1 -1
  53. package/dist-lib/hooks/useOrderTrace.js +1 -1
  54. package/dist-lib/hooks/useOrderVersies.d.ts.map +1 -1
  55. package/dist-lib/hooks/useOrderVersies.js +17 -3
  56. package/dist-lib/hooks/usePlanWachtrij.d.ts +33 -0
  57. package/dist-lib/hooks/usePlanWachtrij.d.ts.map +1 -0
  58. package/dist-lib/hooks/usePlanWachtrij.js +104 -0
  59. package/dist-lib/hooks/useRecalcJob.js +1 -1
  60. package/dist-lib/hooks/useShippingNoteDiagnose.d.ts +20 -0
  61. package/dist-lib/hooks/useShippingNoteDiagnose.d.ts.map +1 -0
  62. package/dist-lib/hooks/useShippingNoteDiagnose.js +52 -0
  63. package/dist-lib/hooks/useTaakGezondheid.d.ts +16 -0
  64. package/dist-lib/hooks/useTaakGezondheid.d.ts.map +1 -0
  65. package/dist-lib/hooks/useTaakGezondheid.js +43 -0
  66. package/dist-lib/hooks/useWerktijden.js +1 -1
  67. package/dist-lib/hooks/useWerkwijzeStatus.js +1 -1
  68. package/dist-lib/index.d.ts +10 -0
  69. package/dist-lib/index.d.ts.map +1 -1
  70. package/dist-lib/index.js +10 -0
  71. package/dist-lib/integrations/supabase/auth-middleware.d.ts +3147 -159
  72. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  73. package/dist-lib/integrations/supabase/client.server.d.ts +3147 -159
  74. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  75. package/dist-lib/integrations/supabase/client.server.js +1 -1
  76. package/dist-lib/integrations/supabase/moduleClient.d.ts +3147 -159
  77. package/dist-lib/integrations/supabase/moduleClient.d.ts.map +1 -1
  78. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +1 -1
  79. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -1
  80. package/dist-lib/integrations/supabase/previewAuthStorage.js +8 -1
  81. package/dist-lib/integrations/supabase/types.d.ts +3183 -161
  82. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  83. package/dist-lib/lib/attachAuthToken.js +1 -1
  84. package/dist-lib/lib/bufferDecision.test.d.ts +2 -0
  85. package/dist-lib/lib/bufferDecision.test.d.ts.map +1 -0
  86. package/dist-lib/lib/bufferDecision.test.js +104 -0
  87. package/dist-lib/lib/cartCoverage.functions.d.ts +30820 -0
  88. package/dist-lib/lib/cartCoverage.functions.d.ts.map +1 -0
  89. package/dist-lib/lib/cartCoverage.functions.js +20 -0
  90. package/dist-lib/lib/floridayDocuments.functions.d.ts +8979 -15
  91. package/dist-lib/lib/floridayDocuments.functions.d.ts.map +1 -1
  92. package/dist-lib/lib/floridayDocuments.functions.js +5 -5
  93. package/dist-lib/lib/floridayPush.functions.d.ts +3147 -159
  94. package/dist-lib/lib/floridayPush.functions.d.ts.map +1 -1
  95. package/dist-lib/lib/floridayPush.functions.js +3 -3
  96. package/dist-lib/lib/flowJourney.d.ts +40 -0
  97. package/dist-lib/lib/flowJourney.d.ts.map +1 -0
  98. package/dist-lib/lib/flowJourney.js +156 -0
  99. package/dist-lib/lib/flowNodes.d.ts +3 -0
  100. package/dist-lib/lib/flowNodes.d.ts.map +1 -1
  101. package/dist-lib/lib/flowNodes.js +6 -0
  102. package/dist-lib/lib/flowRoute.d.ts +11 -1
  103. package/dist-lib/lib/flowRoute.d.ts.map +1 -1
  104. package/dist-lib/lib/flowRoute.js +343 -40
  105. package/dist-lib/lib/flowTree.d.ts +86 -1
  106. package/dist-lib/lib/flowTree.d.ts.map +1 -1
  107. package/dist-lib/lib/flowTree.js +391 -2
  108. package/dist-lib/lib/foutTekst.d.ts +4 -0
  109. package/dist-lib/lib/foutTekst.d.ts.map +1 -0
  110. package/dist-lib/lib/foutTekst.js +43 -0
  111. package/dist-lib/lib/handlers/logistics-tick.server.d.ts +2 -0
  112. package/dist-lib/lib/handlers/logistics-tick.server.d.ts.map +1 -1
  113. package/dist-lib/lib/handlers/logistics-tick.server.js +56 -0
  114. package/dist-lib/lib/handlers/logistics-tick.test.d.ts +2 -0
  115. package/dist-lib/lib/handlers/logistics-tick.test.d.ts.map +1 -0
  116. package/dist-lib/lib/handlers/logistics-tick.test.js +63 -0
  117. package/dist-lib/lib/loadCarrierPlanning.functions.d.ts +14965 -25
  118. package/dist-lib/lib/loadCarrierPlanning.functions.d.ts.map +1 -1
  119. package/dist-lib/lib/loadCarrierPlanning.functions.js +7 -7
  120. package/dist-lib/lib/loadCarrierPlanning.server.d.ts +24 -12
  121. package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -1
  122. package/dist-lib/lib/loadCarrierPlanning.server.js +490 -211
  123. package/dist-lib/lib/logisticsRecalcJob.functions.d.ts +5986 -10
  124. package/dist-lib/lib/logisticsRecalcJob.functions.d.ts.map +1 -1
  125. package/dist-lib/lib/logisticsRecalcJob.functions.js +4 -4
  126. package/dist-lib/lib/logisticsRecalcJob.server.d.ts.map +1 -1
  127. package/dist-lib/lib/logisticsRecalcJob.server.js +17 -2
  128. package/dist-lib/lib/logisticsRecalculate.functions.d.ts +3147 -159
  129. package/dist-lib/lib/logisticsRecalculate.functions.d.ts.map +1 -1
  130. package/dist-lib/lib/logisticsRecalculate.functions.js +2 -2
  131. package/dist-lib/lib/modulePrompts.d.ts.map +1 -1
  132. package/dist-lib/lib/modulePrompts.js +3436 -3325
  133. package/dist-lib/lib/nodeTrace.d.ts +4 -0
  134. package/dist-lib/lib/nodeTrace.d.ts.map +1 -1
  135. package/dist-lib/lib/nodeTrace.js +6 -15
  136. package/dist-lib/lib/planQueue.server.d.ts.map +1 -1
  137. package/dist-lib/lib/planQueue.server.js +24 -3
  138. package/dist-lib/lib/plannerDiff.d.ts +37 -0
  139. package/dist-lib/lib/plannerDiff.d.ts.map +1 -0
  140. package/dist-lib/lib/plannerDiff.functions.d.ts +30821 -0
  141. package/dist-lib/lib/plannerDiff.functions.d.ts.map +1 -0
  142. package/dist-lib/lib/plannerDiff.functions.js +21 -0
  143. package/dist-lib/lib/plannerDiff.js +74 -0
  144. package/dist-lib/lib/plannerDiff.server.d.ts +5 -0
  145. package/dist-lib/lib/plannerDiff.server.d.ts.map +1 -0
  146. package/dist-lib/lib/plannerDiff.server.js +60 -0
  147. package/dist-lib/lib/plannerDiff.test.d.ts +2 -0
  148. package/dist-lib/lib/plannerDiff.test.d.ts.map +1 -0
  149. package/dist-lib/lib/plannerDiff.test.js +41 -0
  150. package/dist-lib/lib/resolveBuffer.test.d.ts +2 -0
  151. package/dist-lib/lib/resolveBuffer.test.d.ts.map +1 -0
  152. package/dist-lib/lib/resolveBuffer.test.js +38 -0
  153. package/dist-lib/lib/ruleEngineSchema.d.ts.map +1 -1
  154. package/dist-lib/lib/ruleEngineSchema.js +52 -0
  155. package/dist-lib/lib/stopReden.d.ts +43 -0
  156. package/dist-lib/lib/stopReden.d.ts.map +1 -0
  157. package/dist-lib/lib/stopReden.js +197 -0
  158. package/dist-lib/lib/taskCounts.d.ts +2 -0
  159. package/dist-lib/lib/taskCounts.d.ts.map +1 -0
  160. package/dist-lib/lib/taskCounts.js +15 -0
  161. package/dist-lib/lib/taskCounts.test.d.ts +2 -0
  162. package/dist-lib/lib/taskCounts.test.d.ts.map +1 -0
  163. package/dist-lib/lib/taskCounts.test.js +12 -0
  164. package/dist-lib/lib/trace.server.d.ts.map +1 -1
  165. package/dist-lib/lib/trace.server.js +3 -0
  166. package/dist-lib/lib/vakUitleg.d.ts.map +1 -1
  167. package/dist-lib/lib/vakUitleg.js +2 -1
  168. package/package.json +12 -9
@@ -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
  });
@@ -569,6 +553,53 @@ export function dimensionCartsToPlannedUnits(carts) {
569
553
  // ---------------------------------------------------------------------------
570
554
  /** Maximaal aantal ladingdragers dat één orderbuffer mag opleveren. */
571
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
+ }
572
603
  /**
573
604
  * Bepaalt de definitieve, persistente identiteit van iedere fysieke kar.
574
605
  * Bij een bufferorder met meerdere orders zijn alle karren gedeelde
@@ -662,7 +693,62 @@ export async function bufferOrdersWithoutCarts(sb, bufferId, orderIds) {
662
693
  }
663
694
  return ids.filter((id) => !withCarts.has(id));
664
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
+ */
665
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 = {}) {
666
752
  const warnings = [];
667
753
  // Bevroren karren blijven staan; al het overige van de héle buffer wordt
668
754
  // samen ingedeeld, zodat orders van de buffer op dezelfde karren komen.
@@ -670,11 +756,17 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
670
756
  const isSupplement = keepCartIds.length > 0;
671
757
  const { data: orders } = await sb
672
758
  .from("orders")
673
- .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")
674
760
  .eq("buffer_id", bufferId)
675
761
  .is("deleted_at", null);
676
762
  const allOrderRows = Array.isArray(orders) ? orders : [];
677
- 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
+ }
678
770
  const orderIds = orderRows.map((o) => o.id);
679
771
  // Binnen een bufferorder mogen orders en klanten altijd samen op een kar.
680
772
  const isBufferOrder = orderRows.length > 1;
@@ -777,22 +869,86 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
777
869
  const { data: items } = await sb
778
870
  .from("order_items")
779
871
  .select("id, order_id, product_id, product_name, quantity, plants_per_crate, crates_per_layer, layers_per_cart, " +
780
- "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)")
781
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
+ }
782
928
  const customerByOrder = new Map(orderRows.map((o) => [o.id, o.customer_id ?? null]));
783
929
  const rawRows = (Array.isArray(items) ? items : []).map((it) => ({
784
930
  ...it,
785
931
  customer_id: customerByOrder.get(it.order_id) ?? null,
786
932
  }));
787
- // Floriday-verpakking per order is de eerste beladingbron.
933
+ // Order-brede Floriday-verpakking: alleen nog als allerlaatste noodgreep.
788
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();
789
938
  for (const o of orderRows) {
790
939
  const belading = beladingFromFloridayPacking(o.custom_fields?.floriday_packing);
791
940
  if (belading)
792
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);
793
945
  }
794
946
  const sources = new Map();
795
- let inputs = await expandOrderItemsForPlanner(sb, rawRows, { packingByOrder, sources });
947
+ let inputs = await expandOrderItemsForPlanner(sb, rawRows, {
948
+ packingByOrder,
949
+ packingByOrderLine,
950
+ sources,
951
+ });
796
952
  // Wat al op een bevroren kar ligt, wordt niet opnieuw ingedeeld.
797
953
  let frozenCartCount = 0;
798
954
  let cartNumberStart = 1;
@@ -922,8 +1078,17 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
922
1078
  // daarom verwijderd.)
923
1079
  const solo = orderRows.length === 1 ? orderRows[0] : null;
924
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);
925
1089
  const takeoverProfile = profileFor(profiles, options.carrierType ?? defaultProfile.code);
926
- const takeoverAllowed = takeoverProfile.capacity_model === "layered" || takeoverProfile.capacity_model === "stacked";
1090
+ const takeoverAllowed = (takeoverProfile.capacity_model === "layered" || takeoverProfile.capacity_model === "stacked") &&
1091
+ uniformeBelading;
927
1092
  if (useDimensions) {
928
1093
  // Globale methode: indelen op basis van fysieke afmetingen.
929
1094
  const carrierRow = profiles.find((p) => p.code === (options.carrierType ?? defaultProfile.code)) ?? null;
@@ -1047,8 +1212,13 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
1047
1212
  }
1048
1213
  // Veiligheidsgrens: een onrealistisch grote uitkomst wordt niet afgekapt maar
1049
1214
  // geblokkeerd. Een half plan laat planten stilzwijgend uit de pickstroom vallen.
1050
- if (plannedCarts.length > MAX_CARTS_PER_BUFFER) {
1051
- 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 ` +
1052
1222
  `${MAX_CARTS_PER_BUFFER}. Er is niets ingedeeld; controleer de aantallen of de belading van de producten.`;
1053
1223
  console.error(`[logistiek-engine] buffer ${bufferId} geblokkeerd: ${reason}`);
1054
1224
  if (!options.dryRun) {
@@ -1059,7 +1229,9 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
1059
1229
  reason_code: "kargrens_overschreden",
1060
1230
  bufferId,
1061
1231
  path: "typescript",
1062
- carts: plannedCarts.length,
1232
+ carts: totalCartsAfterPlan,
1233
+ newCarts: plannedCarts.length,
1234
+ frozenCarts: frozenCartCount,
1063
1235
  limit: MAX_CARTS_PER_BUFFER,
1064
1236
  beladingSources,
1065
1237
  },
@@ -1166,37 +1338,167 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
1166
1338
  await del().in("order_id", ids);
1167
1339
  }
1168
1340
  };
1169
- await clearExistingCarts();
1170
- // In blokken wegschrijven: per ladingdrager een aparte insert is bij grote
1171
- // orders veel te traag.
1172
- let created = 0;
1173
- const CHUNK = 100;
1174
- for (let start = 0; start < plannedCarts.length; start += CHUNK) {
1175
- const chunk = plannedCarts.slice(start, start + CHUNK);
1176
- const cartRows = chunk.map((cart, chunkIndex) => {
1177
- const cartIndex = start + chunkIndex;
1178
- const identity = persistentIdentities[cartIndex];
1179
- if (!identity) {
1180
- 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}`);
1181
1488
  }
1182
- return {
1183
- order_id: identity.ownerOrderId,
1184
- buffer_id: bufferId,
1185
- cart_number: identity.cartNumber,
1186
- total_carts: identity.totalCarts,
1187
- is_auto: true,
1188
- is_complete: cart.is_complete,
1189
- source: usedFloridayPacking ? "floriday" : "auto",
1190
- occupancy_pct: cart.occupancy_pct,
1191
- total_layers_used: cart.total_layers_used,
1192
- carrier_type: cart.carrier_type,
1193
- carrier_type_id: cart.carrier_type_id,
1194
- max_layers: cart.max_layers,
1195
- weight_kg: cart.weight_kg,
1196
- algorithm_version: useDimensions ? DIMENSION_PLANNER_VERSION : LOAD_CARRIER_PLANNER_VERSION,
1197
- layout_method: useDimensions ? "afmetingen" : "belading",
1198
- };
1199
- });
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);
1200
1502
  const { data: insertedCarts, error } = await sb
1201
1503
  .from("order_carts")
1202
1504
  .insert(cartRows)
@@ -1211,33 +1513,13 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
1211
1513
  const identityKey = (orderId, cartNumber) => `${orderId == null ? "shared" : String(orderId)}:${Number(cartNumber)}`;
1212
1514
  const idByIdentity = new Map(insertedCarts.map((c) => [identityKey(c.order_id, c.cart_number), String(c.id)]));
1213
1515
  const itemRows = [];
1214
- for (let chunkIndex = 0; chunkIndex < chunk.length; chunkIndex++) {
1215
- const cart = chunk[chunkIndex];
1216
- const identity = persistentIdentities[start + chunkIndex];
1217
- const cartId = identity
1218
- ? idByIdentity.get(identityKey(identity.ownerOrderId, identity.cartNumber))
1219
- : undefined;
1516
+ for (const g of chunk) {
1517
+ const cartId = idByIdentity.get(identityKey(g.row["order_id"], g.row["cart_number"]));
1220
1518
  if (!cartId)
1221
1519
  continue;
1222
- for (const i of cart.items) {
1223
- itemRows.push({
1224
- cart_id: cartId,
1225
- // Samengestelde sleutels van groepsleden bestaan niet als orderregel.
1226
- order_item_id: i.order_item_id.includes("::") ? i.order_item_id.split("::")[0] : i.order_item_id,
1227
- product_id: i.product_id,
1228
- product_name: i.product_name,
1229
- crates: Math.round(i.crates),
1230
- boxes: i.boxes ?? null,
1231
- weight_kg: i.weight_kg ?? null,
1232
- plants_per_crate: i.plants_per_crate,
1233
- crates_per_layer: i.crates_per_layer,
1234
- layers_per_cart: i.layers_per_cart,
1235
- layer_fraction: i.layer_fraction,
1236
- layer_share_pct: i.layer_share_pct,
1237
- layer_index: i.layer_index ?? null,
1238
- crates_in_layer: i.crates_in_layer ?? null,
1239
- });
1240
- }
1520
+ idByPlanIndex.set(g.planIndex, cartId);
1521
+ for (const i of g.items)
1522
+ itemRows.push({ ...i, cart_id: cartId });
1241
1523
  }
1242
1524
  for (let s = 0; s < itemRows.length; s += 500) {
1243
1525
  const slice = itemRows.slice(s, s + 500);
@@ -1258,14 +1540,11 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
1258
1540
  // Fysieke laagindeling per kar. Alleen de afmetingenmethode levert posities;
1259
1541
  // bij belading blijft de indeling leeg ("nog geen laagindeling aangeleverd").
1260
1542
  if (dimensionPlan) {
1261
- for (let chunkIndex = 0; chunkIndex < chunk.length; chunkIndex++) {
1262
- const cart = chunk[chunkIndex];
1263
- const identity = persistentIdentities[start + chunkIndex];
1264
- const cartId = identity
1265
- ? idByIdentity.get(identityKey(identity.ownerOrderId, identity.cartNumber))
1266
- : undefined;
1267
- const dimCart = dimensionPlan.get(start + chunkIndex);
1268
- 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)
1269
1548
  continue;
1270
1549
  const rows = layerPositionRows(cartId, dimCart);
1271
1550
  // Atomair vervangen: eerst de vorige indeling van deze kar weg.