@flowselections/logistiek-engine 1.0.177 → 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 (28) hide show
  1. package/db/schema/logistiek-engine.sql +850 -0
  2. package/dist-lib/components/BeladingIndelingCard.d.ts.map +1 -1
  3. package/dist-lib/components/BeladingIndelingCard.js +2 -0
  4. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +11 -5
  5. package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -1
  6. package/dist-lib/integrations/supabase/previewAuthStorage.js +38 -21
  7. package/dist-lib/integrations/supabase/types.d.ts +137 -0
  8. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  9. package/dist-lib/lib/beladingRegels.d.ts +78 -0
  10. package/dist-lib/lib/beladingRegels.d.ts.map +1 -0
  11. package/dist-lib/lib/beladingRegels.js +143 -0
  12. package/dist-lib/lib/bulkLoadCarrierPlanning.server.js +2 -2
  13. package/dist-lib/lib/eigenTraysticker.d.ts +17 -0
  14. package/dist-lib/lib/eigenTraysticker.d.ts.map +1 -0
  15. package/dist-lib/lib/eigenTraysticker.js +32 -0
  16. package/dist-lib/lib/loadCarrierPlanning.server.d.ts +35 -3
  17. package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -1
  18. package/dist-lib/lib/loadCarrierPlanning.server.js +251 -89
  19. package/dist-lib/lib/modulePrompts.d.ts.map +1 -1
  20. package/dist-lib/lib/modulePrompts.js +76 -0
  21. package/dist-lib/lib/moduleVersion.d.ts +1 -1
  22. package/dist-lib/lib/moduleVersion.js +1 -1
  23. package/dist-lib/lib/planQueue.server.d.ts.map +1 -1
  24. package/dist-lib/lib/planQueue.server.js +26 -2
  25. package/dist-lib/lib/stickerQueue.server.d.ts +1 -1
  26. package/dist-lib/lib/stickerQueue.server.d.ts.map +1 -1
  27. package/dist-lib/lib/stickerQueue.server.js +88 -0
  28. 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;AAwnMD,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"}
@@ -1241,6 +1241,40 @@ CONTROLELIJST
1241
1241
  order: 2,
1242
1242
  summary: "Voert belading, controle, printen en transport uit volgens de taak-payload.",
