@flowselections/logistiek-engine 1.0.175 → 1.0.180

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 (48) hide show
  1. package/db/schema/logistiek-engine.sql +2112 -0
  2. package/dist-lib/components/BeheerPage.d.ts.map +1 -1
  3. package/dist-lib/components/BeheerPage.js +2 -1
  4. package/dist-lib/components/BeladingIndelingCard.d.ts.map +1 -1
  5. package/dist-lib/components/BeladingIndelingCard.js +2 -0
  6. package/dist-lib/components/DocumentkeuzeCard.d.ts +2 -0
  7. package/dist-lib/components/DocumentkeuzeCard.d.ts.map +1 -0
  8. package/dist-lib/components/DocumentkeuzeCard.js +18 -0
  9. package/dist-lib/index.d.ts +1 -0
  10. package/dist-lib/index.d.ts.map +1 -1
  11. package/dist-lib/index.js +1 -0
  12. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +11 -5
  13. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -1
  14. package/dist-lib/integrations/supabase/previewAuthStorage.js +38 -21
  15. package/dist-lib/integrations/supabase/types.d.ts +376 -15
  16. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  17. package/dist-lib/lib/beladingRegels.d.ts +78 -0
  18. package/dist-lib/lib/beladingRegels.d.ts.map +1 -0
  19. package/dist-lib/lib/beladingRegels.js +143 -0
  20. package/dist-lib/lib/bulkLoadCarrierPlanning.server.js +2 -2
  21. package/dist-lib/lib/controlScan.server.js +1 -1
  22. package/dist-lib/lib/eigenTraysticker.d.ts +17 -0
  23. package/dist-lib/lib/eigenTraysticker.d.ts.map +1 -0
  24. package/dist-lib/lib/eigenTraysticker.js +32 -0
  25. package/dist-lib/lib/layoutPrint.server.d.ts +10 -1
  26. package/dist-lib/lib/layoutPrint.server.d.ts.map +1 -1
  27. package/dist-lib/lib/layoutPrint.server.js +19 -1
  28. package/dist-lib/lib/loadCarrierPlanning.server.d.ts +35 -3
  29. package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -1
  30. package/dist-lib/lib/loadCarrierPlanning.server.js +251 -89
  31. package/dist-lib/lib/modulePrompts.d.ts.map +1 -1
  32. package/dist-lib/lib/modulePrompts.js +233 -0
  33. package/dist-lib/lib/moduleVersion.d.ts +1 -1
  34. package/dist-lib/lib/moduleVersion.js +1 -1
  35. package/dist-lib/lib/planQueue.server.d.ts.map +1 -1
  36. package/dist-lib/lib/planQueue.server.js +26 -2
  37. package/dist-lib/lib/ruleEngineSchema.d.ts.map +1 -1
  38. package/dist-lib/lib/ruleEngineSchema.js +4 -38
  39. package/dist-lib/lib/ruleEngineWizard.d.ts.map +1 -1
  40. package/dist-lib/lib/ruleEngineWizard.js +0 -2
  41. package/dist-lib/lib/shippingNoteQueue.server.js +1 -1
  42. package/dist-lib/lib/stickerQueue.server.d.ts +25 -0
  43. package/dist-lib/lib/stickerQueue.server.d.ts.map +1 -1
  44. package/dist-lib/lib/stickerQueue.server.js +150 -22
  45. package/dist-lib/lib/stickerRules.d.ts +17 -0
  46. package/dist-lib/lib/stickerRules.d.ts.map +1 -1
  47. package/dist-lib/lib/stickerRules.js +30 -0
  48. package/package.json +1 -1
@@ -12,6 +12,7 @@ import { fallbackCarrierProfile } from "./loadCarrierTypes.js";
12
12
  import { planByDimensions, DIMENSION_PLANNER_VERSION } from "./dimensionPlanner.js";
13
13
  import { buildDimensionItems, carrierDimensions } from "./dimensionData.server.js";
14
14
  import { zonderExternAfgehandeld } from "./externalFulfillment.js";
15
+ import { vulBeladingAan, deelUitFloridayVerpakking, plantenVanRegel, plausibiliteit, karrenOnwaarschijnlijk, verpakkingSleutel, controleerInvariant, VELD_NAAM, } from "./beladingRegels.js";
15
16
  // ---------------------------------------------------------------------------
