@flowselections/logistiek-engine 1.0.112 → 1.0.117

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.
@@ -1 +1 @@
1
- {"version":3,"file":"logistics-shipping-note-now.server.d.ts","sourceRoot":"","sources":["../../../src/lib/handlers/logistics-shipping-note-now.server.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAQ5D,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,cAAc,GACvB,OAAO,CAAC,QAAQ,CAAC,CAiGnB"}
1
+ {"version":3,"file":"logistics-shipping-note-now.server.d.ts","sourceRoot":"","sources":["../../../src/lib/handlers/logistics-shipping-note-now.server.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAQ5D,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,cAAc,GACvB,OAAO,CAAC,QAAQ,CAAC,CA2GnB"}
@@ -51,14 +51,25 @@ export async function handleShippingNoteNow(request, supabase) {
51
51
  catch {
52
52
  /* opschonen mag de ronde niet blokkeren */
53
53
  }
54
- // 2. Alle openstaande aanvragen mogen nu meteen worden opgepakt.
54
+ // 2. Alleen de aanvraag van díe pakbon wakker maken; nooit alles.
55
55
  let wake = null;
56
+ let slipJobId = null;
56
57
  try {
57
- const { data } = await supabase.rpc("logistics_shipping_notes_wake_all");
58
- wake = data ?? null;
58
+ const body = (await request.clone().json().catch(() => ({})));
59
+ const v = typeof body["slipJobId"] === "string" ? body["slipJobId"].trim() : "";
60
+ slipJobId = /^[0-9a-f-]{36}$/i.test(v) ? v : null;
59
61
  }
60
62
  catch {
61
- wake = null;
63
+ slipJobId = null;
64
+ }
65
+ if (slipJobId) {
66
+ try {
67
+ const { data } = await supabase.rpc("logistics_shipping_note_wake", { _slip_job_id: slipJobId });
68
+ wake = { gewekt: data ?? 0, slipJobId };
69
+ }
70
+ catch {
71
+ wake = null;
72
+ }
62
73
  }
63
74
  // 3. Gebundeld verwerken tot alles op is of het tijdbudget vol is; de knop
64
75
  // hoeft niet te wachten tot de laatste ronde klaar is.
@@ -218,6 +218,14 @@ async function runTickRound(supabase, tickJob, ident) {
218
218
  catch {
219
219
  /* zelfherstel mag de ronde niet blokkeren */
220
220
  }
221
+ // Vrijgeven (alleen database): wachtende aanvragen van orders met een
222
+ // goedgekeurde controlescan, zodra de poort het aanmaken toestaat.
223
+ try {
224
+ await supabase.rpc("logistics_shipping_note_release", { _order_number: null });
225
+ }
226
+ catch {
227
+ /* vrijgeven mag de ronde niet blokkeren */
228
+ }
221
229
  const { runShippingNoteQueue } = await import("../shippingNoteQueue.server.js");
222
230
  if (typeof runShippingNoteQueue !== "function") {
223
231
  throw new Error("Deze versie van de Logistiek Engine bevat geen verzendbriefverwerking. Werk de module bij.");
@@ -30,7 +30,54 @@ export interface ExpandOptions {
30
30
  packingByOrderLine?: Map<string, Map<string, Belading>>;
31
31
  /** Wordt gevuld met de gebruikte bron per orderregel-sleutel. */
32
32
  sources?: Map<string, BeladingSource>;
33
+ /** Wordt gevuld met de onderdelen van een set per orderregel (aantallen voor de hele regel). */
34
+ components?: Map<string, {
35
+ lineQty: number;
36
+ components: SetComponent[];
37
+ }>;
38
+ /** Wordt gevuld met meldingen per order (bijv. groepsregel naast ledenregels). */
39
+ notes?: Array<{
40
+ order_id: string | null;
41
+ order_item_id: string;
42
+ note: string;
43
+ }>;
33
44
  }
45
+ /** Onderdeel van een set op het laagste niveau, uit de vastgelegde verdeling. */
46
+ export interface SetComponent {
47
+ trade_item_key: string | null;
48
+ product_id: string | null;
49
+ product_name: string | null;
50
+ article_number: string | null;
51
+ qty: number;
52
+ }
53
+ /** Eén regel uit logistics_planner_lines: dezelfde verdeling als reservering en afboeken. */
54
+ export interface PlannerLineRow {
55
+ order_item_id: string;
56
+ unit_kind: "artikel" | "set" | "groepslid" | "uitgesloten" | "onbekend" | "fout" | string;
57
+ product_id: string | null;
58
+ product_name: string | null;
59
+ qty: number;
60
+ trade_item_key: string | null;
61
+ group_id: string | null;
62
+ components: unknown;
63
+ frozen: boolean;
64
+ note: string | null;
65
+ }
66
+ /** Schaal de onderdelen van een setregel naar het aantal sets op één kar. */
67
+ export declare function scaleComponents(entry: {
68
+ lineQty: number;
69
+ components: SetComponent[];
70
+ } | undefined, plantsOnCart: number): SetComponent[] | null;
71
+ /**
72
+ * Zet orderregels om naar planner-invoer. De verdeling komt uitsluitend uit
73
+ * logistics_planner_lines (dezelfde bron als reservering en afboeken):
74
+ * - gewoon artikel: één invoerregel;
75
+ * - set: één pakket met de eigen belading van de set, plus de onderdelen voor de pickrij;
76
+ * - groep (alleen via het groepshoofd van een actieve groep): per lid de vastgelegde hoeveelheid;
77
+ * - groepsregel naast ledenregels van dezelfde groep: niet ingedeeld, wel gemeld;
78
+ * - fout in de samenstelling: indeling geblokkeerd met leesbare reden (fail closed).
79
+ * Een groep wordt nooit via de naam herkend.
80
+ */
34
81
  export declare function expandOrderItemsForPlanner(sb: SupabaseClient, rows: Array<Record<string, any>>, options?: ExpandOptions): Promise<PlannerInputItem[]>;
35
82
  export declare function loadCarrierProfiles(sb: SupabaseClient): Promise<LoadCarrierTypeRow[]>;
36
83
  export interface BeladingRules {
@@ -1 +1 @@
1
- {"version":3,"file":"loadCarrierPlanning.server.d.ts","sourceRoot":"","sources":["../../src/lib/loadCarrierPlanning.server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"loadCarrierPlanning.server.d.ts","sourceRoot":"","sources":["../../src/lib/loadCarrierPlanning.server.ts"],"names":[],"mappings":"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"}
@@ -140,30 +140,80 @@ function resolveBelading(row, packing, linePacking = null) {
140
140
  // 6. Onbekend.
141
141
  return { belading: { plants_per_crate: 1, crates_per_layer: 1, layers_per_cart: 1 }, source: "geen" };
142
142
  }
143
+ /** Schaal de onderdelen van een setregel naar het aantal sets op één kar. */
144
+ export function scaleComponents(entry, plantsOnCart) {
145
+ if (!entry)
146
+ return null;
147
+ const list = Array.isArray(entry.components) ? entry.components : [];
148
+ if (entry.lineQty <= 0)
149
+ return list;
150
+ const ratio = plantsOnCart / entry.lineQty;
151
+ // Hele stuks, som exact (grootste-restmethode), gelijk aan de bevroren verdeling in de database.
152
+ const raw = list.map((c) => Number(c.qty) * ratio);
153
+ const floors = raw.map((r) => Math.floor(r + 1e-9));
154
+ const target = Math.round(raw.reduce((s, r) => s + r, 0));
155
+ let rest = target - floors.reduce((s, f) => s + f, 0);
156
+ const order = raw
157
+ .map((r, i) => ({ i, frac: r - floors[i], old: Number(list[i].qty), key: String(list[i].trade_item_key ?? "") }))
158
+ .sort((a, b) => b.frac - a.frac || b.old - a.old || a.key.localeCompare(b.key));
159
+ for (const o of order) {
160
+ if (rest <= 0)
161
+ break;
162
+ floors[o.i] += 1;
163
+ rest -= 1;
164
+ }
165
+ return list.map((c, i) => ({ ...c, qty: floors[i] }));
166
+ }
167
+ function toComponents(raw) {
168
+ const list = Array.isArray(raw) ? raw : [];
169
+ return list.map((c) => ({
170
+ trade_item_key: c?.trade_item_key != null ? String(c.trade_item_key) : null,
171
+ product_id: c?.product_id != null ? String(c.product_id) : null,
172
+ product_name: c?.product_name != null ? String(c.product_name) : null,
173
+ article_number: c?.article_number != null ? String(c.article_number) : null,
174
+ qty: Number(c?.qty ?? 0),
175
+ }));
176
+ }
177
+ /**
178
+ * Zet orderregels om naar planner-invoer. De verdeling komt uitsluitend uit
179
+ * logistics_planner_lines (dezelfde bron als reservering en afboeken):
180
+ * - gewoon artikel: één invoerregel;
181
+ * - set: één pakket met de eigen belading van de set, plus de onderdelen voor de pickrij;
182
+ * - groep (alleen via het groepshoofd van een actieve groep): per lid de vastgelegde hoeveelheid;
183
+ * - groepsregel naast ledenregels van dezelfde groep: niet ingedeeld, wel gemeld;
184
+ * - fout in de samenstelling: indeling geblokkeerd met leesbare reden (fail closed).
185
+ * Een groep wordt nooit via de naam herkend.
186
+ */
143
187
  export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
144
188
  const out = [];
145
- const groupIds = rows
146
- .map((r) => r.product_id)
147
- .filter((id) => typeof id === "string" && UUID_RE.test(id));
148
- let groupSet = new Set();
149
- // Groepsregels dragen niet altijd een product_id. Zonder koppeling wordt de
150
- // groep op naam opgezocht, zodat de leden en hun afmetingen alsnog meetellen.
151
- // Een regel die aan een echt artikel gekoppeld is, blijft altijd dat artikel,
152
- // ook als er toevallig een productgroep met dezelfde naam bestaat.
153
- const groupByName = new Map();
154
- {
155
- const { data: allGroups } = await sb.from("product_groups").select("id, name");
156
- for (const g of Array.isArray(allGroups) ? allGroups : []) {
157
- if (g?.name)
158
- groupByName.set(String(g.name).trim().toLowerCase(), String(g.id));
189
+ const orderIds = Array.from(new Set(rows.map((r) => (r.order_id ? String(r.order_id) : null)).filter((x) => !!x)));
190
+ const linesByItem = new Map();
191
+ for (const oid of orderIds) {
192
+ const { data, error } = await sb.rpc("logistics_planner_lines", { p_order_id: oid });
193
+ if (error) {
194
+ throw new Error(`Indeling geblokkeerd: de verdeling van de order kon niet worden gelezen (${error.message}).`);
195
+ }
196
+ for (const l of Array.isArray(data) ? data : []) {
197
+ const key = String(l.order_item_id);
198
+ const list = linesByItem.get(key) ?? [];
199
+ list.push(l);
200
+ linesByItem.set(key, list);
159
201
  }
160
202
  }
161
- let realProductIds = new Set();
162
- if (groupIds.length > 0) {
163
- const { data: groups } = await sb.from("product_groups").select("id").in("id", groupIds);
164
- groupSet = new Set((groups ?? []).map((g) => g.id));
165
- const { data: realProds } = await sb.from("products").select("id").in("id", groupIds);
166
- realProductIds = new Set((Array.isArray(realProds) ? realProds : []).map((p) => String(p.id)));
203
+ const memberIds = Array.from(new Set(Array.from(linesByItem.values())
204
+ .flat()
205
+ .filter((l) => l.unit_kind === "groepslid" && l.product_id)
206
+ .map((l) => String(l.product_id))));
207
+ const prodById = new Map();
208
+ if (memberIds.length > 0) {
209
+ const { data: prods, error } = await sb
210
+ .from("products")
211
+ .select("id, product, plants_per_crate, crates_per_layer, layers_per_cart, weight, floriday_trade_item_id")
212
+ .in("id", memberIds);
213
+ if (error)
214
+ throw new Error(`Indeling geblokkeerd: groepsleden konden niet worden gelezen (${error.message}).`);
215
+ for (const p of Array.isArray(prods) ? prods : [])
216
+ prodById.set(String(p.id), p);
167
217
  }
168
218
  for (const row of rows) {
169
219
  const orderKey = row.order_id ? String(row.order_id) : null;
@@ -176,43 +226,24 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
176
226
  : null;
177
227
  const { belading: base, source } = resolveBelading(row, packing, linePacking);
178
228
  const plants = Math.max(0, Number(row.quantity ?? 0));
179
- const explicitGroupPrefix = /^\[Groep\]/i.test(String(row.product_name ?? ""));
180
- const groupNameKey = String(row.product_name ?? "")
181
- .replace(/^\[Groep\]\s*/i, "")
182
- .trim()
183
- .toLowerCase();
184
- const linkedToRealProduct = typeof row.product_id === "string" && realProductIds.has(row.product_id) && !groupSet.has(row.product_id);
185
- const groupId = row.product_id && groupSet.has(row.product_id)
186
- ? String(row.product_id)
187
- : !linkedToRealProduct || explicitGroupPrefix
188
- ? groupByName.get(groupNameKey) ?? null
189
- : null;
190
- const isGroup = groupId != null || (explicitGroupPrefix && !linkedToRealProduct);
191
- if (!isGroup) {
192
- options.sources?.set(String(row.id), source);
193
- out.push({
194
- order_item_id: row.id,
195
- order_id: row.order_id ?? null,
196
- customer_id: row.customer_id ?? null,
197
- product_id: row.product_id ?? null,
198
- product_name: row.product_name ?? "Onbekend",
199
- plants,
200
- ...base,
201
- packing_authoritative: isAuthoritative(source),
202
- weight_per_crate: row.products?.weight != null ? Number(row.products.weight) : null,
203
- });
229
+ const lines = linesByItem.get(String(row.id)) ?? [];
230
+ const kind = lines[0]?.unit_kind ?? "artikel";
231
+ if (kind === "uitgesloten") {
232
+ options.notes?.push({ order_id: orderKey, order_item_id: String(row.id), note: String(lines[0]?.note ?? "") });
204
233
  continue;
205
234
  }
206
- const { data: members } = groupId
207
- ? await sb
208
- .from("product_group_items")
209
- .select("trade_item_id, quantity, sort_order")
210
- .eq("product_group_id", groupId)
211
- .order("sort_order", { ascending: true })
212
- : { data: [] };
213
- const memberList = Array.isArray(members) ? members : [];
214
- if (memberList.length === 0) {
235
+ if (kind === "onbekend") {
236
+ // Regel zonder artikel: niet raden (ook niet via de naam), indeling blokkeert met reden.
237
+ throw new Error(`Indeling geblokkeerd: regel "${row.product_name ?? "onbekend"}" heeft geen artikel; groepen worden alleen via het groepshoofd herkend.`);
238
+ }
239
+ if (kind === "fout") {
240
+ throw new Error(`Indeling geblokkeerd voor ${row.product_name ?? "onbekend artikel"}: ${lines[0]?.note ?? "fout in de samenstelling"}.`);
241
+ }
242
+ if (kind !== "groepslid") {
215
243
  options.sources?.set(String(row.id), source);
244
+ if (kind === "set") {
245
+ options.components?.set(String(row.id), { lineQty: plants, components: toComponents(lines[0]?.components) });
246
+ }
216
247
  out.push({
217
248
  order_item_id: row.id,
218
249
  order_id: row.order_id ?? null,
@@ -222,90 +253,61 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
222
253
  plants,
223
254
  ...base,
224
255
  packing_authoritative: isAuthoritative(source),
225
- weight_per_crate: null,
256
+ weight_per_crate: row.products?.weight != null ? Number(row.products.weight) : null,
226
257
  });
227
258
  continue;
228
259
  }
229
- const tradeIds = memberList.map((m) => String(m.trade_item_id));
230
- const { data: prods } = await sb
231
- .from("products")
232
- .select("id, product, plants_per_crate, crates_per_layer, layers_per_cart, weight, floriday_trade_item_id")
233
- .or(tradeIds
234
- .flatMap((t) => UUID_RE.test(t) ? [`floriday_trade_item_id.eq.${t}`, `id.eq.${t}`] : [`floriday_trade_item_id.eq.${t}`])
235
- .join(","));
236
- const prodByTrade = new Map();
237
- for (const p of prods ?? []) {
238
- if (p.floriday_trade_item_id)
239
- prodByTrade.set(String(p.floriday_trade_item_id), p);
240
- prodByTrade.set(String(p.id), p);
241
- }
242
- const resolved = memberList.map((m) => {
243
- const p = prodByTrade.get(String(m.trade_item_id));
244
- // Onbekend groepslid: niet gokken en geen kale id als artikelnaam.
245
- if (!p)
246
- return { m, p, belading: { ...base }, source: "geen" };
247
- // Een handmatig vastgezette regel blijft leidend voor de hele groep.
260
+ for (const l of lines) {
261
+ const qty = Math.max(0, Number(l.qty ?? 0));
262
+ if (qty <= 0)
263
+ continue;
264
+ const p = l.product_id ? prodById.get(String(l.product_id)) : null;
265
+ if (!p) {
266
+ throw new Error(`Indeling geblokkeerd voor ${row.product_name ?? "groep"}: groepslid ${l.trade_item_key ?? "onbekend"} heeft geen artikel.`);
267
+ }
268
+ let belading;
269
+ let src;
270
+ const eigenLijn = lineMap?.get(String(p.floriday_trade_item_id ?? "")) ?? lineMap?.get(String(p.id)) ?? null;
248
271
  if (source === "handmatig") {
249
- return { m, p, belading: { ...base }, source };
272
+ belading = { ...base };
273
+ src = source;
250
274
  }
251
- // 1. De Floriday-verpakking van dít lid.
252
- const eigenLijn = lineMap?.get(String(m.trade_item_id)) ?? (p ? lineMap?.get(String(p.id)) ?? null : null);
253
- if (eigenLijn)
254
- return { m, p, belading: { ...eigenLijn }, source: "floriday_line" };
255
- // 2. Het artikel zelf. Nooit de belading van de groepsregel of van een ander lid.
256
- const known = p &&
257
- (positive(p.plants_per_crate) != null ||
258
- positive(p.crates_per_layer) != null ||
259
- positive(p.layers_per_cart) != null);
260
- if (known) {
261
- return {
262
- m,
263
- p,
264
- belading: {
265
- plants_per_crate: positive(p.plants_per_crate) ?? 1,
266
- crates_per_layer: positive(p.crates_per_layer) ?? 1,
267
- layers_per_cart: positive(p.layers_per_cart) ?? 1,
268
- },
269
- source: "product",
275
+ else if (eigenLijn) {
276
+ belading = { ...eigenLijn };
277
+ src = "floriday_line";
278
+ }
279
+ else if (positive(p.plants_per_crate) != null ||
280
+ positive(p.crates_per_layer) != null ||
281
+ positive(p.layers_per_cart) != null) {
282
+ belading = {
283
+ plants_per_crate: positive(p.plants_per_crate) ?? 1,
284
+ crates_per_layer: positive(p.crates_per_layer) ?? 1,
285
+ layers_per_cart: positive(p.layers_per_cart) ?? 1,
270
286
  };
287
+ src = "product";
271
288
  }
272
- // 3. Allerlaatste noodgreep: de order-brede Floriday-verpakking van de order.
273
- if (packing)
274
- return { m, p, belading: { ...packing }, source: "floriday_packing" };
275
- // 4. Onbekend: er wordt niet gegokt, de indeling blokkeert.
276
- return { m, p, belading: { ...base }, source: "geen" };
277
- });
278
- const weightSum = resolved.reduce((s, r) => s + Number(r.m.quantity ?? 1), 0) || resolved.length;
279
- const shares = [];
280
- let assigned = 0;
281
- for (const r of resolved) {
282
- const raw = (plants * Number(r.m.quantity ?? 1)) / weightSum;
283
- const ppc = r.belading.plants_per_crate;
284
- const crates = Math.max(0, Math.round(raw / ppc));
285
- shares.push(crates * ppc);
286
- assigned += crates * ppc;
287
- }
288
- if (assigned !== plants && resolved.length > 0) {
289
- const firstPpc = resolved[0].belading.plants_per_crate;
290
- const deltaCrates = Math.round((plants - assigned) / firstPpc);
291
- shares[0] = Math.max(0, shares[0] + deltaCrates * firstPpc);
292
- }
293
- resolved.forEach((r, idx) => {
294
- if (shares[idx] <= 0)
295
- return;
296
- options.sources?.set(`${row.id}::${r.m.trade_item_id}`, r.source);
289
+ else if (packing) {
290
+ belading = { ...packing };
291
+ src = "floriday_packing";
292
+ }
293
+ else {
294
+ belading = { ...base };
295
+ src = "geen";
296
+ }
297
+ const itemKey = `${row.id}::${l.trade_item_key ?? p.id}`;
298
+ options.sources?.set(itemKey, src);
297
299
  out.push({
298
- order_item_id: `${row.id}::${r.m.trade_item_id}`,
300
+ order_item_id: itemKey,
299
301
  order_id: row.order_id ?? null,
300
302
  customer_id: row.customer_id ?? null,
301
- product_id: r.p?.id ?? null,
302
- product_name: `${row.product_name ?? "Groep"} — ${r.p?.product ?? "onbekend groepslid"}`,
303
- plants: shares[idx],
304
- ...r.belading,
305
- packing_authoritative: isAuthoritative(r.source),
306
- weight_per_crate: r.p?.weight != null ? Number(r.p.weight) : null,
303
+ product_id: String(p.id),
304
+ product_name: `${row.product_name ?? "Groep"} — ${p.product ?? "onbekend groepslid"}`,
305
+ plants: qty,
306
+ ...belading,
307
+ packing_authoritative: isAuthoritative(src),
308
+ weight_per_crate: p.weight != null ? Number(p.weight) : null,
307
309
  });
308
- });
310
+ }
309
311
  }
310
312
  return out;
311
313
  }
@@ -967,11 +969,31 @@ async function replanBufferUnlocked(sb, bufferId, options = {}) {
967
969
  packingByOrderLine.set(String(o.id), perRegel);
968
970
  }
969
971
  const sources = new Map();
972
+ const setComponents = new Map();
973
+ const expandNotes = [];
970
974
  let inputs = await expandOrderItemsForPlanner(sb, rawRows, {
971
975
  packingByOrder,
972
976
  packingByOrderLine,
973
977
  sources,
978
+ components: setComponents,
979
+ notes: expandNotes,
974
980
  });
981
+ // Groepsregel naast ledenregels: niet ingedeeld, wel zichtbaar gemeld bij de order.
982
+ for (const n of expandNotes) {
983
+ if (!n.order_id)
984
+ continue;
985
+ const { error: noteError } = await sb.rpc("logistics_trace_order_step", {
986
+ p_order_id: n.order_id,
987
+ p_node: "stap:beladingindeling",
988
+ p_step: "groepsregel niet ingedeeld",
989
+ p_status: "waarschuwing",
990
+ p_decision: "groepsregel niet meegeteld",
991
+ p_reason: n.note,
992
+ p_next_step: "Karrenindeling",
993
+ });
994
+ if (noteError)
995
+ console.warn(`[indeling] melding bij order ${n.order_id} mislukt: ${noteError.message}`);
996
+ }
975
997
  // Wat al op een bevroren kar ligt, wordt niet opnieuw ingedeeld.
976
998
  let frozenCartCount = 0;
977
999
  let cartNumberStart = 1;
@@ -1192,19 +1214,8 @@ async function replanBufferUnlocked(sb, bufferId, options = {}) {
1192
1214
  });
1193
1215
  if (blockError)
1194
1216
  warnings.push(`Blokkade kon niet worden vastgelegd: ${blockError.message}`);
1195
- // Karren die met een andere (oudere) werkwijze zijn gemaakt mogen niet
1196
- // blijven staan: dan lijkt er een geldige indeling te zijn terwijl de
1197
- // afmetingen ontbreken. Karren van de afmetingenmethode blijven staan.
1198
- // Bij aanvullen blijven bestaande (bevroren) karren altijd staan.
1199
- if (!isSupplement) {
1200
- const { error: staleError } = await sb
1201
- .from("order_carts")
1202
- .delete()
1203
- .eq("buffer_id", bufferId)
1204
- .or("layout_method.is.null,layout_method.neq.afmetingen");
1205
- if (staleError)
1206
- warnings.push(`Oude karrenindeling kon niet worden opgeruimd: ${staleError.message}`);
1207
- }
1217
+ // A3: bij een mislukte indeling blijven bestaande karren staan; de blokkade
1218
+ // hierboven meldt dat de indeling verouderd is en waarom.
1208
1219
  return {
1209
1220
  ok: false,
1210
1221
  bufferId,
@@ -1398,22 +1409,10 @@ async function replanBufferUnlocked(sb, bufferId, options = {}) {
1398
1409
  beladingSources,
1399
1410
  };
1400
1411
  }
1401
- // Opruimen op buffer EN op order. Verhuist een order naar een andere buffer,
1402
- // dan blijven de karren van de oude buffer anders als weeskar staan en telt
1403
- // Controle karren ze dubbel. Bevroren karren (geprint, gescand, gecontroleerd)
1404
- // blijven altijd staan.
1412
+ // Bevroren karren (geprint, gescand, gecontroleerd) blijven altijd staan.
1413
+ // A3: bestaande karren worden nooit weggehaald als de nieuwe indeling mislukt;
1414
+ // vervangen gebeurt alleen in één transactie (logistics_apply_cart_swap).
1405
1415
  const keepFilter = keepCartIds.length > 0 ? `(${keepCartIds.join(",")})` : null;
1406
- const clearExistingCarts = async () => {
1407
- const ids = orderIds.filter(Boolean);
1408
- const del = () => {
1409
- const q = sb.from("order_carts").delete();
1410
- return keepFilter ? q.not("id", "in", keepFilter) : q;
1411
- };
1412
- await del().eq("buffer_id", bufferId);
1413
- if (ids.length > 0) {
1414
- await del().in("order_id", ids);
1415
- }
1416
- };
1417
1416
  // ---------------------------------------------------------------------------
1418
1417
  // Vergelijken in plaats van wissen. Een kar die exact hetzelfde blijft, blijft
1419
1418
  // staan: zelfde rij, zelfde id, zelfde barcode. Alleen echte verschillen worden
@@ -1476,6 +1475,10 @@ async function replanBufferUnlocked(sb, bufferId, options = {}) {
1476
1475
  };
1477
1476
  const items = cart.items.map((i) => ({
1478
1477
  order_item_id: i.order_item_id.includes("::") ? i.order_item_id.split("::")[0] : i.order_item_id,
1478
+ // Set: onderdelen op laagste niveau, geschaald naar het aantal sets op deze kar.
1479
+ components: i.order_item_id.includes("::")
1480
+ ? null
1481
+ : scaleComponents(setComponents.get(i.order_item_id), Math.round(i.crates) * i.plants_per_crate),
1479
1482
  product_id: i.product_id,
1480
1483
  product_name: i.product_name,
1481
1484
  crates: Math.round(i.crates),
@@ -1547,72 +1550,34 @@ async function replanBufferUnlocked(sb, bufferId, options = {}) {
1547
1550
  const teVerwijderen = [];
1548
1551
  for (const lijst of beschikbaarPerSig.values())
1549
1552
  teVerwijderen.push(...lijst);
1550
- if (teVerwijderen.length > 0) {
1551
- for (let s = 0; s < teVerwijderen.length; s += 200) {
1552
- const slice = teVerwijderen.slice(s, s + 200);
1553
- // Alleen voor karren die echt verdwijnen: openstaande printopdrachten en barcodes vervallen.
1554
- await sb
1555
- .from("print_jobs")
1556
- .update({ status: "geannuleerd", foutmelding: "Vervallen: kar verwijderd bij herindeling", updated_at: new Date().toISOString() })
1557
- .in("status", ["wachtend", "bezig"])
1558
- .in("payload->>cart_id", slice);
1559
- await sb.from("pakbon_barcode_inhoud").delete().in("cart_id", slice);
1560
- await sb.from("order_cart_layer_positions").delete().in("cart_id", slice);
1561
- const { error: delError } = await sb.from("order_carts").delete().in("id", slice);
1562
- if (delError) {
1563
- throw new Error(`Verouderde karren konden niet worden opgeruimd: ${delError.message}`);
1564
- }
1565
- }
1566
- }
1567
1553
  let created = behouden.size;
1568
1554
  const idByPlanIndex = new Map(behouden);
1569
- if (nieuw.length === 0) {
1570
- console.log(`[logistiek-engine] buffer ${bufferId}: indeling ongewijzigd, ${behouden.size} kar(ren) behouden, ` +
1571
- `${teVerwijderen.length} verouderde kar(ren) verwijderd.`);
1572
- }
1573
- // Alleen de echt nieuwe karren wegschrijven, in blokken.
1574
- const CHUNK = 40;
1575
- for (let start = 0; start < nieuw.length; start += CHUNK) {
1576
- const chunk = nieuw.slice(start, start + CHUNK);
1577
- const cartRows = chunk.map((g) => g.row);
1578
- const { data: insertedCarts, error } = await sb
1579
- .from("order_carts")
1580
- .insert(cartRows)
1581
- .select("id, order_id, cart_number");
1582
- if (error || !Array.isArray(insertedCarts)) {
1583
- const message = `Ladingdragers konden niet worden opgeslagen: ${error?.message ?? "onbekende fout"}`;
1555
+ // A3: verouderde karren weg en nieuwe karren erbij in één transactie. Mislukt dit,
1556
+ // dan blijven alle bestaande karren onveranderd staan.
1557
+ if (teVerwijderen.length > 0 || nieuw.length > 0) {
1558
+ const { data: swap, error: swapError } = await sb.rpc("logistics_apply_cart_swap", {
1559
+ p_delete_ids: teVerwijderen,
1560
+ p_new_carts: nieuw.map((g) => ({ planIndex: g.planIndex, row: g.row, items: g.items })),
1561
+ });
1562
+ if (swapError) {
1563
+ const message = `Nieuwe karindeling kon niet worden opgeslagen; de bestaande karren blijven staan: ${swapError.message}`;
1584
1564
  console.error(`[logistiek-engine] buffer ${bufferId}: ${message}`);
1585
- await clearExistingCarts();
1586
1565
  throw new Error(message);
1587
1566
  }
1588
- created += insertedCarts.length;
1589
- const identityKey = (orderId, cartNumber) => `${orderId == null ? "shared" : String(orderId)}:${Number(cartNumber)}`;
1590
- const idByIdentity = new Map(insertedCarts.map((c) => [identityKey(c.order_id, c.cart_number), String(c.id)]));
1591
- const itemRows = [];
1592
- for (const g of chunk) {
1593
- const cartId = idByIdentity.get(identityKey(g.row["order_id"], g.row["cart_number"]));
1594
- if (!cartId)
1595
- continue;
1596
- idByPlanIndex.set(g.planIndex, cartId);
1597
- for (const i of g.items)
1598
- itemRows.push({ ...i, cart_id: cartId });
1599
- }
1600
- for (let s = 0; s < itemRows.length; s += 500) {
1601
- const slice = itemRows.slice(s, s + 500);
1602
- if (slice.length === 0)
1603
- continue;
1604
- const { error: itemError } = await sb.from("order_cart_items").insert(slice);
1605
- if (itemError) {
1606
- // Ladingdragers zonder inhoud zijn een ongeldige uitkomst: alles terugdraaien
1607
- // en de planrun laten falen, zodat de wachtrij het opnieuw of geblokkeerd oppakt.
1608
- const message = `Inhoud van de ladingdragers kon niet worden opgeslagen: ${itemError.message}` +
1609
- (itemError.details ? ` — ${itemError.details}` : "") +
1610
- (itemError.hint ? ` (${itemError.hint})` : "");
1611
- console.error(`[logistiek-engine] buffer ${bufferId}: ${message}`);
1612
- await clearExistingCarts();
1613
- throw new Error(message);
1567
+ const mapping = Array.isArray(swap?.karren) ? swap.karren : [];
1568
+ for (const m of mapping) {
1569
+ if (m && m.id != null && Number.isFinite(Number(m.planIndex))) {
1570
+ idByPlanIndex.set(Number(m.planIndex), String(m.id));
1614
1571
  }
1615
1572
  }
1573
+ created += mapping.length;
1574
+ }
1575
+ if (nieuw.length === 0) {
1576
+ console.log(`[logistiek-engine] buffer ${bufferId}: indeling ongewijzigd, ${behouden.size} kar(ren) behouden, ` +
1577
+ `${teVerwijderen.length} verouderde kar(ren) verwijderd.`);
1578
+ }
1579
+ {
1580
+ const chunk = nieuw;
1616
1581
  // Fysieke laagindeling per kar. Alleen de afmetingenmethode levert posities;
1617
1582
  // bij belading blijft de indeling leeg ("nog geen laagindeling aangeleverd").
1618
1583
  if (dimensionPlan) {
@@ -1633,9 +1598,7 @@ async function replanBufferUnlocked(sb, bufferId, options = {}) {
1633
1598
  .from("order_cart_layer_positions")
1634
1599
  .upsert(slice, { onConflict: "cart_id,layer,keg_id" });
1635
1600
  if (posError) {
1636
- // Een halve indeling is misleidend: de volledige buffer verwijderen
1637
- // en de wachtrij laten falen, zodat niets als geldig wordt getoond.
1638
- await clearExistingCarts();
1601
+ // A3: karren blijven staan; de planrun faalt met een duidelijke reden.
1639
1602
  throw new Error(`De fysieke laagindeling van kar ${cart.cart_number} kon niet worden opgeslagen: ${posError.message}`);
1640
1603
  }
1641
1604
  }
@@ -1645,7 +1608,6 @@ async function replanBufferUnlocked(sb, bufferId, options = {}) {
1645
1608
  .select("id", { count: "exact", head: true })
1646
1609
  .eq("cart_id", cartId);
1647
1610
  if (countError || storedPositions !== expectedPositions) {
1648
- await clearExistingCarts();
1649
1611
  throw new Error(`De fysieke indeling van kar ${cart.cart_number} is niet volledig opgeslagen: ` +
1650
1612
  `${expectedPositions} fusten verwacht, ${storedPositions ?? 0} opgeslagen.`);
1651
1613
  }
@@ -1674,9 +1636,8 @@ async function replanBufferUnlocked(sb, bufferId, options = {}) {
1674
1636
  const reason = verifyError
1675
1637
  ? `Karrenindeling kon niet worden gecontroleerd: ${verifyError.message}`
1676
1638
  : `Karrenindeling is niet volgens de ingestelde werkwijze "${expectedMethod}" vastgelegd ` +
1677
- `(${wrong.length} van ${rows.length} karren). Er is niets ingedeeld.`;
1639
+ `(${wrong.length} van ${rows.length} karren). De karren blijven staan; de order is geblokkeerd.`;
1678
1640
  console.error(`[logistiek-engine] buffer ${bufferId}: ${reason}`);
1679
- await clearExistingCarts();
1680
1641
  await sb.rpc("logistics_plan_block", {
1681
1642
  p_order_ids: orderIds,
1682
1643
  p_reason: reason,