@flowselections/logistiek-engine 1.0.14 → 1.0.16

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 (31) hide show
  1. package/dist-lib/integrations/supabase/auth-middleware.d.ts +53 -0
  2. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  3. package/dist-lib/integrations/supabase/client.server.d.ts +53 -0
  4. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  5. package/dist-lib/integrations/supabase/moduleClient.d.ts +53 -0
  6. package/dist-lib/integrations/supabase/moduleClient.d.ts.map +1 -1
  7. package/dist-lib/integrations/supabase/types.d.ts +53 -0
  8. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  9. package/dist-lib/lib/bulkLoadCarrierPlanning.server.d.ts +2 -0
  10. package/dist-lib/lib/bulkLoadCarrierPlanning.server.d.ts.map +1 -1
  11. package/dist-lib/lib/bulkLoadCarrierPlanning.server.js +11 -0
  12. package/dist-lib/lib/floridayDocuments.functions.d.ts +159 -0
  13. package/dist-lib/lib/floridayDocuments.functions.d.ts.map +1 -1
  14. package/dist-lib/lib/floridayPush.functions.d.ts +53 -0
  15. package/dist-lib/lib/floridayPush.functions.d.ts.map +1 -1
  16. package/dist-lib/lib/loadCarrierPlanner.d.ts +1 -1
  17. package/dist-lib/lib/loadCarrierPlanner.js +1 -1
  18. package/dist-lib/lib/loadCarrierPlanning.functions.d.ts +265 -0
  19. package/dist-lib/lib/loadCarrierPlanning.functions.d.ts.map +1 -1
  20. package/dist-lib/lib/loadCarrierPlanning.server.d.ts +33 -2
  21. package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -1
  22. package/dist-lib/lib/loadCarrierPlanning.server.js +245 -47
  23. package/dist-lib/lib/loadCarrierTaskSync.server.js +1 -1
  24. package/dist-lib/lib/logisticsRecalcJob.functions.d.ts +106 -0
  25. package/dist-lib/lib/logisticsRecalcJob.functions.d.ts.map +1 -1
  26. package/dist-lib/lib/logisticsRecalculate.functions.d.ts +53 -0
  27. package/dist-lib/lib/logisticsRecalculate.functions.d.ts.map +1 -1
  28. package/dist-lib/lib/planQueue.server.d.ts +2 -0
  29. package/dist-lib/lib/planQueue.server.d.ts.map +1 -1
  30. package/dist-lib/lib/planQueue.server.js +22 -4
  31. package/package.json +1 -1