16
17
  // Buffer
17
18
  // ---------------------------------------------------------------------------
@@ -84,62 +85,62 @@ export function beladingPerRegelUitOrder(customFields) {
84
85
  const tradeItemId = line.trade_item_id ? String(line.trade_item_id) : null;
85
86
  if (!tradeItemId)
86
87
  continue;
87
- const belading = beladingFromFloridayPacking(line.packing);
88
- if (belading)
89
- out.set(tradeItemId, belading);
88
+ // 1.0.179: per veld; 0 (klokorders) = onbekend, de rest van de verpakking telt wel.
89
+ const deel = deelUitFloridayVerpakking(line.packing);
90
+ if (deel)
91
+ out.set(tradeItemId, deel);
90
92
  }
91
93
  return out;
92
94
  }
93
- /** Waarden van de orderregel zelf, of null als de regel niets heeft. */
94
- function beladingVanRegel(row) {
95
- if (positive(row.plants_per_crate) == null &&
96
- positive(row.crates_per_layer) == null &&
97
- positive(row.layers_per_cart) == null) {
95
+ /** Waarden van de orderregel zelf (per veld; 0 of leeg = onbekend). */
96
+ function deelVanRij(row) {
97
+ if (!row)
98
98
  return null;
99
- }
100
- return {
101
- plants_per_crate: positive(row.plants_per_crate) ?? 1,
102
- crates_per_layer: positive(row.crates_per_layer) ?? 1,
103
- layers_per_cart: positive(row.layers_per_cart) ?? 1,
99
+ const d = {
100
+ plants_per_crate: positive(row.plants_per_crate),
101
+ crates_per_layer: positive(row.crates_per_layer),
102
+ layers_per_cart: positive(row.layers_per_cart),
104
103
  };
104
+ return d.plants_per_crate != null || d.crates_per_layer != null || d.layers_per_cart != null ? d : null;
105
105
  }
106
106
  /**
107
- * Beladingbronnen in vaste volgorde:
108
- * 1. handmatig vastgezette regel (packing_locked_at);
109
- * 2. de Floriday-verpakking van díe regel (op artikel gekoppeld);
110
- * 3. de eigen waarden van de bestelregel;
111
- * 4. het product;
112
- * 5. de order-brede Floriday-verpakking — uitsluitend als allerlaatste noodgreep
113
- * voor een regel die van 1 t/m 4 niets heeft;
114
- * 6. anders onbekend: er wordt niet gegokt, de indeling blokkeert.
107
+ * 1.0.179: belading per veld, nooit een stille 1. Volgorde per veld:
108
+ * handmatig vastgezet (alleen regel, dan product) of anders
109
+ * Floriday-regel -> orderregel-snapshot -> product -> order-brede Floriday-verpakking.
110
+ * De standaard van de ladingdrager vult daarna in de indeling nog fusten per laag en lagen aan.
111
+ * Blijft een veld leeg, dan wordt die regel geblokkeerd (belading_onvolledig).
115
112
  */
116
- function resolveBelading(row, packing, linePacking = null) {
117
- const fromRow = beladingVanRegel(row);
118
- // 1. Handmatig vastgezet: niemand overschrijft dat, ook Floriday niet.
119
- if (row.packing_locked_at && fromRow)
120
- return { belading: fromRow, source: "handmatig" };
121
- // 2. De Floriday-verpakking van deze regel.
122
- if (linePacking)
123
- return { belading: linePacking, source: "floriday_line" };
124
- // 3. De eigen waarden van de bestelregel.
125
- if (fromRow)
126
- return { belading: fromRow, source: "order_items" };
127
- const p = row.products ?? null;
128
- const fromProduct = p && (positive(p.plants_per_crate) != null || positive(p.crates_per_layer) != null || positive(p.layers_per_cart) != null)
129
- ? {
130
- plants_per_crate: positive(p.plants_per_crate) ?? 1,
131
- crates_per_layer: positive(p.crates_per_layer) ?? 1,
132
- layers_per_cart: positive(p.layers_per_cart) ?? 1,
113
+ export function resolveBelading(row, packing, linePacking = null) {
114
+ const regel = deelVanRij(row);
115
+ const product = deelVanRij(row.products ?? null);
116
+ const bronnen = row.packing_locked_at && regel
117
+ ? [
118
+ { naam: "handmatig", waarden: regel },
119
+ { naam: "product", waarden: product },
120
+ ]
121
+ : [
122
+ { naam: "floriday_line", waarden: linePacking },
123
+ { naam: "order_items", waarden: regel },
124
+ { naam: "product", waarden: product },
125
+ { naam: "floriday_packing", waarden: packing },
126
+ ];
127
+ const u = vulBeladingAan(bronnen);
128
+ const gebruikte = new Set(Object.values(u.bronPerVeld));
129
+ const source = (u.bronPerVeld.plants_per_crate ?? u.bronPerVeld.crates_per_layer ?? u.bronPerVeld.layers_per_cart ?? "geen");
130
+ const deel = u.belading ?? {
131
+ plants_per_crate: 0,
132
+ crates_per_layer: 0,
133
+ layers_per_cart: 0,
134
+ };
135
+ if (!u.belading) {
136
+ for (const b of bronnen) {
137
+ for (const v of ["plants_per_crate", "crates_per_layer", "layers_per_cart"]) {
138
+ if (deel[v] === 0 && positive(b.waarden?.[v]) != null)
139
+ deel[v] = positive(b.waarden?.[v]);
140
+ }
133
141
  }
134
- : null;
135
- // 4. Het product.
136
- if (fromProduct)
137
- return { belading: fromProduct, source: "product" };
138
- // 5. Allerlaatste noodgreep: de order-brede Floriday-verpakking.
139
- if (packing)
140
- return { belading: packing, source: "floriday_packing" };
141
- // 6. Onbekend.
142
- return { belading: { plants_per_crate: 1, crates_per_layer: 1, layers_per_cart: 1 }, source: "geen" };
142
+ }
143
+ return { belading: deel, source, ontbrekend: u.ontbrekend, aangevuld: gebruikte.size > 1 };
143
144
  }
144
145
  /** Schaal de onderdelen van een setregel naar het aantal sets op één kar. */
145
146
  export function scaleComponents(entry, plantsOnCart) {
@@ -225,8 +226,14 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
225
226
  lineMap.get(String(row.products?.floriday_trade_item_id ?? "")) ??
226
227
  null
227
228
  : null;
228
- const { belading: base, source } = resolveBelading(row, packing, linePacking);
229
- const plants = Math.max(0, Number(row.quantity ?? 0));
229
+ const opgelost = resolveBelading(row, packing, linePacking);
230
+ const { belading: base, source } = opgelost;
231
+ // 1.0.179: intern altijd planten; eenheid fust = quantity x planten per fust van de regel.
232
+ const planten = plantenVanRegel(row.unit, row.quantity, row.plants_per_crate ?? base.plants_per_crate);
233
+ if (planten == null) {
234
+ options.onbekendePlanten?.push({ order_id: orderKey, order_item_id: String(row.id), naam: String(row.product_name ?? "onbekend") });
235
+ }
236
+ const plants = Math.max(0, planten ?? 0);
230
237
  const lines = linesByItem.get(String(row.id)) ?? [];
231
238
  const kind = lines[0]?.unit_kind ?? "artikel";
232
239
  if (kind === "uitgesloten") {
@@ -242,6 +249,8 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
242
249
  }
243
250
  if (kind !== "groepslid") {
244
251
  options.sources?.set(String(row.id), source);
252
+ if (opgelost.ontbrekend.length > 0)
253
+ options.ontbrekend?.set(String(row.id), opgelost.ontbrekend);
245
254
  if (kind === "set") {
246
255
  options.components?.set(String(row.id), { lineQty: plants, components: toComponents(lines[0]?.components) });
247
256
  }
@@ -253,7 +262,7 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
253
262
  product_name: row.product_name ?? "Onbekend",
254
263
  plants,
255
264
  ...base,
256
- packing_authoritative: isAuthoritative(source),
265
+ packing_authoritative: isAuthoritative(source) && !opgelost.aangevuld && opgelost.ontbrekend.length === 0,
257
266
  weight_per_crate: row.products?.weight != null ? Number(row.products.weight) : null,
258
267
  });
259
268
  continue;
@@ -266,37 +275,16 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
266
275
  if (!p) {
267
276
  throw new Error(`Indeling geblokkeerd voor ${row.product_name ?? "groep"}: groepslid ${l.trade_item_key ?? "onbekend"} heeft geen artikel.`);
268
277
  }
269
- let belading;
270
- let src;
271
278
  const eigenLijn = lineMap?.get(String(p.floriday_trade_item_id ?? "")) ?? lineMap?.get(String(p.id)) ?? null;
272
- if (source === "handmatig") {
273
- belading = { ...base };
274
- src = source;
275
- }
276
- else if (eigenLijn) {
277
- belading = { ...eigenLijn };
278
- src = "floriday_line";
279
- }
280
- else if (positive(p.plants_per_crate) != null ||
281
- positive(p.crates_per_layer) != null ||
282
- positive(p.layers_per_cart) != null) {
283
- belading = {
284
- plants_per_crate: positive(p.plants_per_crate) ?? 1,
285
- crates_per_layer: positive(p.crates_per_layer) ?? 1,
286
- layers_per_cart: positive(p.layers_per_cart) ?? 1,
287
- };
288
- src = "product";
289
- }
290
- else if (packing) {
291
- belading = { ...packing };
292
- src = "floriday_packing";
293
- }
294
- else {
295
- belading = { ...base };
296
- src = "geen";
297
- }
279
+ const lid = source === "handmatig"
280
+ ? { belading: { ...base }, source: source, ontbrekend: opgelost.ontbrekend, aangevuld: opgelost.aangevuld }
281
+ : resolveBelading({ products: p }, packing, eigenLijn);
282
+ const belading = lid.belading;
283
+ const src = lid.source;
298
284
  const itemKey = `${row.id}::${l.trade_item_key ?? p.id}`;
299
285
  options.sources?.set(itemKey, src);
286
+ if (lid.ontbrekend.length > 0)
287
+ options.ontbrekend?.set(itemKey, lid.ontbrekend);
300
288
  out.push({
301
289
  order_item_id: itemKey,
302
290
  order_id: row.order_id ?? null,
@@ -305,13 +293,71 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
305
293
  product_name: `${row.product_name ?? "Groep"} — ${p.product ?? "onbekend groepslid"}`,
306
294
  plants: qty,
307
295
  ...belading,
308
- packing_authoritative: isAuthoritative(src),
296
+ packing_authoritative: isAuthoritative(src) && !lid.aangevuld && lid.ontbrekend.length === 0,
309
297
  weight_per_crate: p.weight != null ? Number(p.weight) : null,
310
298
  });
311
299
  }
312
300
  }
313
301
  return out;
314
302
  }
303
+ async function bevestigdeSleutels(sb, orderIds) {
304
+ const set = new Set();
305
+ if (orderIds.length === 0)
306
+ return set;
307
+ const { data, error } = await sb
308
+ .from("logistics_belading_bevestigingen")
309
+ .select("order_id, verpakking_sleutel")
310
+ .in("order_id", orderIds);
311
+ if (error)
312
+ throw new Error(`Indeling geblokkeerd: bevestigingen konden niet worden gelezen (${error.message}).`);
313
+ for (const r of (Array.isArray(data) ? data : []))
314
+ set.add(`${r.order_id}|${r.verpakking_sleutel}`);
315
+ return set;
316
+ }
317
+ async function beladingOnwaarschijnlijk(sb, inputs, rawRows, profile) {
318
+ const fustIds = Array.from(new Set(rawRows.map((r) => r.products?.fust_type_id).filter((x) => !!x).map(String)));
319
+ const fustById = new Map();
320
+ if (fustIds.length > 0) {
321
+ const { data } = await sb.from("fust_types").select("id, name, capacity_units").in("id", fustIds);
322
+ for (const f of (Array.isArray(data) ? data : []))
323
+ fustById.set(String(f.id), f);
324
+ }
325
+ const fustPerProduct = new Map();
326
+ for (const r of rawRows)
327
+ if (r.product_id)
328
+ fustPerProduct.set(String(r.product_id), r.products?.fust_type_id ?? null);
329
+ const kandidaten = [];
330
+ for (const i of inputs) {
331
+ if (i.plants <= 0)
332
+ continue;
333
+ const fustId = i.product_id ? fustPerProduct.get(String(i.product_id)) ?? null : null;
334
+ const redenen = plausibiliteit({
335
+ belading: { plants_per_crate: i.plants_per_crate, crates_per_layer: i.crates_per_layer, layers_per_cart: i.layers_per_cart },
336
+ fust: fustId ? fustById.get(fustId) ?? null : null,
337
+ maxLagenLadingdrager: i.packing_authoritative ? null : profile.layers_per_carrier,
338
+ });
339
+ if (redenen.length === 0)
340
+ continue;
341
+ kandidaten.push({
342
+ order_id: i.order_id ?? null,
343
+ product: i.product_name,
344
+ sleutel: verpakkingSleutel(i.product_id, i),
345
+ redenen,
346
+ });
347
+ }
348
+ if (kandidaten.length === 0)
349
+ return [];
350
+ const ok = await bevestigdeSleutels(sb, Array.from(new Set(kandidaten.map((k) => k.order_id).filter((x) => !!x))));
351
+ return kandidaten.filter((k) => !ok.has(`${k.order_id}|${k.sleutel}`));
352
+ }
353
+ /** Invariant: elk fust van elke ingedeelde regel staat op precies één kar en de planten kloppen. */
354
+ export function invariantFouten(inputs, carts) {
355
+ const regels = inputs
356
+ .filter((i) => i.plants > 0 && i.plants_per_crate > 0)
357
+ .map((i) => ({ id: i.order_item_id, planten: i.plants, plants_per_crate: i.plants_per_crate }));
358
+ const karRegels = carts.flatMap((c, idx) => c.items.map((it) => ({ regelId: it.order_item_id, cartId: String(idx), fusten: it.crates, planten: null })));
359
+ return controleerInvariant(regels, karRegels);
360
+ }
315
361
  // ---------------------------------------------------------------------------
316
362
  // Ladingdragertypen
317
363
  // ---------------------------------------------------------------------------
@@ -734,6 +780,9 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
734
780
  // Een proefberekening schrijft niets weg en hoeft dus niets te vergrendelen.
735
781
  if (options.dryRun)
736
782
  return replanBufferUnlocked(sb, bufferId, options);
783
+ // Bijdelen binnen dezelfde ronde: de lease is al in handen, herbruiken.
784
+ if (options.leaseHeld)
785
+ return replanBufferUnlocked(sb, bufferId, options);
737
786
  const lockKey = `buffer:${bufferId}`;
738
787
  let held = false;
739
788
  try {
@@ -756,7 +805,7 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
756
805
  replacedManualCount: 0,
757
806
  usedFloridayPacking: false,
758
807
  warnings: [BUFFER_BUSY_MESSAGE],
759
- skipped: "locked",
808
+ skipped: "busy",
760
809
  };
761
810
  }
