@flowselections/logistiek-engine 1.0.27 → 1.0.29

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 (101) hide show
  1. package/README.md +24 -24
  2. package/dist-lib/components/AfmetingenBronnenCard.d.ts +2 -0
  3. package/dist-lib/components/AfmetingenBronnenCard.d.ts.map +1 -0
  4. package/dist-lib/components/AfmetingenBronnenCard.js +46 -0
  5. package/dist-lib/components/BufferDebugCard.d.ts.map +1 -1
  6. package/dist-lib/components/BufferDebugCard.js +6 -1
  7. package/dist-lib/components/ConsistentieCard.d.ts.map +1 -1
  8. package/dist-lib/components/ConsistentieCard.js +1 -1
  9. package/dist-lib/components/GeblokkeerdeOrdersCard.d.ts +2 -0
  10. package/dist-lib/components/GeblokkeerdeOrdersCard.d.ts.map +1 -0
  11. package/dist-lib/components/GeblokkeerdeOrdersCard.js +44 -0
  12. package/dist-lib/components/KarplanStatusCard.d.ts +2 -0
  13. package/dist-lib/components/KarplanStatusCard.d.ts.map +1 -0
  14. package/dist-lib/components/KarplanStatusCard.js +31 -0
  15. package/dist-lib/components/RegiePage.d.ts.map +1 -1
  16. package/dist-lib/components/RegiePage.js +4 -1
  17. package/dist-lib/components/RuleEnginePage.d.ts.map +1 -1
  18. package/dist-lib/components/RuleEnginePage.js +7 -4
  19. package/dist-lib/components/rule-engine/FloridayConnectionBanner.d.ts +2 -0
  20. package/dist-lib/components/rule-engine/FloridayConnectionBanner.d.ts.map +1 -0
  21. package/dist-lib/components/rule-engine/FloridayConnectionBanner.js +41 -0
  22. package/dist-lib/components/rule-engine/RuleFieldInput.d.ts.map +1 -1
  23. package/dist-lib/components/rule-engine/RuleFieldInput.js +9 -0
  24. package/dist-lib/hooks/useGeblokkeerdeOrders.d.ts +28 -0
  25. package/dist-lib/hooks/useGeblokkeerdeOrders.d.ts.map +1 -0
  26. package/dist-lib/hooks/useGeblokkeerdeOrders.js +94 -0
  27. package/dist-lib/hooks/useRecalcJob.d.ts.map +1 -1
  28. package/dist-lib/hooks/useRecalcJob.js +26 -1
  29. package/dist-lib/hooks/useScannersPrinters.d.ts +11 -0
  30. package/dist-lib/hooks/useScannersPrinters.d.ts.map +1 -0
  31. package/dist-lib/hooks/useScannersPrinters.js +65 -0
  32. package/dist-lib/index.d.ts +3 -0
  33. package/dist-lib/index.d.ts.map +1 -1
  34. package/dist-lib/index.js +3 -0
  35. package/dist-lib/integrations/supabase/auth-middleware.d.ts +2105 -68
  36. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  37. package/dist-lib/integrations/supabase/client.d.ts +20293 -0
  38. package/dist-lib/integrations/supabase/client.d.ts.map +1 -0
  39. package/dist-lib/integrations/supabase/client.js +13 -0
  40. package/dist-lib/integrations/supabase/client.server.d.ts +2105 -68
  41. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  42. package/dist-lib/integrations/supabase/client.server.js +1 -1
  43. package/dist-lib/integrations/supabase/moduleClient.d.ts +2105 -68
  44. package/dist-lib/integrations/supabase/moduleClient.d.ts.map +1 -1
  45. package/dist-lib/integrations/supabase/types.d.ts +2135 -54
  46. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  47. package/dist-lib/integrations/supabase/types.js +1 -0
  48. package/dist-lib/lib/attachAuthToken.d.ts +2 -0
  49. package/dist-lib/lib/attachAuthToken.d.ts.map +1 -0
  50. package/dist-lib/lib/attachAuthToken.js +50 -0
  51. package/dist-lib/lib/bufferMatching.d.ts.map +1 -1
  52. package/dist-lib/lib/bufferMatching.js +1 -0
  53. package/dist-lib/lib/cartPlanRequests.server.d.ts +13 -0
  54. package/dist-lib/lib/cartPlanRequests.server.d.ts.map +1 -0
  55. package/dist-lib/lib/cartPlanRequests.server.js +168 -0
  56. package/dist-lib/lib/controlScan.server.d.ts +12 -0
  57. package/dist-lib/lib/controlScan.server.d.ts.map +1 -0
  58. package/dist-lib/lib/controlScan.server.js +197 -0
  59. package/dist-lib/lib/dimensionData.server.d.ts +11 -1
  60. package/dist-lib/lib/dimensionData.server.d.ts.map +1 -1
  61. package/dist-lib/lib/dimensionData.server.js +58 -48
  62. package/dist-lib/lib/dimensionPlanner.d.ts +26 -1
  63. package/dist-lib/lib/dimensionPlanner.d.ts.map +1 -1
  64. package/dist-lib/lib/dimensionPlanner.js +37 -6
  65. package/dist-lib/lib/floridayDocuments.functions.d.ts +6314 -203
  66. package/dist-lib/lib/floridayDocuments.functions.d.ts.map +1 -1
  67. package/dist-lib/lib/floridayDocuments.functions.js +4 -3
  68. package/dist-lib/lib/floridayPush.functions.d.ts +2106 -69
  69. package/dist-lib/lib/floridayPush.functions.d.ts.map +1 -1
  70. package/dist-lib/lib/floridayPush.functions.js +2 -1
  71. package/dist-lib/lib/handlers/logistics-tick.server.d.ts.map +1 -1
  72. package/dist-lib/lib/handlers/logistics-tick.server.js +64 -0
  73. package/dist-lib/lib/loadCarrierPlanning.functions.d.ts +10480 -295
  74. package/dist-lib/lib/loadCarrierPlanning.functions.d.ts.map +1 -1
  75. package/dist-lib/lib/loadCarrierPlanning.functions.js +6 -5
  76. package/dist-lib/lib/loadCarrierPlanning.server.d.ts +42 -0
  77. package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -1
  78. package/dist-lib/lib/loadCarrierPlanning.server.js +583 -31
  79. package/dist-lib/lib/logisticsPlanner.d.ts.map +1 -1
  80. package/dist-lib/lib/logisticsPlanner.js +24 -9
  81. package/dist-lib/lib/logisticsRecalcJob.functions.d.ts +4231 -157
  82. package/dist-lib/lib/logisticsRecalcJob.functions.d.ts.map +1 -1
  83. package/dist-lib/lib/logisticsRecalcJob.functions.js +3 -2
  84. package/dist-lib/lib/logisticsRecalcJob.server.d.ts.map +1 -1
  85. package/dist-lib/lib/logisticsRecalcJob.server.js +1 -0
  86. package/dist-lib/lib/logisticsRecalculate.functions.d.ts +2106 -69
  87. package/dist-lib/lib/logisticsRecalculate.functions.d.ts.map +1 -1
  88. package/dist-lib/lib/logisticsRecalculate.functions.js +3 -1
  89. package/dist-lib/lib/modulePrompts.d.ts.map +1 -1
  90. package/dist-lib/lib/modulePrompts.js +2560 -1586
  91. package/dist-lib/lib/planQueue.server.d.ts.map +1 -1
  92. package/dist-lib/lib/planQueue.server.js +6 -0
  93. package/dist-lib/lib/readableServerErrors.d.ts +2 -0
  94. package/dist-lib/lib/readableServerErrors.d.ts.map +1 -0
  95. package/dist-lib/lib/readableServerErrors.js +36 -0
  96. package/dist-lib/lib/ruleEngineSchema.d.ts +1 -1
  97. package/dist-lib/lib/ruleEngineSchema.d.ts.map +1 -1
  98. package/dist-lib/lib/ruleEngineSchema.js +72 -51
  99. package/dist-lib/lib/ruleEngineWizard.d.ts.map +1 -1
  100. package/dist-lib/lib/ruleEngineWizard.js +5 -6
  101. package/package.json +1 -1