1243
1243
  revisions: [
1244
+ {
1245
+ number: 30,
1246
+ title: "Karnummer koppelen aan het kar-id",
1247
+ date: "2026-10-21",
1248
+ summary: "cart_numbers heeft een kolom cart_id; vul die bij elk nieuw karnummer.",
1249
+ body: `KOPPELPROMPT ENGINE 1.0.179 – karnummers op kar-id
1250
+
1251
+ De Engine heeft cart_numbers uitgebreid met cart_id uuid (stabiel na herindeling) en
1252
+ een unieke index (pick_date, cart_id) waar cart_id gevuld is. Graag:
1253
+ - Vul in assign_cart_numbers bij elk nieuw karnummer ook cart_id = order_carts.id.
1254
+ - Zoek bestaande nummers voortaan eerst op cart_id, pas daarna op order + cart_index.
1255
+ - logistics_cart_seq geeft nu een duidelijke fout als een order twee karren met
1256
+ hetzelfde volgnummer heeft (er wordt geen kar meer weggelaten). Toon die fout.
1257
+ - logistics_cart_seq is niet meer aan te roepen als anon; ingelogd en server blijven werken.
1258
+ Bestaande nummers worden pas gekoppeld na akkoord van de opdrachtgever.
1259
+ Verander niets aan de Engine-functies.`,
1260
+ },
1261
+ {
1262
+ number: 29,
1263
+ title: "Antwoord 1.0.74: fusten en land per kar",
1264
+ date: "2026-10-21",
1265
+ summary: "control_get_todays_carts() geeft per kar ook crates_on_cart en country_code.",
1266
+ body: `ANTWOORD ENGINE 1.0.178 – Afhandeling 1.0.74 (fusten en land per kar)
1267
+
1268
+ control_get_todays_carts() heeft achteraan twee kolommen gekregen:
1269
+ - crates_on_cart integer: som van order_cart_items.crates op deze kar (op kar-id).
1270
+ Bij een gedeelde kar het totaal van alle orders, één keer geteld. Geen indeling: NULL.
1271
+ - country_code text: ISO-landcode (2 letters) van de afleverlocatie van de eerste
1272
+ order op de kar (op ordernummer), via orders.custom_fields->>'delivery_location'
1273
+ -> customer_delivery_locations.id. Geen geldige 2-letterige code (bijv. "Nederland")
1274
+ of geen locatie: NULL; er wordt niet omgezet uit een landnaam.
1275
+ Bestaande kolommen, volgorde, rechten en filtering zijn ongewijzigd.
1276
+ Je hoeft niets aan de Engine aan te passen.`,
1277
+ },
1244
1278
  {
1245
1279
  number: 28,
1246
1280
  title: "Geen layoutkeuze meer: document automatisch uit de order",
@@ -5317,6 +5351,48 @@ Zelf rekenen doe je niet.
5317
5351
  order: 9,
5318
5352
  summary: "Levert de documentlayouts. Kiest zelf geen layout en geen printer: de Engine bepaalt welke layout en welke printplek bij een document horen.",
5319
5353
  revisions: [
5354
+ {
5355
+ number: 13,
5356
+ title: "Correctie 1.69.2: eigen traysticker per fust",
5357
+ date: "2026-10-21",
5358
+ summary: "Een tray is een fust, geen laag: de Engine maakt één eigen traysticker per fust.",
5359
+ body: `CORRECTIE ENGINE 1.0.178 – Layouts 1.69.2 (eigen traystickers per fust)
5360
+
5361
+ Vervangt de laag-indeling uit antwoord 12. Een tray is een FUST.
5362
+ - Per orderregel op een kar: order_cart_items.crates opdrachten, één per fust.
5363
+ - tray_nummer / tray_totaal = fust x van y binnen de hele order
5364
+ (volgorde: kar, regel, fust).
5365
+ - tray_producten: één item, het product van die regel (bij een samengesteld
5366
+ product zoals een mixtray het samengestelde product), met
5367
+ aantal = planten per fust (regel, anders product); het laatste fust van een
5368
+ regel krijgt de rest. Optioneel 'onderdelen' als de karregel componenten heeft.
5369
+ - Idempotentie: eigen_traysticker:<order>:<kar>:<karregel>:<fustnummer>:<layout>.
5370
+ - Aantal fusten onbekend: alleen die sticker wacht met "Aantal fusten onbekend".
5371
+ - Verder ongewijzigd: _documenttype 'traysticker', _herkomst 'eigen',
5372
+ _kopieen 1, layout via layout_voor_document, geen logo_url, zelfde vrijgave.`,
5373
+ },
5374
+ {
5375
+ number: 12,
5376
+ title: "Antwoord 1.69.2: eigen traystickers per tray",
5377
+ date: "2026-10-21",
5378
+ summary: "De Engine print traystickers voor eigen orders, één opdracht per tray, via layout_print_document.",
5379
+ body: `ANTWOORD ENGINE 1.0.178 – Layouts 1.69.2 (eigen traystickers)
5380
+
5381
+ Voor eigen orders (HO/MO/AO) maakt de Engine per tray (laag) van de karindeling
5382
+ één opdracht via layout_print_document:
5383
+ - _documenttype 'traysticker', _herkomst 'eigen', _kopieen 1
5384
+ - _layout_id uit logistics_document_for_order(order_id, 'traysticker'), die
5385
+ layout_voor_document aanroept (= fs.eigen_traysticker)
5386
+ - _payload: order_id, order_number, ordernummer, klantnaam, leverdatum,
5387
+ afleverlocatie, cart_id, print_plek_id, tray_nummer, tray_totaal,
5388
+ tray_producten [{ artikelnummer, naam, aantal }]
5389
+ - Geen logo_url: het logo van de eigen organisatie haal je zelf uit de bedrijfsgegevens.
5390
+ - Idempotentie: eigen_traysticker:<order>:<kar>:<laag>:<layout>.
5391
+ Vrijgave: alleen als de instelling Tray-stickers niet op "Niet printen" staat;
5392
+ het moment is de pakbon van de kar (eigen orders hebben geen Floriday-belading).
5393
+ Ontbreekt de trayindeling of de layout: voor die tray geen opdracht, met uitleg;
5394
+ de rest van de order gaat door. Floriday-traystickers zijn ongewijzigd.`,
5395
+ },
5320
5396
  {
5321
5397
  number: 11,
5322
5398
  title: "Verzenddocument: de Engine bepaalt afleverbrief of veilingbrief",
@@ -1,2 +1,2 @@
1
- export declare const MODULE_VERSION = "1.0.177";
1
+ export declare const MODULE_VERSION = "1.0.180";
2
2
  //# sourceMappingURL=moduleVersion.d.ts.map
@@ -1,2 +1,2 @@
1
1
  // Automatisch gegenereerd door prebuild:lib; niet handmatig aanpassen.
2
- export const MODULE_VERSION = "1.0.177";
2
+ export const MODULE_VERSION = "1.0.180";
@@ -1 +1 @@
1
- {"version":3,"file":"planQueue.server.d.ts","sourceRoot":"","sources":["../../src/lib/planQueue.server.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAQ5D,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,2FAA2F;IAC3F,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAwBD,wFAAwF;AACxF,wBAAsB,YAAY,CAChC,EAAE,EAAE,cAAc,EAClB,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GAC3D,OAAO,CAAC,eAAe,CAAC,CAwR1B;AAgCD,iGAAiG;AACjG,eAAO,MAAM,eAAe,0EAA2E,CAAC;AAgCxG,4EAA4E;AAE5E,wBAAsB,cAAc,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAEtE"}
1
+ {"version":3,"file":"planQueue.server.d.ts","sourceRoot":"","sources":["../../src/lib/planQueue.server.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAS5D,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,2FAA2F;IAC3F,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAwBD,wFAAwF;AACxF,wBAAsB,YAAY,CAChC,EAAE,EAAE,cAAc,EAClB,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GAC3D,OAAO,CAAC,eAAe,CAAC,CA8S1B;AAgCD,iGAAiG;AACjG,eAAO,MAAM,eAAe,0EAA2E,CAAC;AAgCxG,4EAA4E;AAE5E,wBAAsB,cAAc,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAEtE"}