762
811
  try {
@@ -866,7 +915,11 @@ async function replanBufferUnlocked(sb, bufferId, options = {}) {
866
915
  const supplement = await replanBufferInternal(sb, bufferId, {
867
916
  ...options,
868
917
  keepCartIds: keep,
918
+ leaseHeld: true,
869
919
  });
920
+ // Melding alleen bij succes; anders de echte uitkomst (geblokkeerd/bezet) doorgeven.
921
+ if (!supplement.ok || supplement.skipped)
922
+ return supplement;
870
923
  return {
871
924
  ...supplement,
872
925
  warnings: [
@@ -884,9 +937,9 @@ async function replanBufferUnlocked(sb, bufferId, options = {}) {
884
937
  const idList = orderIds.length > 0 ? orderIds : ["00000000-0000-0000-0000-000000000000"];
885
938
  const { data: rawItems } = await sb
886
939
  .from("order_items")
887
- .select("id, order_id, product_id, product_name, quantity, plants_per_crate, crates_per_layer, layers_per_cart, " +
940
+ .select("id, order_id, product_id, product_name, quantity, unit, plants_per_crate, crates_per_layer, layers_per_cart, " +
888
941
  "packing_locked_at, packing_locked_by, source_line_key, " +
889
- "products(plants_per_crate, crates_per_layer, layers_per_cart, weight, floriday_trade_item_id)")
942
+ "products(plants_per_crate, crates_per_layer, layers_per_cart, weight, floriday_trade_item_id, fust_type_id)")
890
943
  .in("order_id", idList)
891
944
  // Regels in quarantaine (Verkoop-herstel) nooit inplannen.
892
945
  .is("quarantined_at", null);
@@ -973,12 +1026,16 @@ async function replanBufferUnlocked(sb, bufferId, options = {}) {
973
1026
  const sources = new Map();
974
1027
  const setComponents = new Map();
975
1028
  const expandNotes = [];
1029
+ const ontbrekendeVelden = new Map();
1030
+ const onbekendePlanten = [];
976
1031
  let inputs = await expandOrderItemsForPlanner(sb, rawRows, {
977
1032
  packingByOrder,
978
1033
  packingByOrderLine,
979
1034
  sources,
980
1035
  components: setComponents,
981
1036
  notes: expandNotes,
1037
+ ontbrekend: ontbrekendeVelden,
1038
+ onbekendePlanten,
982
1039
  });
983
1040
  // Groepsregel naast ledenregels: niet ingedeeld, wel zichtbaar gemeld bij de order.
984
1041
  for (const n of expandNotes) {
@@ -1108,23 +1165,53 @@ async function replanBufferUnlocked(sb, bufferId, options = {}) {
1108
1165
  // lagenmodel (los product / enkele eenheid) hebben de belading niet nodig.
1109
1166
  const guardProfile = profileFor(profiles, options.carrierType ?? defaultProfile.code);
1110
1167
  const needsBelading = guardProfile.capacity_model !== "single" && guardProfile.capacity_model !== "none";
1111
- const unknownItems = inputs.filter((i) => sources.get(i.order_item_id) === "geen" && i.plants > 0);
1112
1168
  const useDimensions = rules?.layoutMethod === "afmetingen";
1113
- if (!useDimensions && needsBelading && unknownItems.length > 0 && !options.dryRun) {
1114
- const reason = `Belading onbekend voor ${unknownItems.length} orderregel(s): geen Floriday-verpakking, ` +
1115
- `geen waarden op de orderregel en geen productinstelling. Er is niets ingedeeld.`;
1169
+ // 1.0.179 (punt 1/2): ontbrekende fusten per laag en lagen aanvullen uit de standaard van de ladingdrager.
1170
+ const standaard = {
1171
+ crates_per_layer: positive(guardProfile.max_crates_per_layer),
1172
+ layers_per_cart: positive(guardProfile.layers_per_carrier),
1173
+ };
1174
+ const onvolledig = [];
1175
+ for (const i of inputs) {
1176
+ const mist = ontbrekendeVelden.get(i.order_item_id);
1177
+ if (!mist || mist.length === 0)
1178
+ continue;
1179
+ const rest = [];
1180
+ for (const v of mist) {
1181
+ const w = v === "plants_per_crate" ? null : standaard[v] ?? null;
1182
+ if (w != null) {
1183
+ i[v] = w;
1184
+ i.packing_authoritative = false;
1185
+ }
1186
+ else
1187
+ rest.push(v);
1188
+ }
1189
+ if (rest.length > 0 && i.plants > 0)
1190
+ onvolledig.push({ item: i, velden: rest });
1191
+ }
1192
+ // Planten per fust is altijd nodig (ook zonder lagenmodel): fusten = planten / planten per fust.
1193
+ const blokkeerOnvolledig = onvolledig.filter((o) => o.velden.includes("plants_per_crate") || (needsBelading && !useDimensions));
1194
+ if ((blokkeerOnvolledig.length > 0 || onbekendePlanten.length > 0) && !options.dryRun) {
1195
+ const delen = [
1196
+ ...blokkeerOnvolledig.slice(0, 10).map((o) => `${o.item.product_name}: ${o.velden.map((v) => VELD_NAAM[v]).join(", ")} onbekend`),
1197
+ ...onbekendePlanten.slice(0, 10).map((o) => `${o.naam}: eenheid fust zonder planten per fust`),
1198
+ ];
1199
+ const reason = `Belading onvolledig voor ${blokkeerOnvolledig.length + onbekendePlanten.length} orderregel(s): ${delen.join("; ")}. ` +
1200
+ "Er wordt niet gegokt; vul de ontbrekende waarde in (orderregel, product of ladingdrager).";
1116
1201
  console.error(`[logistiek-engine] buffer ${bufferId} geblokkeerd: ${reason}`);
1117
1202
  const { error: blockError } = await sb.rpc("logistics_plan_block", {
1118
1203
  p_order_ids: orderIds,
1119
1204
  p_reason: reason,
1120
1205
  p_details: {
1121
- // Stabiele sleutel: nooit afgeleid uit ordernummers of aantallen.
1122
- reason_code: "belading_onbekend",
1206
+ reason_code: "belading_onvolledig",
1123
1207
  bufferId,
1124
1208
  path: "typescript",
1125
1209
  carrier: guardProfile.code,
1126
1210
  beladingSources,
1127
- items: unknownItems.slice(0, 20).map((i) => ({ id: i.order_item_id, product: i.product_name })),
1211
+ items: [
1212
+ ...blokkeerOnvolledig.slice(0, 20).map((o) => ({ id: o.item.order_item_id, product: o.item.product_name, velden: o.velden })),
1213
+ ...onbekendePlanten.slice(0, 20).map((o) => ({ id: o.order_item_id, product: o.naam, velden: ["plants_per_crate"] })),
1214
+ ],
1128
1215
  },
1129
1216
  });
1130
1217
  if (blockError)
@@ -1142,6 +1229,33 @@ async function replanBufferUnlocked(sb, bufferId, options = {}) {
1142
1229
  beladingSources,
1143
1230
  };
1144
1231
  }
1232
+ // 1.0.179 (punt 3): plausibiliteit, alle ordersoorten. Eén bevestiging per order en verpakking.
1233
+ if (!options.dryRun) {
1234
+ const onwaarschijnlijk = await beladingOnwaarschijnlijk(sb, inputs, rawRows, guardProfile);
1235
+ if (onwaarschijnlijk.length > 0) {
1236
+ const reason = `Belading onwaarschijnlijk: ${onwaarschijnlijk.slice(0, 10).map((o) => `${o.product}: ${o.redenen.join("; ")}`).join(" | ")}. ` +
1237
+ "Kantoor moet dit bevestigen voordat er wordt ingedeeld.";
1238
+ const { error: blockError } = await sb.rpc("logistics_plan_block", {
1239
+ p_order_ids: orderIds,
1240
+ p_reason: reason,
1241
+ p_details: { reason_code: "belading_onwaarschijnlijk", bufferId, path: "typescript", bevestigen: onwaarschijnlijk.slice(0, 50) },
1242
+ });
1243
+ if (blockError)
1244
+ warnings.push(`Blokkade kon niet worden vastgelegd: ${blockError.message}`);
1245
+ return {
1246
+ ok: false,
1247
+ bufferId,
1248
+ orderIds,
1249
+ cartsCreated: 0,
1250
+ replacedManualCount: 0,
1251
+ usedFloridayPacking: false,
1252
+ warnings: [...warnings, reason],
1253
+ skipped: "blocked",
1254
+ blockedReason: reason,
1255
+ beladingSources,
1256
+ };
1257
+ }
1258
+ }
1145
1259
  let plannedCarts = [];
1146
1260
  // Laagindeling van de afmetingenmethode, per cart_number. Alleen gevuld als
1147
1261
  // er daadwerkelijk op afmetingen is ingedeeld.
@@ -1319,6 +1433,54 @@ async function replanBufferUnlocked(sb, bufferId, options = {}) {
1319
1433
  beladingSources,
1320
1434
  };
1321
1435
  }
1436
+ // 1.0.179 (punt 3): meer dan 50 karren voor één order vraagt bevestiging van kantoor.
1437
+ // 1.0.179 (punt 11): invariant; bij een verschil wordt niets weggeschreven en niets geprint.
1438
+ if (!options.dryRun) {
1439
+ const perOrder = new Map();
1440
+ for (const c of plannedCarts) {
1441
+ for (const oid of new Set(c.items.map((it) => it.order_id).filter((x) => !!x))) {
1442
+ perOrder.set(oid, (perOrder.get(oid) ?? 0) + 1);
1443
+ }
1444
+ }
1445
+ const teVeel = Array.from(perOrder.entries()).filter(([, n]) => karrenOnwaarschijnlijk(n) != null);
1446
+ if (teVeel.length > 0) {
1447
+ const ok = await bevestigdeSleutels(sb, teVeel.map(([oid]) => oid));
1448
+ const open = teVeel.filter(([oid, n]) => !ok.has(`${oid}|karren:${n}`));
1449
+ if (open.length > 0) {
1450
+ const reason = `Belading onwaarschijnlijk: ${open.map(([oid, n]) => `order ${orderRows.find((o) => String(o.id) === oid)?.order_number ?? oid}: ${karrenOnwaarschijnlijk(n)}`).join("; ")}. ` +
1451
+ "Kantoor moet dit bevestigen voordat er wordt ingedeeld.";
1452
+ await sb.rpc("logistics_plan_block", {
1453
+ p_order_ids: orderIds,
1454
+ p_reason: reason,
1455
+ p_details: {
1456
+ reason_code: "belading_onwaarschijnlijk",
1457
+ bufferId,
1458
+ path: "typescript",
1459
+ bevestigen: open.map(([oid, n]) => ({ order_id: oid, sleutel: `karren:${n}`, redenen: [karrenOnwaarschijnlijk(n)] })),
1460
+ },
1461
+ });
1462
+ return {
1463
+ ok: false, bufferId, orderIds, cartsCreated: 0, replacedManualCount, usedFloridayPacking,
1464
+ warnings: [...warnings, reason], skipped: "blocked", blockedReason: reason, beladingSources,
1465
+ };
1466
+ }
1467
+ }
1468
+ if (!useDimensions) {
1469
+ const fouten = invariantFouten(inputs, plannedCarts);
1470
+ if (fouten.length > 0) {
1471
+ const reason = `Indeling klopt niet: ${fouten.slice(0, 5).join(" ")} Er is niets weggeschreven en niets geprint.`;
1472
+ await sb.rpc("logistics_plan_block", {
1473
+ p_order_ids: orderIds,
1474
+ p_reason: reason,
1475
+ p_details: { reason_code: "invariant_fusten", bufferId, path: "typescript", fouten: fouten.slice(0, 20) },
1476
+ });
1477
+ return {
1478
+ ok: false, bufferId, orderIds, cartsCreated: 0, replacedManualCount, usedFloridayPacking,
1479
+ warnings: [...warnings, reason], skipped: "blocked", blockedReason: reason, beladingSources,
1480
+ };
1481
+ }
1482
+ }
1483
+ }
1322
1484
  if (options.dryRun) {
1323
1485
  return {
1324
1486
  ok: true,
@@ -1 +1 @@
1
- {"version":3,"file":"modulePrompts.d.ts","sourceRoot":"","sources":["../../src/lib/modulePrompts.ts"],"names":[],"mappings":"AAWA;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;CAKvB,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,oBAAoB,CAAC;AAKhE,MAAM,WAAW,oBAAoB;IACnC,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,eAAe,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,oBAAoB,EAAE,CAAC;CACnC;AAw9LD,eAAO,MAAM,cAAc,EAAE,YAAY,EAUL,CAAC;AAErC,gEAAgE;AAChE,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,oBAAoB,GAC7B,MAAM,CAmBR;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAE9D;AAED,oEAAoE;AACpE,wBAAgB,qBAAqB,IAAI,MAAM,CAiB9C;AAED,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,eAAe,EAC1B,cAAc,CAAC,EAAE,MAAM,GACtB,MAAM,CAIR"}
1
+ {"version":3,"file":"modulePrompts.d.ts","sourceRoot":"","sources":["../../src/lib/modulePrompts.ts"],"names":[],"mappings":"AAWA;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;CAKvB,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,oBAAoB,CAAC;AAKhE,MAAM,WAAW,oBAAoB;IACnC,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,eAAe,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,oBAAoB,EAAE,CAAC;CACnC;AAosMD,eAAO,MAAM,cAAc,EAAE,YAAY,EAUL,CAAC;AAErC,gEAAgE;AAChE,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,oBAAoB,GAC7B,MAAM,CAmBR;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAE9D;AAED,oEAAoE;AACpE,wBAAgB,qBAAqB,IAAI,MAAM,CAiB9C;AAED,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,eAAe,EAC1B,cAAc,CAAC,EAAE,MAAM,GACtB,MAAM,CAIR"}