@@ -12,7 +12,27 @@ export declare function buildBufferKey(order: {
12
12
  location: string;
13
13
  };
14
14
  export declare function resolveBufferForOrderInternal(sb: SupabaseClient, orderId: string): Promise<string | null>;
15
- export declare function expandOrderItemsForPlanner(sb: SupabaseClient, rows: Array<Record<string, any>>): Promise<PlannerInputItem[]>;
15
+ interface Belading {
16
+ plants_per_crate: number;
17
+ crates_per_layer: number;
18
+ layers_per_cart: number;
19
+ }
20
+ /** Waar de belading van een orderregel vandaan komt. */
21
+ export type BeladingSource = "floriday_packing" | "order_items" | "product" | "geen";
22
+ /**
23
+ * Zet een Floriday-verpakking om naar beladingwaarden.
24
+ * pieces_per_package -> planten per fust
25
+ * packages_per_layer -> fusten per laag
26
+ * layers_per_load_carrier -> lagen per ladingdrager
27
+ */
28
+ export declare function beladingFromFloridayPacking(packing: unknown): Belading | null;
29
+ export interface ExpandOptions {
30
+ /** Beladingwaarden per order uit custom_fields.floriday_packing. */
31
+ packingByOrder?: Map<string, Belading>;
32
+ /** Wordt gevuld met de gebruikte bron per orderregel-sleutel. */
33
+ sources?: Map<string, BeladingSource>;
34
+ }
35
+ export declare function expandOrderItemsForPlanner(sb: SupabaseClient, rows: Array<Record<string, any>>, options?: ExpandOptions): Promise<PlannerInputItem[]>;
16
36
  export declare function loadCarrierProfiles(sb: SupabaseClient): Promise<LoadCarrierTypeRow[]>;
17
37
  export interface BeladingRules {
18
38
  maxOrders: number | null;
@@ -25,6 +45,12 @@ export interface BeladingRules {
25
45
  export declare function loadBeladingRules(sb: SupabaseClient): Promise<BeladingRules | null>;
26
46
  /** Past de grenzen uit de werkwijze toe op het capaciteitsprofiel van een ladingdrager. */
27
47
  export declare function applyRulesToProfile(profile: CarrierProfile, rules: BeladingRules | null): CarrierProfile;
48
+ /**
49
+ * Verdeelt de planten over ladingdragers volgens de Floriday-verhouding:
50
+ * planten per kar = pieces_per_package x packages_per_layer x layers_per_load_carrier.
51
+ * Geen optimalisatie, geen mengregels, geen laagherverdeling — alleen vullen.
52
+ */
53
+ export declare function planByFloridayPacking(inputs: PlannerInputItem[], profile: CarrierProfile, packing: Belading): PlannedLoadUnit[];
28
54
  export interface ReplanResult {
29
55
  ok: boolean;
30
56
  bufferId: string;
@@ -33,7 +59,11 @@ export interface ReplanResult {
33
59
  replacedManualCount: number;
34
60
  usedFloridayPacking: boolean;
35
61
  warnings: string[];
36
- skipped?: "locked";
62
+ skipped?: "locked" | "blocked";
63
+ /** Leesbare reden waarom er niets is ingedeeld. */
64
+ blockedReason?: string;
65
+ /** Gebruikte beladingbronnen in deze buffer, met aantal orderregels per bron. */
66
+ beladingSources?: Record<string, number>;
37
67
  carts?: PlannedLoadUnit[];
38
68
  }
39
69
  export interface ReplanOptions {
@@ -44,4 +74,5 @@ export interface ReplanOptions {
44
74
  }
45
75
  export declare function replanBufferInternal(sb: SupabaseClient, bufferId: string, options?: ReplanOptions): Promise<ReplanResult>;
46
76
  export declare function replanOrderInternal(sb: SupabaseClient, orderId: string, options?: ReplanOptions): Promise<ReplanResult | null>;
77
+ export {};
47
78
  //# sourceMappingURL=loadCarrierPlanning.server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loadCarrierPlanning.server.d.ts","sourceRoot":"","sources":["../../src/lib/loadCarrierPlanning.server.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAA0B,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAiCrF,wBAAgB,cAAc,CAC5B,KAAK,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,aAAa,CAAC,EAAE,GAAG,CAAA;CAAE,EACtG,IAAI,EAAE,MAAM,GACX;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAqCvD;AAID,wBAAsB,6BAA6B,CAAC,EAAE,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAmE/G;AAgBD,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,cAAc,EAClB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,GAC/B,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA0H7B;AAMD,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAO3F;AAUD,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,OAAO,CAAC;CACtB;AAOD,wFAAwF;AACxF,wBAAsB,iBAAiB,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAsBzF;AAED,2FAA2F;AAC3F,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,aAAa,GAAG,IAAI,GAAG,cAAc,CAaxG;AAWD,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,OAAO,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,QAAQ,CAAC;IACnB,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,4CAA4C;IAC5C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,cAAc,EAClB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC,CA0OvB;AAED,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,cAAc,EAClB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAI9B"}
1
+ {"version":3,"file":"loadCarrierPlanning.server.d.ts","sourceRoot":"","sources":["../../src/lib/loadCarrierPlanning.server.ts"],"names":[],"mappings":"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"}
@@ -128,10 +128,53 @@ export async function resolveBufferForOrderInternal(sb, orderId) {
128
128
  }
129
129
  return bufferId;
130
130
  }
131
- function isDefaultBelading(b) {
132
- return b.plants_per_crate === 1 && b.crates_per_layer === 1 && b.layers_per_cart === 1;
131
+ const positive = (v) => {
132
+ const n = Number(v);
133
+ return Number.isFinite(n) && n > 0 ? Math.round(n) : null;
134
+ };
135
+ /**
136
+ * Zet een Floriday-verpakking om naar beladingwaarden.
137
+ * pieces_per_package -> planten per fust
138
+ * packages_per_layer -> fusten per laag
139
+ * layers_per_load_carrier -> lagen per ladingdrager
140
+ */
141
+ export function beladingFromFloridayPacking(packing) {
142
+ if (!packing || typeof packing !== "object")
143
+ return null;
144
+ const p = packing;
145
+ const ppc = positive(p.pieces_per_package);
146
+ const cpl = positive(p.packages_per_layer);
147
+ const lpc = positive(p.layers_per_load_carrier);
148
+ if (ppc == null || cpl == null || lpc == null)
149
+ return null;
150
+ return { plants_per_crate: ppc, crates_per_layer: cpl, layers_per_cart: lpc };
151
+ }
152
+ /** Beladingbronnen in volgorde: Floriday-verpakking > orderregel > product > geen. */
153
+ function resolveBelading(row, packing) {
154
+ if (packing)
155
+ return { belading: packing, source: "floriday_packing" };
156
+ const fromRow = positive(row.plants_per_crate) != null || positive(row.crates_per_layer) != null || positive(row.layers_per_cart) != null
157
+ ? {
158
+ plants_per_crate: positive(row.plants_per_crate) ?? 1,
159
+ crates_per_layer: positive(row.crates_per_layer) ?? 1,
160
+ layers_per_cart: positive(row.layers_per_cart) ?? 1,
161
+ }
162
+ : null;
163
+ if (fromRow)
164
+ return { belading: fromRow, source: "order_items" };
165
+ const p = row.products ?? null;
166
+ const fromProduct = p && (positive(p.plants_per_crate) != null || positive(p.crates_per_layer) != null || positive(p.layers_per_cart) != null)
167
+ ? {
168
+ plants_per_crate: positive(p.plants_per_crate) ?? 1,
169
+ crates_per_layer: positive(p.crates_per_layer) ?? 1,
170
+ layers_per_cart: positive(p.layers_per_cart) ?? 1,
171
+ }
172
+ : null;
173
+ if (fromProduct)
174
+ return { belading: fromProduct, source: "product" };
175
+ return { belading: { plants_per_crate: 1, crates_per_layer: 1, layers_per_cart: 1 }, source: "geen" };
133
176
  }
134
- export async function expandOrderItemsForPlanner(sb, rows) {
177
+ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
135
178
  const out = [];
136
179
  const groupIds = rows
137
180
  .map((r) => r.product_id)
@@ -142,15 +185,13 @@ export async function expandOrderItemsForPlanner(sb, rows) {
142
185
  groupSet = new Set((groups ?? []).map((g) => g.id));
143
186
  }
144
187
  for (const row of rows) {
145
- const base = {
146
- plants_per_crate: Math.max(1, Number(row.plants_per_crate ?? row.products?.plants_per_crate ?? 1)),
147
- crates_per_layer: Math.max(1, Number(row.crates_per_layer ?? row.products?.crates_per_layer ?? 1)),
148
- layers_per_cart: Math.max(1, Number(row.layers_per_cart ?? row.products?.layers_per_cart ?? 1)),
149
- };
188
+ const packing = row.order_id ? options.packingByOrder?.get(String(row.order_id)) ?? null : null;
189
+ const { belading: base, source } = resolveBelading(row, packing);
150
190
  const plants = Math.max(0, Number(row.quantity ?? 0));
151
191
  const isGroup = (row.product_id && groupSet.has(row.product_id)) ||
152
192
  String(row.product_name ?? "").startsWith("[Groep]");
153
193
  if (!isGroup) {
194
+ options.sources?.set(String(row.id), source);
154
195
  out.push({
155
196
  order_item_id: row.id,
156
197
  order_id: row.order_id ?? null,
@@ -170,6 +211,7 @@ export async function expandOrderItemsForPlanner(sb, rows) {
170
211
  .order("sort_order", { ascending: true });
171
212
  const memberList = Array.isArray(members) ? members : [];
172
213
  if (memberList.length === 0) {
214
+ options.sources?.set(String(row.id), source);
173
215
  out.push({
174
216
  order_item_id: row.id,
175
217
  order_id: row.order_id ?? null,
@@ -195,22 +237,22 @@ export async function expandOrderItemsForPlanner(sb, rows) {
195
237
  }
196
238
  const resolved = memberList.map((m) => {
197
239
  const p = prodByTrade.get(String(m.trade_item_id));
198
- const belading = p
240
+ // Een Floriday-verpakking geldt voor de hele orderregel, dus ook voor de leden.
241
+ if (packing)
242
+ return { m, p, belading: { ...base }, source: "floriday_packing" };
243
+ const known = p &&
244
+ (positive(p.plants_per_crate) != null ||
245
+ positive(p.crates_per_layer) != null ||
246
+ positive(p.layers_per_cart) != null);
247
+ const belading = known
199
248
  ? {
200
- plants_per_crate: Math.max(1, Number(p.plants_per_crate ?? 1)),
201
- crates_per_layer: Math.max(1, Number(p.crates_per_layer ?? 1)),
202
- layers_per_cart: Math.max(1, Number(p.layers_per_cart ?? 1)),
249
+ plants_per_crate: positive(p.plants_per_crate) ?? 1,
250
+ crates_per_layer: positive(p.crates_per_layer) ?? 1,
251
+ layers_per_cart: positive(p.layers_per_cart) ?? 1,
203
252
  }
204
- : { plants_per_crate: 1, crates_per_layer: 1, layers_per_cart: 1 };
205
- return { m, p, belading };
253
+ : { ...base };
254
+ return { m, p, belading, source: (known ? "product" : source) };
206
255
  });
207
- // Fallback: sibling met bekende belading, anders de groepsregel zelf.
208
- const known = resolved.find((r) => !isDefaultBelading(r.belading))?.belading;
209
- for (const r of resolved) {
210
- if (isDefaultBelading(r.belading)) {
211
- r.belading = known ?? (isDefaultBelading(base) ? r.belading : base);
212
- }
213
- }
214
256
  const weightSum = resolved.reduce((s, r) => s + Number(r.m.quantity ?? 1), 0) || resolved.length;
215
257
  const shares = [];
216
258
  let assigned = 0;
@@ -229,6 +271,7 @@ export async function expandOrderItemsForPlanner(sb, rows) {
229
271
  resolved.forEach((r, idx) => {
230
272
  if (shares[idx] <= 0)
231
273
  return;
274
+ options.sources?.set(`${row.id}::${r.m.trade_item_id}`, r.source);
232
275
  out.push({
233
276
  order_item_id: `${row.id}::${r.m.trade_item_id}`,
234
277
  order_id: row.order_id ?? null,
@@ -299,6 +342,75 @@ export function applyRulesToProfile(profile, rules) {
299
342
  };
300
343
  }
301
344
  // ---------------------------------------------------------------------------
345
+ // Overname-pad: de beladingverhouding van Floriday letterlijk volgen
346
+ // ---------------------------------------------------------------------------
347
+ /**
348
+ * Verdeelt de planten over ladingdragers volgens de Floriday-verhouding:
349
+ * planten per kar = pieces_per_package x packages_per_layer x layers_per_load_carrier.
350
+ * Geen optimalisatie, geen mengregels, geen laagherverdeling — alleen vullen.
351
+ */
352
+ export function planByFloridayPacking(inputs, profile, packing) {
353
+ const ppc = Math.max(1, packing.plants_per_crate);
354
+ const cpl = Math.max(1, packing.crates_per_layer);
355
+ const lpc = Math.max(1, packing.layers_per_cart);
356
+ const perCart = ppc * cpl * lpc;
357
+ const carts = [];
358
+ let current = null;
359
+ let currentPlants = 0;
360
+ const openCart = () => {
361
+ current = {
362
+ cart_number: carts.length + 1,
363
+ carrier_type: profile.code,
364
+ carrier_type_id: profile.id ?? null,
365
+ source: "auto",
366
+ is_complete: false,
367
+ occupancy_pct: 0,
368
+ total_layers_used: 0,
369
+ max_layers: lpc,
370
+ weight_kg: null,
371
+ items: [],
372
+ };
373
+ currentPlants = 0;
374
+ carts.push(current);
375
+ };
376
+ for (const input of inputs) {
377
+ let left = Math.max(0, Math.round(input.plants));
378
+ while (left > 0) {
379
+ if (!current || currentPlants >= perCart)
380
+ openCart();
381
+ const cart = current;
382
+ const take = Math.min(left, perCart - currentPlants);
383
+ const crates = Math.max(1, Math.ceil(take / ppc));
384
+ const layerFraction = crates / cpl;
385
+ const item = {
386
+ order_item_id: input.order_item_id,
387
+ order_id: input.order_id ?? null,
388
+ product_id: input.product_id,
389
+ product_name: input.product_name,
390
+ crates,
391
+ plants: take,
392
+ boxes: crates,
393
+ weight_kg: input.weight_per_crate != null ? Number((input.weight_per_crate * crates).toFixed(2)) : null,
394
+ plants_per_crate: ppc,
395
+ crates_per_layer: cpl,
396
+ layers_per_cart: lpc,
397
+ layer_fraction: Number(layerFraction.toFixed(3)),
398
+ layer_share_pct: Number(((100 * layerFraction) / lpc).toFixed(2)),
399
+ };
400
+ cart.items.push(item);
401
+ currentPlants += take;
402
+ left -= take;
403
+ cart.total_layers_used = Number(cart.items.reduce((s, i) => s + i.layer_fraction, 0).toFixed(2));
404
+ cart.occupancy_pct = Number(((100 * currentPlants) / perCart).toFixed(1));
405
+ cart.is_complete = currentPlants >= perCart;
406
+ cart.weight_kg = cart.items.some((i) => i.weight_kg != null)
407
+ ? Number(cart.items.reduce((s, i) => s + (i.weight_kg ?? 0), 0).toFixed(2))
408
+ : null;
409
+ }
410
+ }
411
+ return carts;
412
+ }
413
+ // ---------------------------------------------------------------------------
302
414
  // Herplannen van een buffer
303
415
  // ---------------------------------------------------------------------------
304
416
  /** Maximaal aantal ladingdragers dat één orderbuffer mag opleveren. */
@@ -335,11 +447,23 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
335
447
  ...it,
336
448
  customer_id: customerByOrder.get(it.order_id) ?? null,
337
449
  }));
338
- const inputs = await expandOrderItemsForPlanner(sb, rawRows);
450
+ // Floriday-verpakking per order is de eerste beladingbron.
451
+ const packingByOrder = new Map();
452
+ for (const o of orderRows) {
453
+ const belading = beladingFromFloridayPacking(o.custom_fields?.floriday_packing);
454
+ if (belading)
455
+ packingByOrder.set(String(o.id), belading);
456
+ }
457
+ const sources = new Map();
458
+ const inputs = await expandOrderItemsForPlanner(sb, rawRows, { packingByOrder, sources });
339
459
  const itemToOrder = new Map();
340
460
  for (const i of inputs)
341
461
  if (i.order_id)
342
462
  itemToOrder.set(i.order_item_id, i.order_id);
463
+ const beladingSources = {};
464
+ for (const s of sources.values())
465
+ beladingSources[s] = (beladingSources[s] ?? 0) + 1;
466
+ console.log(`[logistiek-engine] buffer ${bufferId}: beladingbronnen ${JSON.stringify(beladingSources)}`);
343
467
  // Bestaande beladingen tellen en wissen.
344
468
  const { data: existingCarts } = await sb
345
469
  .from("order_carts")
@@ -355,27 +479,59 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
355
479
  const profiles = await loadCarrierProfiles(sb);
356
480
  const rules = await loadBeladingRules(sb);
357
481
  const defaultProfile = profiles.find((p) => p.is_default) ?? profileFor(profiles, "trolley");
482
+ // Guard: zonder beladingbron mag er niet gegokt worden. Ladingdragers zonder
483
+ // lagenmodel (los product / enkele eenheid) hebben de belading niet nodig.
484
+ const guardProfile = profileFor(profiles, options.carrierType ?? defaultProfile.code);
485
+ const needsBelading = guardProfile.capacity_model !== "single" && guardProfile.capacity_model !== "none";
486
+ const unknownItems = inputs.filter((i) => sources.get(i.order_item_id) === "geen" && i.plants > 0);
487
+ if (needsBelading && unknownItems.length > 0 && !options.dryRun) {
488
+ const reason = `Belading onbekend voor ${unknownItems.length} orderregel(s): geen Floriday-verpakking, ` +
489
+ `geen waarden op de orderregel en geen productinstelling. Er is niets ingedeeld.`;
490
+ console.error(`[logistiek-engine] buffer ${bufferId} geblokkeerd: ${reason}`);
491
+ const { error: blockError } = await sb.rpc("logistics_plan_block", {
492
+ p_order_ids: orderIds,
493
+ p_reason: reason,
494
+ p_details: {
495
+ bufferId,
496
+ path: "typescript",
497
+ carrier: guardProfile.code,
498
+ beladingSources,
499
+ items: unknownItems.slice(0, 20).map((i) => ({ id: i.order_item_id, product: i.product_name })),
500
+ },
501
+ });
502
+ if (blockError)
503
+ warnings.push(`Blokkade kon niet worden vastgelegd: ${blockError.message}`);
504
+ return {
505
+ ok: false,
506
+ bufferId,
507
+ orderIds,
508
+ cartsCreated: 0,
509
+ replacedManualCount: 0,
510
+ usedFloridayPacking: false,
511
+ warnings: [...warnings, reason],
512
+ skipped: "blocked",
513
+ blockedReason: reason,
514
+ beladingSources,
515
+ };
516
+ }
358
517
  let plannedCarts = [];
359
518
  let usedFloridayPacking = false;
519
+ // Overname-pad: één order in de buffer én een volledige Floriday-verpakking.
520
+ // Dan neemt de engine de verhouding van Floriday over en optimaliseert niet.
521
+ // (De oude tak eiste `floriday_packing.carts` als array; Floriday stuurt zo'n
522
+ // kant-en-klare karrenlijst nergens mee, die tak is nooit genomen en is
523
+ // daarom verwijderd.)
360
524
  const solo = orderRows.length === 1 ? orderRows[0] : null;
361
- const floridayPacking = solo?.custom_fields?.floriday_packing ?? null;
362
- const isFo = solo && (solo.custom_fields?.source === "floriday" || floridayPacking?.pieces_per_package);
363
- if (isFo && floridayPacking && Array.isArray(floridayPacking.carts)) {
525
+ const soloPacking = solo ? beladingFromFloridayPacking(solo.custom_fields?.floriday_packing) : null;
526
+ const takeoverProfile = profileFor(profiles, options.carrierType ?? defaultProfile.code);
527
+ const takeoverAllowed = takeoverProfile.capacity_model === "layered" || takeoverProfile.capacity_model === "stacked";
528
+ if (solo && soloPacking && takeoverAllowed) {
364
529
  usedFloridayPacking = true;
365
- const code = options.carrierType ?? floridayPacking.carrier_type ?? defaultProfile.code;
366
- const profile = profileFor(profiles, code);
367
- plannedCarts = floridayPacking.carts.map((c, idx) => ({
368
- cart_number: idx + 1,
369
- carrier_type: profile.code,
370
- carrier_type_id: profile.id ?? null,
371
- source: "auto",
372
- is_complete: Boolean(c.is_complete),
373
- occupancy_pct: Number(c.occupancy_pct ?? 100),
374
- total_layers_used: Number(c.total_layers_used ?? profile.layers_per_carrier),
375
- max_layers: profile.layers_per_carrier,
376
- weight_kg: null,
377
- items: Array.isArray(c.items) ? c.items : [],
378
- }));
530
+ plannedCarts = planByFloridayPacking(inputs, takeoverProfile, soloPacking);
531
+ console.log(`[logistiek-engine] buffer ${bufferId}: overname Floriday-verpakking ` +
532
+ `(${soloPacking.plants_per_crate}x${soloPacking.crates_per_layer}x${soloPacking.layers_per_cart} = ` +
533
+ `${soloPacking.plants_per_crate * soloPacking.crates_per_layer * soloPacking.layers_per_cart} planten/kar) ` +
534
+ `-> ${plannedCarts.length} ladingdragers`);
379
535
  }
380
536
  else {
381
537
  // Ladingdrager per orderregel bepalen en per type een ronde draaien.
@@ -407,11 +563,39 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
407
563
  c.cart_number = idx + 1;
408
564
  });
409
565
  }
410
- // Veiligheidsgrens: een onrealistisch grote order (bijv. een verkeerd
411
- // ingevoerd aantal planten) mag de indeling niet laten vastlopen.
566
+ // Veiligheidsgrens: een onrealistisch grote uitkomst wordt niet afgekapt maar
567
+ // geblokkeerd. Een half plan laat planten stilzwijgend uit de pickstroom vallen.
412
568
  if (plannedCarts.length > MAX_CARTS_PER_BUFFER) {
413
- warnings.push(`Deze orderbuffer levert ${plannedCarts.length} ladingdragers op. Er zijn er ${MAX_CARTS_PER_BUFFER} ingedeeld; controleer de aantallen of de belading van de producten.`);
414
- plannedCarts = plannedCarts.slice(0, MAX_CARTS_PER_BUFFER);
569
+ const reason = `Deze orderbuffer levert ${plannedCarts.length} ladingdragers op, meer dan de grens van ` +
570
+ `${MAX_CARTS_PER_BUFFER}. Er is niets ingedeeld; controleer de aantallen of de belading van de producten.`;
571
+ console.error(`[logistiek-engine] buffer ${bufferId} geblokkeerd: ${reason}`);
572
+ if (!options.dryRun) {
573
+ const { error: blockError } = await sb.rpc("logistics_plan_block", {
574
+ p_order_ids: orderIds,
575
+ p_reason: reason,
576
+ p_details: {
577
+ bufferId,
578
+ path: "typescript",
579
+ carts: plannedCarts.length,
580
+ limit: MAX_CARTS_PER_BUFFER,
581
+ beladingSources,
582
+ },
583
+ });
584
+ if (blockError)
585
+ warnings.push(`Blokkade kon niet worden vastgelegd: ${blockError.message}`);
586
+ }
587
+ return {
588
+ ok: false,
589
+ bufferId,
590
+ orderIds,
591
+ cartsCreated: 0,
592
+ replacedManualCount,
593
+ usedFloridayPacking,
594
+ warnings: [...warnings, reason],
595
+ skipped: "blocked",
596
+ blockedReason: reason,
597
+ beladingSources,
598
+ };
415
599
  }
416
600
  if (options.dryRun) {
417
601
  return {
@@ -460,8 +644,10 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
460
644
  .insert(cartRows)
461
645
  .select("id, cart_number");
462
646
  if (error || !Array.isArray(insertedCarts)) {
463
- warnings.push(`Een deel van de ladingdragers kon niet worden opgeslagen: ${error?.message ?? ""}`);
464
- continue;
647
+ const message = `Ladingdragers konden niet worden opgeslagen: ${error?.message ?? "onbekende fout"}`;
648
+ console.error(`[logistiek-engine] buffer ${bufferId}: ${message}`);
649
+ await sb.from("order_carts").delete().eq("buffer_id", bufferId);
650
+ throw new Error(message);
465
651
  }
466
652
  created += insertedCarts.length;
467
653
  const idByNumber = new Map(insertedCarts.map((c) => [Number(c.cart_number), String(c.id)]));
@@ -490,8 +676,19 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
490
676
  }
491
677
  for (let s = 0; s < itemRows.length; s += 500) {
492
678
  const slice = itemRows.slice(s, s + 500);
493
- if (slice.length > 0)
494
- await sb.from("order_cart_items").insert(slice);
679
+ if (slice.length === 0)
680
+ continue;
681
+ const { error: itemError } = await sb.from("order_cart_items").insert(slice);
682
+ if (itemError) {
683
+ // Ladingdragers zonder inhoud zijn een ongeldige uitkomst: alles terugdraaien
684
+ // en de planrun laten falen, zodat de wachtrij het opnieuw of geblokkeerd oppakt.
685
+ const message = `Inhoud van de ladingdragers kon niet worden opgeslagen: ${itemError.message}` +
686
+ (itemError.details ? ` — ${itemError.details}` : "") +
687
+ (itemError.hint ? ` (${itemError.hint})` : "");
688
+ console.error(`[logistiek-engine] buffer ${bufferId}: ${message}`);
689
+ await sb.from("order_carts").delete().eq("buffer_id", bufferId);
690
+ throw new Error(message);
691
+ }
495
692
  }
496
693
  }
497
694
  return {
@@ -502,6 +699,7 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
502
699
  replacedManualCount,
503
700
  usedFloridayPacking,
504
701
  warnings,
702
+ beladingSources,
505
703
  carts: plannedCarts,
506
704
  };
507
705
  }
@@ -6,7 +6,7 @@
6
6
  // ============================================================================
7
7
  const OPEN_STATUSES = ["planned", "ready", "in_progress"];
8
8
  export async function attachIndelingToTasks(sb, result) {
9
- if (!result || result.skipped === "locked")
9
+ if (!result || result.skipped)
10
10
  return 0;
11
11
  const carts = Array.isArray(result.carts) ? result.carts : [];
12
12
  if (result.orderIds.length === 0)
@@ -15727,6 +15727,45 @@ export declare const startRecalcJob: import("@tanstack/start-client-core").Optio
15727
15727
  };
15728
15728
  Relationships: [];
15729
15729
  };
15730
+ stock_consumption_alerts: {
15731
+ Row: {
15732
+ alert_type: string;
15733
+ created_at: string;
15734
+ id: string;
15735
+ mutation_log_id: string | null;
15736
+ order_id: string | null;
15737
+ order_item_id: string | null;
15738
+ qty: number;
15739
+ reason: string | null;
15740
+ resolved_at: string | null;
15741
+ updated_at: string;
15742
+ };
15743
+ Insert: {
15744
+ alert_type: string;
15745
+ created_at?: string;
15746
+ id?: string;
15747
+ mutation_log_id?: string | null;
15748
+ order_id?: string | null;
15749
+ order_item_id?: string | null;
15750
+ qty?: number;
15751
+ reason?: string | null;
15752
+ resolved_at?: string | null;
15753
+ updated_at?: string;
15754
+ };
15755
+ Update: {
15756
+ alert_type?: string;
15757
+ created_at?: string;
15758
+ id?: string;
15759
+ mutation_log_id?: string | null;
15760
+ order_id?: string | null;
15761
+ order_item_id?: string | null;
15762
+ qty?: number;
15763
+ reason?: string | null;
15764
+ resolved_at?: string | null;
15765
+ updated_at?: string;
15766
+ };
15767
+ Relationships: [];
15768
+ };
15730
15769
  stock_import_runs: {
15731
15770
  Row: {
15732
15771
  connection_id: string | null;
@@ -21399,6 +21438,14 @@ export declare const startRecalcJob: import("@tanstack/start-client-core").Optio
21399
21438
  };
21400
21439
  Returns: import("../integrations/supabase/types").Json;
21401
21440
  };
21441
+ logistics_plan_block: {
21442
+ Args: {
21443
+ p_details?: import("../integrations/supabase/types").Json;
21444
+ p_order_ids: string[];
21445
+ p_reason: string;
21446
+ };
21447
+ Returns: number;
21448
+ };
21402
21449
  logistics_plan_buffer_sql: {
21403
21450
  Args: {
21404
21451
  p_buffer_id: string;
@@ -21767,6 +21814,12 @@ export declare const startRecalcJob: import("@tanstack/start-client-core").Optio
21767
21814
  verwijderd: number;
21768
21815
  }[];
21769
21816
  };
21817
+ order_departure_proof: {
21818
+ Args: {
21819
+ p_order_id: string;
21820
+ };
21821
+ Returns: import("../integrations/supabase/types").Json;
21822
+ };
21770
21823
  order_number_prefix: {
21771
21824
  Args: {
21772
21825
  p_custom_fields?: import("../integrations/supabase/types").Json;
@@ -38604,6 +38657,45 @@ export declare const tickRecalcJob: import("@tanstack/start-client-core").Option
38604
38657
  };
38605
38658
  Relationships: [];
38606
38659
  };
38660
+ stock_consumption_alerts: {
38661
+ Row: {
38662
+ alert_type: string;
38663
+ created_at: string;
38664
+ id: string;
38665
+ mutation_log_id: string | null;
38666
+ order_id: string | null;
38667
+ order_item_id: string | null;
38668
+ qty: number;
38669
+ reason: string | null;
38670
+ resolved_at: string | null;
38671
+ updated_at: string;
38672
+ };
38673
+ Insert: {
38674
+ alert_type: string;
38675
+ created_at?: string;
38676
+ id?: string;
38677
+ mutation_log_id?: string | null;
38678
+ order_id?: string | null;
38679
+ order_item_id?: string | null;
38680
+ qty?: number;
38681
+ reason?: string | null;
38682
+ resolved_at?: string | null;
38683
+ updated_at?: string;
38684
+ };
38685
+ Update: {
38686
+ alert_type?: string;
38687
+ created_at?: string;
38688
+ id?: string;
38689
+ mutation_log_id?: string | null;
38690
+ order_id?: string | null;
38691
+ order_item_id?: string | null;
38692
+ qty?: number;
38693
+ reason?: string | null;
38694
+ resolved_at?: string | null;
38695
+ updated_at?: string;
38696
+ };
38697
+ Relationships: [];
38698
+ };
38607
38699
  stock_import_runs: {
38608
38700
  Row: {
38609
38701
  connection_id: string | null;
@@ -44276,6 +44368,14 @@ export declare const tickRecalcJob: import("@tanstack/start-client-core").Option
44276
44368
  };
44277
44369
  Returns: import("../integrations/supabase/types").Json;
44278
44370
  };
44371
+ logistics_plan_block: {
44372
+ Args: {
44373
+ p_details?: import("../integrations/supabase/types").Json;
44374
+ p_order_ids: string[];
44375
+ p_reason: string;
44376
+ };
44377
+ Returns: number;
44378
+ };
44279
44379
  logistics_plan_buffer_sql: {
44280
44380
  Args: {
44281
44381
  p_buffer_id: string;
@@ -44644,6 +44744,12 @@ export declare const tickRecalcJob: import("@tanstack/start-client-core").Option
44644
44744
  verwijderd: number;
44645
44745
  }[];
44646
44746
  };
44747
+ order_departure_proof: {
44748
+ Args: {
44749
+ p_order_id: string;
44750
+ };
44751
+ Returns: import("../integrations/supabase/types").Json;
44752
+ };
44647
44753
  order_number_prefix: {
44648
44754
  Args: {
44649
44755
  p_custom_fields?: import("../integrations/supabase/types").Json;