@@ -182,6 +182,16 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
182
182
  .map((r) => r.product_id)
183
183
  .filter((id) => typeof id === "string" && UUID_RE.test(id));
184
184
  let groupSet = new Set();
185
+ // Groepsregels dragen niet altijd een product_id. Zonder koppeling wordt de
186
+ // groep op naam opgezocht, zodat de leden en hun afmetingen alsnog meetellen.
187
+ const groupByName = new Map();
188
+ {
189
+ const { data: allGroups } = await sb.from("product_groups").select("id, name");
190
+ for (const g of Array.isArray(allGroups) ? allGroups : []) {
191
+ if (g?.name)
192
+ groupByName.set(String(g.name).trim().toLowerCase(), String(g.id));
193
+ }
194
+ }
185
195
  if (groupIds.length > 0) {
186
196
  const { data: groups } = await sb.from("product_groups").select("id").in("id", groupIds);
187
197
  groupSet = new Set((groups ?? []).map((g) => g.id));
@@ -190,8 +200,14 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
190
200
  const packing = row.order_id ? options.packingByOrder?.get(String(row.order_id)) ?? null : null;
191
201
  const { belading: base, source } = resolveBelading(row, packing);
192
202
  const plants = Math.max(0, Number(row.quantity ?? 0));
193
- const isGroup = (row.product_id && groupSet.has(row.product_id)) ||
194
- String(row.product_name ?? "").startsWith("[Groep]");
203
+ const groupNameKey = String(row.product_name ?? "")
204
+ .replace(/^\[Groep\]\s*/i, "")
205
+ .trim()
206
+ .toLowerCase();
207
+ const groupId = row.product_id && groupSet.has(row.product_id)
208
+ ? String(row.product_id)
209
+ : groupByName.get(groupNameKey) ?? null;
210
+ const isGroup = groupId != null || String(row.product_name ?? "").startsWith("[Groep]");
195
211
  if (!isGroup) {
196
212
  options.sources?.set(String(row.id), source);
197
213
  out.push({
@@ -207,11 +223,13 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
207
223
  });
208
224
  continue;
209
225
  }
210
- const { data: members } = await sb
211
- .from("product_group_items")
212
- .select("trade_item_id, quantity, sort_order")
213
- .eq("product_group_id", row.product_id)
214
- .order("sort_order", { ascending: true });
226
+ const { data: members } = groupId
227
+ ? await sb
228
+ .from("product_group_items")
229
+ .select("trade_item_id, quantity, sort_order")
230
+ .eq("product_group_id", groupId)
231
+ .order("sort_order", { ascending: true })
232
+ : { data: [] };
215
233
  const memberList = Array.isArray(members) ? members : [];
216
234
  if (memberList.length === 0) {
217
235
  options.sources?.set(String(row.id), source);
@@ -419,6 +437,73 @@ export function planByFloridayPacking(inputs, profile, packing) {
419
437
  // ---------------------------------------------------------------------------
420
438
  // Indeling op afmetingen omzetten naar het bestaande opslagmodel
421
439
  // ---------------------------------------------------------------------------
440
+ /** Herkomst van de laagindeling, zoals vastgelegd in order_cart_layer_positions. */
441
+ export const LAYER_POSITION_SOURCE = "afmetingen-planken";
442
+ /** cm -> mm; de weergavemodules rekenen in millimeters. */
443
+ const mm = (cm) => Number((cm * 10).toFixed(1));
444
+ /**
445
+ * Zet de laagindeling van één kar om naar rijen voor
446
+ * public.order_cart_layer_positions. (0,0) is linksboven van de laag,
447
+ * x loopt naar rechts, y naar beneden, alles in millimeters.
448
+ */
449
+ export function layerPositionRows(cartId, cart) {
450
+ const rows = [];
451
+ for (const layer of cart.layers) {
452
+ for (const pos of layer.positions) {
453
+ const rawItemId = pos.order_item_id;
454
+ // Samengestelde sleutels van groepsleden bestaan niet als orderregel.
455
+ const orderItemId = rawItemId.includes("::") ? rawItemId.split("::")[0] : rawItemId;
456
+ const isUuid = /^[0-9a-f-]{36}$/i.test(orderItemId);
457
+ rows.push({
458
+ cart_id: cartId,
459
+ layer: layer.layer_index,
460
+ keg_id: pos.keg_id,
461
+ x: mm(pos.x),
462
+ y: mm(pos.y),
463
+ width: mm(pos.width),
464
+ height: mm(pos.height),
465
+ rotation: pos.rotation,
466
+ layer_width: mm(layer.layer_length_cm),
467
+ layer_height: mm(layer.layer_width_cm),
468
+ product_id: pos.product_id,
469
+ order_item_id: isUuid ? orderItemId : null,
470
+ label: pos.label,
471
+ source: LAYER_POSITION_SOURCE,
472
+ algorithm_version: DIMENSION_PLANNER_VERSION,
473
+ });
474
+ }
475
+ }
476
+ return rows;
477
+ }
478
+ function validateDimensionCart(cart) {
479
+ const itemCrates = cart.layers.reduce((sum, layer) => sum + layer.placements.reduce((layerSum, placement) => layerSum + placement.crates, 0), 0);
480
+ const positions = cart.layers.reduce((sum, layer) => sum + layer.positions.length, 0);
481
+ if (itemCrates !== positions) {
482
+ return `Kar ${cart.cart_number} bevat ${itemCrates} fusten, maar heeft ${positions} fysieke posities.`;
483
+ }
484
+ for (const layer of cart.layers) {
485
+ for (let index = 0; index < layer.positions.length; index++) {
486
+ const position = layer.positions[index];
487
+ if (position.x < 0 ||
488
+ position.y < 0 ||
489
+ position.x + position.width > layer.layer_length_cm + 1e-9 ||
490
+ position.y + position.height > layer.layer_width_cm + 1e-9) {
491
+ return `Fust ${position.keg_id} van kar ${cart.cart_number} valt buiten de bruikbare afmetingen.`;
492
+ }
493
+ for (let otherIndex = index + 1; otherIndex < layer.positions.length; otherIndex++) {
494
+ const other = layer.positions[otherIndex];
495
+ const overlaps = position.x < other.x + other.width - 1e-9 &&
496
+ position.x + position.width > other.x + 1e-9 &&
497
+ position.y < other.y + other.height - 1e-9 &&
498
+ position.y + position.height > other.y + 1e-9;
499
+ if (overlaps) {
500
+ return `Fusten ${position.keg_id} en ${other.keg_id} overlappen op kar ${cart.cart_number}, laag ${layer.layer_index}.`;
501
+ }
502
+ }
503
+ }
504
+ }
505
+ return null;
506
+ }
422
507
  export function dimensionCartsToPlannedUnits(carts) {
423
508
  return carts.map((cart) => {
424
509
  const layerCount = cart.layers.length || 1;
@@ -465,15 +550,131 @@ export function dimensionCartsToPlannedUnits(carts) {
465
550
  // ---------------------------------------------------------------------------
466
551
  /** Maximaal aantal ladingdragers dat één orderbuffer mag opleveren. */
467
552
  const MAX_CARTS_PER_BUFFER = 500;
553
+ /**
554
+ * Bepaalt de definitieve, persistente identiteit van iedere fysieke kar.
555
+ * Bij een bufferorder met meerdere orders zijn alle karren gedeelde
556
+ * bufferkarren: geen eigen order, doorlopend genummerd binnen de buffer.
557
+ * Bij één order per buffer krijgt die order exact 1..N.
558
+ */
559
+ export function assignPersistentCartIdentities(carts, itemToOrder, identityOptions = {}) {
560
+ if (identityOptions.shared) {
561
+ const start = Math.max(1, identityOptions.startNumber ?? 1);
562
+ const total = (identityOptions.existingCount ?? 0) + carts.length;
563
+ return carts.map((_, index) => ({
564
+ ownerOrderId: null,
565
+ cartNumber: start + index,
566
+ totalCarts: total,
567
+ }));
568
+ }
569
+ const owners = carts.map((cart) => {
570
+ const orderIds = new Set();
571
+ for (const item of cart.items) {
572
+ const orderId = item.order_id ?? itemToOrder.get(item.order_item_id);
573
+ if (orderId)
574
+ orderIds.add(orderId);
575
+ }
576
+ return orderIds.size === 1 ? [...orderIds][0] : null;
577
+ });
578
+ const totals = new Map();
579
+ for (const owner of owners) {
580
+ if (owner)
581
+ totals.set(owner, (totals.get(owner) ?? 0) + 1);
582
+ }
583
+ const sequences = new Map();
584
+ let sharedSequence = 0;
585
+ return owners.map((ownerOrderId) => {
586
+ if (!ownerOrderId) {
587
+ sharedSequence += 1;
588
+ return { ownerOrderId: null, cartNumber: -sharedSequence, totalCarts: null };
589
+ }
590
+ const cartNumber = (sequences.get(ownerOrderId) ?? 0) + 1;
591
+ sequences.set(ownerOrderId, cartNumber);
592
+ return { ownerOrderId, cartNumber, totalCarts: totals.get(ownerOrderId) ?? null };
593
+ });
594
+ }
595
+ function assertPersistentCartIdentities(identities, orderIds) {
596
+ for (const orderId of orderIds) {
597
+ const own = identities.filter((identity) => identity.ownerOrderId === orderId);
598
+ if (own.length === 0)
599
+ continue;
600
+ const numbers = own.map((identity) => identity.cartNumber).sort((a, b) => a - b);
601
+ const valid = numbers.every((number, index) => number === index + 1) &&
602
+ own.every((identity) => identity.totalCarts === own.length);
603
+ if (!valid) {
604
+ throw new Error(`Karrenindeling ongeldig voor order ${orderId}: verwacht vaste karnummers 1 t/m ${own.length}.`);
605
+ }
606
+ }
607
+ }
608
+ /**
609
+ * Leden van een buffer die nog helemaal geen fysieke kar hebben: geen eigen kar
610
+ * (order_carts.order_id) én geen gedeelde bufferkar waar een van hun orderregels
611
+ * op ligt. Zonder kar krijgt zo'n order ook geen pickdocument, dus dit mag nooit
612
+ * blijven staan.
613
+ */
614
+ export async function bufferOrdersWithoutCarts(sb, bufferId, orderIds) {
615
+ const ids = orderIds.map(String).filter(Boolean);
616
+ if (ids.length === 0)
617
+ return [];
618
+ const { data: carts, error: cartsError } = await sb
619
+ .from("order_carts")
620
+ .select("id, order_id")
621
+ .or(`buffer_id.eq.${bufferId},order_id.in.(${ids.join(",")})`);
622
+ if (cartsError) {
623
+ throw new Error(`Karren van de buffer konden niet worden gelezen: ${cartsError.message}`);
624
+ }
625
+ const cartRows = Array.isArray(carts) ? carts : [];
626
+ const withCarts = new Set(cartRows.map((c) => (c.order_id ? String(c.order_id) : "")).filter(Boolean));
627
+ // Gedeelde karren (zonder eigen order) tellen mee voor elk lid waarvan er een
628
+ // orderregel op ligt.
629
+ const sharedIds = cartRows.filter((c) => !c.order_id).map((c) => String(c.id));
630
+ if (sharedIds.length > 0) {
631
+ const { data: items, error: itemsError } = await sb
632
+ .from("order_cart_items")
633
+ .select("order_item_id, order_items(order_id)")
634
+ .in("cart_id", sharedIds);
635
+ if (itemsError) {
636
+ throw new Error(`Inhoud van gedeelde karren kon niet worden gelezen: ${itemsError.message}`);
637
+ }
638
+ for (const row of Array.isArray(items) ? items : []) {
639
+ const orderId = row?.order_items?.order_id;
640
+ if (orderId)
641
+ withCarts.add(String(orderId));
642
+ }
643
+ }
644
+ return ids.filter((id) => !withCarts.has(id));
645
+ }
468
646
  export async function replanBufferInternal(sb, bufferId, options = {}) {
469
647
  const warnings = [];
648
+ // Bevroren karren blijven staan; al het overige van de héle buffer wordt
649
+ // samen ingedeeld, zodat orders van de buffer op dezelfde karren komen.
650
+ const keepCartIds = (options.keepCartIds ?? []).map(String).filter(Boolean);
651
+ const isSupplement = keepCartIds.length > 0;
470
652
  const { data: orders } = await sb
471
653
  .from("orders")
472
- .select("id, customer_id, custom_fields, plan_locked_at")
654
+ .select("id, order_number, customer_id, custom_fields, plan_locked_at")
473
655
  .eq("buffer_id", bufferId)
474
656
  .is("deleted_at", null);
475
- const orderRows = Array.isArray(orders) ? orders : [];
657
+ const allOrderRows = Array.isArray(orders) ? orders : [];
658
+ const orderRows = allOrderRows;
476
659
  const orderIds = orderRows.map((o) => o.id);
660
+ // Binnen een bufferorder mogen orders en klanten altijd samen op een kar.
661
+ const isBufferOrder = orderRows.length > 1;
662
+ const orderRefs = orderRows.map((o) => String(o.order_number ?? "")).filter(Boolean);
663
+ const { data: releasedTasks } = orderRefs.length > 0
664
+ ? await sb
665
+ .from("logistics_tasks")
666
+ .select("order_ref")
667
+ .eq("task_type", "pick.execute")
668
+ .eq("shadow", false)
669
+ .not("status", "in", "(done,cancelled,superseded,skipped)")
670
+ .lte("planned_start", new Date().toISOString())
671
+ .in("order_ref", orderRefs)
672
+ : { data: [] };
673
+ // Alleen een expliciet slot op de order blokkeert een herplanning hard.
674
+ // Vrijgave alleen is niet genoeg: zolang er nog niets is geprint, gescand of
675
+ // gecontroleerd mag een gewijzigde belading nog doorwerken. De bevriezing van
676
+ // karidentiteiten hieronder bewaakt de fysieke werkelijkheid.
677
+ void releasedTasks;
477
678
  if (orderRows.some((o) => o.plan_locked_at)) {
478
679
  return {
479
680
  ok: true,
@@ -486,6 +687,66 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
486
687
  skipped: "locked",
487
688
  };
488
689
  }
690
+ // Een eenmaal geprinte, gescande of gecontroleerde fysieke kar heeft een
691
+ // blijvende identiteit. Ook vóór de formele vrijgavetijd mag een replan die
692
+ // id, het nummer of total_carts daarom niet meer vervangen.
693
+ if (!isSupplement) {
694
+ const { data: existingCartRows, error: existingCartError } = await sb
695
+ .from("order_carts")
696
+ .select("id, order_id, cart_number, total_carts")
697
+ .or(`buffer_id.eq.${bufferId},order_id.in.(${orderIds.join(",")})`);
698
+ if (existingCartError) {
699
+ throw new Error(`Bestaande karidentiteiten konden niet worden gecontroleerd: ${existingCartError.message}`);
700
+ }
701
+ const existingCartIds = (Array.isArray(existingCartRows) ? existingCartRows : [])
702
+ .map((cart) => String(cart.id ?? ""))
703
+ .filter(Boolean);
704
+ if (existingCartIds.length > 0) {
705
+ const [controlResult, barcodeResult, printResult] = await Promise.all([
706
+ sb.from("cart_control_items").select("cart_number_id").in("cart_number_id", existingCartIds),
707
+ sb.from("pakbon_barcode_inhoud").select("cart_id").in("cart_id", existingCartIds),
708
+ sb.from("print_jobs").select("id, payload").in("payload->>cart_id", existingCartIds),
709
+ ]);
710
+ const lockCheckError = controlResult.error ?? barcodeResult.error ?? printResult.error;
711
+ if (lockCheckError) {
712
+ throw new Error(`Karvergrendeling kon niet veilig worden gecontroleerd: ${lockCheckError.message}`);
713
+ }
714
+ const frozen = new Set();
715
+ for (const r of controlResult.data ?? []) {
716
+ const id = r?.cart_number_id;
717
+ if (id)
718
+ frozen.add(String(id));
719
+ }
720
+ for (const r of barcodeResult.data ?? []) {
721
+ const id = r?.cart_id;
722
+ if (id)
723
+ frozen.add(String(id));
724
+ }
725
+ for (const r of printResult.data ?? []) {
726
+ const id = r?.payload?.cart_id;
727
+ if (id)
728
+ frozen.add(String(id));
729
+ }
730
+ const frozenIds = existingCartIds.filter((id) => frozen.has(id));
731
+ if (frozenIds.length > 0) {
732
+ // Geprinte, gescande of gecontroleerde karren blijven exact zoals ze zijn.
733
+ // Al het overige van de buffer wordt in één ronde samen ingedeeld, zodat
734
+ // de resterende fusten van alle orders op dezelfde karren komen.
735
+ const supplement = await replanBufferInternal(sb, bufferId, {
736
+ ...options,
737
+ keepCartIds: frozenIds,
738
+ });
739
+ return {
740
+ ...supplement,
741
+ warnings: [
742
+ ...(supplement.warnings ?? []),
743
+ `${frozenIds.length} reeds geprinte of gecontroleerde kar(ren) zijn ongewijzigd gebleven; ` +
744
+ "de overige fusten van de buffer zijn samen opnieuw ingedeeld.",
745
+ ],
746
+ };
747
+ }
748
+ }
749
+ }
489
750
  const idList = orderIds.length > 0 ? orderIds : ["00000000-0000-0000-0000-000000000000"];
490
751
  const { data: items } = await sb
491
752
  .from("order_items")
@@ -505,7 +766,63 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
505
766
  packingByOrder.set(String(o.id), belading);
506
767
  }
507
768
  const sources = new Map();
508
- const inputs = await expandOrderItemsForPlanner(sb, rawRows, { packingByOrder, sources });
769
+ let inputs = await expandOrderItemsForPlanner(sb, rawRows, { packingByOrder, sources });
770
+ // Wat al op een bevroren kar ligt, wordt niet opnieuw ingedeeld.
771
+ let frozenCartCount = 0;
772
+ let cartNumberStart = 1;
773
+ if (isSupplement) {
774
+ const { data: keptCarts, error: keptError } = await sb
775
+ .from("order_carts")
776
+ .select("id, cart_number")
777
+ .in("id", keepCartIds);
778
+ if (keptError) {
779
+ throw new Error(`Bevroren karren konden niet worden gelezen: ${keptError.message}`);
780
+ }
781
+ const keptRows = Array.isArray(keptCarts) ? keptCarts : [];
782
+ frozenCartCount = keptRows.length;
783
+ cartNumberStart =
784
+ keptRows.reduce((max, c) => Math.max(max, Number(c.cart_number) || 0), 0) + 1;
785
+ const { data: frozenItems, error: frozenItemsError } = await sb
786
+ .from("order_cart_items")
787
+ .select("order_item_id, product_id, crates, plants_per_crate")
788
+ .in("cart_id", keepCartIds);
789
+ if (frozenItemsError) {
790
+ throw new Error(`Inhoud van bevroren karren kon niet worden gelezen: ${frozenItemsError.message}`);
791
+ }
792
+ const doneKey = (itemId, productId) => `${String(itemId ?? "")}|${String(productId ?? "")}`;
793
+ const done = new Map();
794
+ for (const r of Array.isArray(frozenItems) ? frozenItems : []) {
795
+ const plants = Math.max(0, Math.round(Number(r.crates ?? 0))) *
796
+ Math.max(1, Math.round(Number(r.plants_per_crate ?? 1)));
797
+ const key = doneKey(r.order_item_id, r.product_id);
798
+ done.set(key, (done.get(key) ?? 0) + plants);
799
+ }
800
+ for (const i of inputs) {
801
+ const baseId = i.order_item_id.includes("::") ? i.order_item_id.split("::")[0] : i.order_item_id;
802
+ const key = doneKey(baseId, i.product_id);
803
+ const already = done.get(key) ?? 0;
804
+ if (already <= 0)
805
+ continue;
806
+ const used = Math.min(already, i.plants);
807
+ i.plants -= used;
808
+ done.set(key, already - used);
809
+ }
810
+ inputs = inputs.filter((i) => i.plants > 0);
811
+ if (inputs.length === 0) {
812
+ return {
813
+ ok: true,
814
+ bufferId,
815
+ orderIds,
816
+ cartsCreated: 0,
817
+ replacedManualCount: 0,
818
+ usedFloridayPacking: false,
819
+ warnings: [
820
+ "Alle fusten van deze buffer liggen al op geprinte of gecontroleerde karren; niets opnieuw ingedeeld.",
821
+ ],
822
+ skipped: "locked",
823
+ };
824
+ }
825
+ }
509
826
  const itemToOrder = new Map();
510
827
  for (const i of inputs)
511
828
  if (i.order_id)
@@ -543,6 +860,8 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
543
860
  p_order_ids: orderIds,
544
861
  p_reason: reason,
545
862
  p_details: {
863
+ // Stabiele sleutel: nooit afgeleid uit ordernummers of aantallen.
864
+ reason_code: "belading_onbekend",
546
865
  bufferId,
547
866
  path: "typescript",
548
867
  carrier: guardProfile.code,
@@ -566,6 +885,9 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
566
885
  };
567
886
  }
568
887
  let plannedCarts = [];
888
+ // Laagindeling van de afmetingenmethode, per cart_number. Alleen gevuld als
889
+ // er daadwerkelijk op afmetingen is ingedeeld.
890
+ let dimensionPlan = null;
569
891
  let usedFloridayPacking = false;
570
892
  // Overname-pad: één order in de buffer én een volledige Floriday-verpakking.
571
893
  // Dan neemt de engine de verhouding van Floriday over en optimaliseert niet.
@@ -580,7 +902,8 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
580
902
  // Globale methode: indelen op basis van fysieke afmetingen.
581
903
  const carrierRow = profiles.find((p) => p.code === (options.carrierType ?? defaultProfile.code)) ?? null;
582
904
  const { carrier, missing: carrierMissing } = carrierDimensions(carrierRow);
583
- const { items: dimItems, missing: itemMissing } = await buildDimensionItems(sb, inputs);
905
+ const { items: dimItems, missing: itemMissing, derived: derivedHeights, } = await buildDimensionItems(sb, inputs, carrier, carrierRow?.layers_per_carrier ?? null);
906
+ warnings.push(...derivedHeights);
584
907
  const missing = [...carrierMissing, ...itemMissing];
585
908
  let planError = null;
586
909
  if (missing.length === 0 && carrier) {
@@ -588,8 +911,14 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
588
911
  if (result.error)
589
912
  planError = result.error;
590
913
  else {
591
- plannedCarts = dimensionCartsToPlannedUnits(result.carts);
592
- warnings.push(...result.warnings);
914
+ const invalidCart = result.carts.map(validateDimensionCart).find((message) => message != null) ?? null;
915
+ if (invalidCart)
916
+ planError = invalidCart;
917
+ else {
918
+ plannedCarts = dimensionCartsToPlannedUnits(result.carts);
919
+ dimensionPlan = new Map(result.carts.map((c) => [c.cart_number, c]));
920
+ warnings.push(...result.warnings);
921
+ }
593
922
  }
594
923
  }
595
924
  if (missing.length > 0 || planError) {
@@ -613,6 +942,7 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
613
942
  p_order_ids: orderIds,
614
943
  p_reason: reason,
615
944
  p_details: {
945
+ reason_code: "afmetingen_onbekend",
616
946
  bufferId,
617
947
  path: "afmetingen",
618
948
  carrier: carrierRow?.code ?? null,
@@ -621,6 +951,19 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
621
951
  });
622
952
  if (blockError)
623
953
  warnings.push(`Blokkade kon niet worden vastgelegd: ${blockError.message}`);
954
+ // Karren die met een andere (oudere) werkwijze zijn gemaakt mogen niet
955
+ // blijven staan: dan lijkt er een geldige indeling te zijn terwijl de
956
+ // afmetingen ontbreken. Karren van de afmetingenmethode blijven staan.
957
+ // Bij aanvullen blijven bestaande (bevroren) karren altijd staan.
958
+ if (!isSupplement) {
959
+ const { error: staleError } = await sb
960
+ .from("order_carts")
961
+ .delete()
962
+ .eq("buffer_id", bufferId)
963
+ .or("layout_method.is.null,layout_method.neq.afmetingen");
964
+ if (staleError)
965
+ warnings.push(`Oude karrenindeling kon niet worden opgeruimd: ${staleError.message}`);
966
+ }
624
967
  return {
625
968
  ok: false,
626
969
  bufferId,
@@ -660,8 +1003,10 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
660
1003
  // Zonder optimale vulling wordt er niet bijgemengd in een laag.
661
1004
  maxBeladingenPerLayer: rules && !rules.optimalFill ? 1 : (settings?.max_beladingen_per_layer ?? 2),
662
1005
  neverMixProductIds: neverMix,
663
- maxOrders: rules?.maxOrders ?? null,
664
- maxCustomers: rules?.maxCustomers ?? settings?.max_customers_per_cart ?? null,
1006
+ // Binnen een bufferorder mag alles samen op een kar: de grenzen voor
1007
+ // aantal orders en klanten gelden dan niet.
1008
+ maxOrders: isBufferOrder ? null : rules?.maxOrders ?? null,
1009
+ maxCustomers: isBufferOrder ? null : rules?.maxCustomers ?? settings?.max_customers_per_cart ?? null,
665
1010
  oneCustomerPerLayer: Boolean(settings?.max_one_customer_per_layer),
666
1011
  ownCarrierFromLayers: settings?.own_cart_from_layers ?? null,
667
1012
  sortOrder: "none",
@@ -684,6 +1029,7 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
684
1029
  p_order_ids: orderIds,
685
1030
  p_reason: reason,
686
1031
  p_details: {
1032
+ reason_code: "kargrens_overschreden",
687
1033
  bufferId,
688
1034
  path: "typescript",
689
1035
  carts: plannedCarts.length,
@@ -719,24 +1065,98 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
719
1065
  carts: plannedCarts,
720
1066
  };
721
1067
  }
722
- await sb.from("order_carts").delete().eq("buffer_id", bufferId);
1068
+ const persistentIdentities = assignPersistentCartIdentities(plannedCarts, itemToOrder, {
1069
+ shared: isBufferOrder || isSupplement,
1070
+ startNumber: cartNumberStart,
1071
+ existingCount: frozenCartCount,
1072
+ });
1073
+ assertPersistentCartIdentities(persistentIdentities, orderIds);
1074
+ // Houd de fysieke afmetingenindeling aan dezelfde definitieve karidentiteit.
1075
+ const dimensionCartsByPlanIndex = dimensionPlan
1076
+ ? plannedCarts.map((cart) => dimensionPlan?.get(cart.cart_number) ?? null)
1077
+ : null;
1078
+ if (dimensionPlan) {
1079
+ const normalizedDimensionPlan = new Map();
1080
+ plannedCarts.forEach((cart, index) => {
1081
+ const dimensionCart = dimensionCartsByPlanIndex?.[index] ?? null;
1082
+ const identity = persistentIdentities[index];
1083
+ if (!dimensionCart || !identity)
1084
+ return;
1085
+ cart.cart_number = identity.cartNumber;
1086
+ dimensionCart.cart_number = identity.cartNumber;
1087
+ normalizedDimensionPlan.set(index, dimensionCart);
1088
+ });
1089
+ dimensionPlan = normalizedDimensionPlan;
1090
+ }
1091
+ else {
1092
+ plannedCarts.forEach((cart, index) => {
1093
+ const identity = persistentIdentities[index];
1094
+ if (identity)
1095
+ cart.cart_number = identity.cartNumber;
1096
+ });
1097
+ }
1098
+ if (plannedCarts.length === 0 && orderIds.length > 0) {
1099
+ const reason = `Karrenindeling ongeldig voor order(s) ${orderRefs.join(", ")}: ` +
1100
+ "een vrij te geven order moet minimaal één fysieke kar hebben.";
1101
+ const { error: blockError } = await sb.rpc("logistics_plan_block", {
1102
+ p_order_ids: orderIds,
1103
+ p_reason: reason,
1104
+ p_details: {
1105
+ reason_code: "geen_karren",
1106
+ bufferId,
1107
+ path: useDimensions ? "afmetingen" : "belading",
1108
+ carts: 0,
1109
+ },
1110
+ });
1111
+ if (blockError)
1112
+ warnings.push(`Blokkade kon niet worden vastgelegd: ${blockError.message}`);
1113
+ return {
1114
+ ok: false,
1115
+ bufferId,
1116
+ orderIds,
1117
+ cartsCreated: 0,
1118
+ replacedManualCount,
1119
+ usedFloridayPacking,
1120
+ warnings: [...warnings, reason],
1121
+ skipped: "blocked",
1122
+ blockedReason: reason,
1123
+ beladingSources,
1124
+ };
1125
+ }
1126
+ // Opruimen op buffer EN op order. Verhuist een order naar een andere buffer,
1127
+ // dan blijven de karren van de oude buffer anders als weeskar staan en telt
1128
+ // Controle karren ze dubbel. Bevroren karren (geprint, gescand, gecontroleerd)
1129
+ // blijven altijd staan.
1130
+ const keepFilter = keepCartIds.length > 0 ? `(${keepCartIds.join(",")})` : null;
1131
+ const clearExistingCarts = async () => {
1132
+ const ids = orderIds.filter(Boolean);
1133
+ const del = () => {
1134
+ const q = sb.from("order_carts").delete();
1135
+ return keepFilter ? q.not("id", "in", keepFilter) : q;
1136
+ };
1137
+ await del().eq("buffer_id", bufferId);
1138
+ if (ids.length > 0) {
1139
+ await del().in("order_id", ids);
1140
+ }
1141
+ };
1142
+ await clearExistingCarts();
723
1143
  // In blokken wegschrijven: per ladingdrager een aparte insert is bij grote
724
1144
  // orders veel te traag.
725
1145
  let created = 0;
726
1146
  const CHUNK = 100;
727
1147
  for (let start = 0; start < plannedCarts.length; start += CHUNK) {
728
1148
  const chunk = plannedCarts.slice(start, start + CHUNK);
729
- const cartRows = chunk.map((cart) => {
730
- const orderIdsOnCart = new Set();
731
- for (const i of cart.items) {
732
- const oid = i.order_id ?? itemToOrder.get(i.order_item_id);
733
- if (oid)
734
- orderIdsOnCart.add(oid);
1149
+ const cartRows = chunk.map((cart, chunkIndex) => {
1150
+ const cartIndex = start + chunkIndex;
1151
+ const identity = persistentIdentities[cartIndex];
1152
+ if (!identity) {
1153
+ throw new Error(`Karrenindeling ongeldig: identiteit ontbreekt voor kar ${cartIndex + 1}.`);
735
1154
  }
736
1155
  return {
737
- order_id: orderIdsOnCart.size === 1 ? [...orderIdsOnCart][0] : null,
1156
+ order_id: identity.ownerOrderId,
738
1157
  buffer_id: bufferId,
739
- cart_number: cart.cart_number,
1158
+ cart_number: identity.cartNumber,
1159
+ total_carts: identity.totalCarts,
740
1160
  is_auto: true,
741
1161
  is_complete: cart.is_complete,
742
1162
  source: usedFloridayPacking ? "floriday" : "auto",
@@ -753,18 +1173,23 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
753
1173
  const { data: insertedCarts, error } = await sb
754
1174
  .from("order_carts")
755
1175
  .insert(cartRows)
756
- .select("id, cart_number");
1176
+ .select("id, order_id, cart_number");
757
1177
  if (error || !Array.isArray(insertedCarts)) {
758
1178
  const message = `Ladingdragers konden niet worden opgeslagen: ${error?.message ?? "onbekende fout"}`;
759
1179
  console.error(`[logistiek-engine] buffer ${bufferId}: ${message}`);
760
- await sb.from("order_carts").delete().eq("buffer_id", bufferId);
1180
+ await clearExistingCarts();
761
1181
  throw new Error(message);
762
1182
  }
763
1183
  created += insertedCarts.length;
764
- const idByNumber = new Map(insertedCarts.map((c) => [Number(c.cart_number), String(c.id)]));
1184
+ const identityKey = (orderId, cartNumber) => `${orderId == null ? "shared" : String(orderId)}:${Number(cartNumber)}`;
1185
+ const idByIdentity = new Map(insertedCarts.map((c) => [identityKey(c.order_id, c.cart_number), String(c.id)]));
765
1186
  const itemRows = [];
766
- for (const cart of chunk) {
767
- const cartId = idByNumber.get(cart.cart_number);
1187
+ for (let chunkIndex = 0; chunkIndex < chunk.length; chunkIndex++) {
1188
+ const cart = chunk[chunkIndex];
1189
+ const identity = persistentIdentities[start + chunkIndex];
1190
+ const cartId = identity
1191
+ ? idByIdentity.get(identityKey(identity.ownerOrderId, identity.cartNumber))
1192
+ : undefined;
768
1193
  if (!cartId)
769
1194
  continue;
770
1195
  for (const i of cart.items) {
@@ -774,7 +1199,7 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
774
1199
  order_item_id: i.order_item_id.includes("::") ? i.order_item_id.split("::")[0] : i.order_item_id,
775
1200
  product_id: i.product_id,
776
1201
  product_name: i.product_name,
777
- crates: Math.max(1, Math.round(i.crates)),
1202
+ crates: Math.round(i.crates),
778
1203
  boxes: i.boxes ?? null,
779
1204
  weight_kg: i.weight_kg ?? null,
780
1205
  plants_per_crate: i.plants_per_crate,
@@ -799,10 +1224,137 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
799
1224
  (itemError.details ? ` — ${itemError.details}` : "") +
800
1225
  (itemError.hint ? ` (${itemError.hint})` : "");
801
1226
  console.error(`[logistiek-engine] buffer ${bufferId}: ${message}`);
802
- await sb.from("order_carts").delete().eq("buffer_id", bufferId);
1227
+ await clearExistingCarts();
803
1228
  throw new Error(message);
804
1229
  }
805
1230
  }
1231
+ // Fysieke laagindeling per kar. Alleen de afmetingenmethode levert posities;
1232
+ // bij belading blijft de indeling leeg ("nog geen laagindeling aangeleverd").
1233
+ if (dimensionPlan) {
1234
+ for (let chunkIndex = 0; chunkIndex < chunk.length; chunkIndex++) {
1235
+ const cart = chunk[chunkIndex];
1236
+ const identity = persistentIdentities[start + chunkIndex];
1237
+ const cartId = identity
1238
+ ? idByIdentity.get(identityKey(identity.ownerOrderId, identity.cartNumber))
1239
+ : undefined;
1240
+ const dimCart = dimensionPlan.get(start + chunkIndex);
1241
+ if (!cartId || !dimCart)
1242
+ continue;
1243
+ const rows = layerPositionRows(cartId, dimCart);
1244
+ // Atomair vervangen: eerst de vorige indeling van deze kar weg.
1245
+ await sb.from("order_cart_layer_positions").delete().eq("cart_id", cartId);
1246
+ for (let s = 0; s < rows.length; s += 500) {
1247
+ const slice = rows.slice(s, s + 500);
1248
+ if (slice.length === 0)
1249
+ continue;
1250
+ const { error: posError } = await sb
1251
+ .from("order_cart_layer_positions")
1252
+ .upsert(slice, { onConflict: "cart_id,layer,keg_id" });
1253
+ if (posError) {
1254
+ // Een halve indeling is misleidend: de volledige buffer verwijderen
1255
+ // en de wachtrij laten falen, zodat niets als geldig wordt getoond.
1256
+ await clearExistingCarts();
1257
+ throw new Error(`De fysieke laagindeling van kar ${cart.cart_number} kon niet worden opgeslagen: ${posError.message}`);
1258
+ }
1259
+ }
1260
+ const expectedPositions = rows.length;
1261
+ const { count: storedPositions, error: countError } = await sb
1262
+ .from("order_cart_layer_positions")
1263
+ .select("id", { count: "exact", head: true })
1264
+ .eq("cart_id", cartId);
1265
+ if (countError || storedPositions !== expectedPositions) {
1266
+ await clearExistingCarts();
1267
+ throw new Error(`De fysieke indeling van kar ${cart.cart_number} is niet volledig opgeslagen: ` +
1268
+ `${expectedPositions} fusten verwacht, ${storedPositions ?? 0} opgeslagen.`);
1269
+ }
1270
+ }
1271
+ }
1272
+ }
1273
+ // Sluitcontrole: elke geschreven kar moet de actieve methode én het bijbehorende
1274
+ // versienummer dragen. Zo kan een oudere rekenwijze (bijvoorbeeld een nog niet
1275
+ // bijgewerkte publicatie) nooit stilzwijgend een indeling achterlaten.
1276
+ {
1277
+ const expectedMethod = useDimensions ? "afmetingen" : "belading";
1278
+ const expectedVersion = useDimensions ? DIMENSION_PLANNER_VERSION : LOAD_CARRIER_PLANNER_VERSION;
1279
+ const verifyQuery = sb
1280
+ .from("order_carts")
1281
+ .select("cart_number, layout_method, algorithm_version")
1282
+ .eq("buffer_id", bufferId);
1283
+ // Bevroren karren zijn met een eerdere ronde geschreven en blijven buiten
1284
+ // deze controle: hun identiteit mag nooit meer wijzigen.
1285
+ const { data: writtenCarts, error: verifyError } = keepFilter
1286
+ ? await verifyQuery.not("id", "in", keepFilter)
1287
+ : await verifyQuery;
1288
+ const rows = Array.isArray(writtenCarts) ? writtenCarts : [];
1289
+ const wrong = rows.filter((c) => String(c.layout_method ?? "") !== expectedMethod ||
1290
+ Number(c.algorithm_version) !== expectedVersion);
1291
+ if (verifyError || wrong.length > 0) {
1292
+ const reason = verifyError
1293
+ ? `Karrenindeling kon niet worden gecontroleerd: ${verifyError.message}`
1294
+ : `Karrenindeling is niet volgens de ingestelde werkwijze "${expectedMethod}" vastgelegd ` +
1295
+ `(${wrong.length} van ${rows.length} karren). Er is niets ingedeeld.`;
1296
+ console.error(`[logistiek-engine] buffer ${bufferId}: ${reason}`);
1297
+ await clearExistingCarts();
1298
+ await sb.rpc("logistics_plan_block", {
1299
+ p_order_ids: orderIds,
1300
+ p_reason: reason,
1301
+ p_details: {
1302
+ reason_code: "verificatie_mislukt",
1303
+ bufferId,
1304
+ path: "verificatie",
1305
+ expectedMethod,
1306
+ expectedVersion,
1307
+ },
1308
+ });
1309
+ return {
1310
+ ok: false,
1311
+ bufferId,
1312
+ orderIds,
1313
+ cartsCreated: 0,
1314
+ replacedManualCount,
1315
+ usedFloridayPacking,
1316
+ warnings: [...warnings, reason],
1317
+ skipped: "blocked",
1318
+ blockedReason: reason,
1319
+ beladingSources,
1320
+ };
1321
+ }
1322
+ }
1323
+ // Sluitcontrole op de buffer: elk lid moet na afloop een fysieke kar hebben,
1324
+ // eigen kar of een gedeelde bufferkar met een van zijn orderregels. Zonder kar
1325
+ // krijgt dat lid geen pickdocument, en dat is nooit een geldige uitkomst.
1326
+ {
1327
+ const membersToCheck = allOrderRows.map((o) => String(o.id));
1328
+ const stillMissing = await bufferOrdersWithoutCarts(sb, bufferId, membersToCheck);
1329
+ if (stillMissing.length > 0) {
1330
+ const numberById = new Map(allOrderRows.map((o) => [String(o.id), String(o.order_number ?? o.id)]));
1331
+ const refs = stillMissing.map((id) => numberById.get(id) ?? id);
1332
+ const reason = `Bufferindeling onvolledig: order(s) ${refs.join(", ")} hebben geen enkele kar ` +
1333
+ `en zouden dus geen pickdocument krijgen.`;
1334
+ console.error(`[logistiek-engine] buffer ${bufferId}: ${reason}`);
1335
+ await sb.rpc("logistics_plan_block", {
1336
+ p_order_ids: stillMissing,
1337
+ p_reason: reason,
1338
+ p_details: {
1339
+ reason_code: "bufferdekking_onvolledig",
1340
+ bufferId,
1341
+ path: "bufferdekking",
1342
+ missing: refs,
1343
+ },
1344
+ });
1345
+ return {
1346
+ ok: false,
1347
+ bufferId,
1348
+ orderIds,
1349
+ cartsCreated: created,
1350
+ replacedManualCount,
1351
+ usedFloridayPacking,
1352
+ warnings: [...warnings, reason],
1353
+ skipped: "blocked",
1354
+ blockedReason: reason,
1355
+ beladingSources,
1356
+ };
1357
+ }
806
1358
  }
807
1359
  return {
808
1360
  ok: true,