@flowselections/logistiek-engine 1.0.27 → 1.0.28
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.
- package/README.md +24 -24
- package/dist-lib/components/AfmetingenBronnenCard.d.ts +2 -0
- package/dist-lib/components/AfmetingenBronnenCard.d.ts.map +1 -0
- package/dist-lib/components/AfmetingenBronnenCard.js +46 -0
- package/dist-lib/components/BufferDebugCard.d.ts.map +1 -1
- package/dist-lib/components/BufferDebugCard.js +6 -1
- package/dist-lib/components/ConsistentieCard.d.ts.map +1 -1
- package/dist-lib/components/ConsistentieCard.js +1 -1
- package/dist-lib/components/KarplanStatusCard.d.ts +2 -0
- package/dist-lib/components/KarplanStatusCard.d.ts.map +1 -0
- package/dist-lib/components/KarplanStatusCard.js +31 -0
- package/dist-lib/components/RegiePage.d.ts.map +1 -1
- package/dist-lib/components/RegiePage.js +3 -1
- package/dist-lib/components/RuleEnginePage.d.ts.map +1 -1
- package/dist-lib/components/RuleEnginePage.js +7 -4
- package/dist-lib/components/rule-engine/FloridayConnectionBanner.d.ts +2 -0
- package/dist-lib/components/rule-engine/FloridayConnectionBanner.d.ts.map +1 -0
- package/dist-lib/components/rule-engine/FloridayConnectionBanner.js +41 -0
- package/dist-lib/components/rule-engine/RuleFieldInput.d.ts.map +1 -1
- package/dist-lib/components/rule-engine/RuleFieldInput.js +9 -0
- package/dist-lib/hooks/useRecalcJob.d.ts.map +1 -1
- package/dist-lib/hooks/useRecalcJob.js +26 -1
- package/dist-lib/hooks/useScannersPrinters.d.ts +11 -0
- package/dist-lib/hooks/useScannersPrinters.d.ts.map +1 -0
- package/dist-lib/hooks/useScannersPrinters.js +65 -0
- package/dist-lib/index.d.ts +2 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +2 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +2063 -78
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +20293 -0
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -0
- package/dist-lib/integrations/supabase/client.js +13 -0
- package/dist-lib/integrations/supabase/client.server.d.ts +2063 -78
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.js +1 -1
- package/dist-lib/integrations/supabase/moduleClient.d.ts +2063 -78
- package/dist-lib/integrations/supabase/moduleClient.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +2097 -68
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +1 -0
- package/dist-lib/lib/attachAuthToken.d.ts +2 -0
- package/dist-lib/lib/attachAuthToken.d.ts.map +1 -0
- package/dist-lib/lib/attachAuthToken.js +50 -0
- package/dist-lib/lib/bufferMatching.d.ts.map +1 -1
- package/dist-lib/lib/bufferMatching.js +1 -0
- package/dist-lib/lib/cartPlanRequests.server.d.ts +13 -0
- package/dist-lib/lib/cartPlanRequests.server.d.ts.map +1 -0
- package/dist-lib/lib/cartPlanRequests.server.js +168 -0
- package/dist-lib/lib/controlScan.server.d.ts +12 -0
- package/dist-lib/lib/controlScan.server.d.ts.map +1 -0
- package/dist-lib/lib/controlScan.server.js +197 -0
- package/dist-lib/lib/dimensionData.server.d.ts +11 -1
- package/dist-lib/lib/dimensionData.server.d.ts.map +1 -1
- package/dist-lib/lib/dimensionData.server.js +58 -48
- package/dist-lib/lib/dimensionPlanner.d.ts +26 -1
- package/dist-lib/lib/dimensionPlanner.d.ts.map +1 -1
- package/dist-lib/lib/dimensionPlanner.js +37 -6
- package/dist-lib/lib/floridayDocuments.functions.d.ts +6240 -285
- package/dist-lib/lib/floridayDocuments.functions.d.ts.map +1 -1
- package/dist-lib/lib/floridayDocuments.functions.js +4 -3
- package/dist-lib/lib/floridayPush.functions.d.ts +2064 -79
- package/dist-lib/lib/floridayPush.functions.d.ts.map +1 -1
- package/dist-lib/lib/floridayPush.functions.js +2 -1
- package/dist-lib/lib/handlers/logistics-tick.server.d.ts.map +1 -1
- package/dist-lib/lib/handlers/logistics-tick.server.js +57 -0
- package/dist-lib/lib/loadCarrierPlanning.functions.d.ts +10302 -377
- package/dist-lib/lib/loadCarrierPlanning.functions.d.ts.map +1 -1
- package/dist-lib/lib/loadCarrierPlanning.functions.js +6 -5
- package/dist-lib/lib/loadCarrierPlanning.server.d.ts +42 -0
- package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -1
- package/dist-lib/lib/loadCarrierPlanning.server.js +563 -31
- package/dist-lib/lib/logisticsPlanner.d.ts.map +1 -1
- package/dist-lib/lib/logisticsPlanner.js +24 -9
- package/dist-lib/lib/logisticsRecalcJob.functions.d.ts +4163 -193
- package/dist-lib/lib/logisticsRecalcJob.functions.d.ts.map +1 -1
- package/dist-lib/lib/logisticsRecalcJob.functions.js +3 -2
- package/dist-lib/lib/logisticsRecalcJob.server.d.ts.map +1 -1
- package/dist-lib/lib/logisticsRecalcJob.server.js +1 -0
- package/dist-lib/lib/logisticsRecalculate.functions.d.ts +2064 -79
- package/dist-lib/lib/logisticsRecalculate.functions.d.ts.map +1 -1
- package/dist-lib/lib/logisticsRecalculate.functions.js +3 -1
- package/dist-lib/lib/modulePrompts.d.ts.map +1 -1
- package/dist-lib/lib/modulePrompts.js +2560 -1586
- package/dist-lib/lib/planQueue.server.d.ts.map +1 -1
- package/dist-lib/lib/planQueue.server.js +6 -0
- package/dist-lib/lib/readableServerErrors.d.ts +2 -0
- package/dist-lib/lib/readableServerErrors.d.ts.map +1 -0
- package/dist-lib/lib/readableServerErrors.js +36 -0
- package/dist-lib/lib/ruleEngineSchema.d.ts +1 -1
- package/dist-lib/lib/ruleEngineSchema.d.ts.map +1 -1
- package/dist-lib/lib/ruleEngineSchema.js +72 -51
- package/dist-lib/lib/ruleEngineWizard.d.ts.map +1 -1
- package/dist-lib/lib/ruleEngineWizard.js +5 -6
- 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
|
|
194
|
-
|
|
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 } =
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
@@ -566,6 +883,9 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
566
883
|
};
|
|
567
884
|
}
|
|
568
885
|
let plannedCarts = [];
|
|
886
|
+
// Laagindeling van de afmetingenmethode, per cart_number. Alleen gevuld als
|
|
887
|
+
// er daadwerkelijk op afmetingen is ingedeeld.
|
|
888
|
+
let dimensionPlan = null;
|
|
569
889
|
let usedFloridayPacking = false;
|
|
570
890
|
// Overname-pad: één order in de buffer én een volledige Floriday-verpakking.
|
|
571
891
|
// Dan neemt de engine de verhouding van Floriday over en optimaliseert niet.
|
|
@@ -580,7 +900,8 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
580
900
|
// Globale methode: indelen op basis van fysieke afmetingen.
|
|
581
901
|
const carrierRow = profiles.find((p) => p.code === (options.carrierType ?? defaultProfile.code)) ?? null;
|
|
582
902
|
const { carrier, missing: carrierMissing } = carrierDimensions(carrierRow);
|
|
583
|
-
const { items: dimItems, missing: itemMissing } = await buildDimensionItems(sb, inputs);
|
|
903
|
+
const { items: dimItems, missing: itemMissing, derived: derivedHeights, } = await buildDimensionItems(sb, inputs, carrier, carrierRow?.layers_per_carrier ?? null);
|
|
904
|
+
warnings.push(...derivedHeights);
|
|
584
905
|
const missing = [...carrierMissing, ...itemMissing];
|
|
585
906
|
let planError = null;
|
|
586
907
|
if (missing.length === 0 && carrier) {
|
|
@@ -588,8 +909,14 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
588
909
|
if (result.error)
|
|
589
910
|
planError = result.error;
|
|
590
911
|
else {
|
|
591
|
-
|
|
592
|
-
|
|
912
|
+
const invalidCart = result.carts.map(validateDimensionCart).find((message) => message != null) ?? null;
|
|
913
|
+
if (invalidCart)
|
|
914
|
+
planError = invalidCart;
|
|
915
|
+
else {
|
|
916
|
+
plannedCarts = dimensionCartsToPlannedUnits(result.carts);
|
|
917
|
+
dimensionPlan = new Map(result.carts.map((c) => [c.cart_number, c]));
|
|
918
|
+
warnings.push(...result.warnings);
|
|
919
|
+
}
|
|
593
920
|
}
|
|
594
921
|
}
|
|
595
922
|
if (missing.length > 0 || planError) {
|
|
@@ -621,6 +948,19 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
621
948
|
});
|
|
622
949
|
if (blockError)
|
|
623
950
|
warnings.push(`Blokkade kon niet worden vastgelegd: ${blockError.message}`);
|
|
951
|
+
// Karren die met een andere (oudere) werkwijze zijn gemaakt mogen niet
|
|
952
|
+
// blijven staan: dan lijkt er een geldige indeling te zijn terwijl de
|
|
953
|
+
// afmetingen ontbreken. Karren van de afmetingenmethode blijven staan.
|
|
954
|
+
// Bij aanvullen blijven bestaande (bevroren) karren altijd staan.
|
|
955
|
+
if (!isSupplement) {
|
|
956
|
+
const { error: staleError } = await sb
|
|
957
|
+
.from("order_carts")
|
|
958
|
+
.delete()
|
|
959
|
+
.eq("buffer_id", bufferId)
|
|
960
|
+
.or("layout_method.is.null,layout_method.neq.afmetingen");
|
|
961
|
+
if (staleError)
|
|
962
|
+
warnings.push(`Oude karrenindeling kon niet worden opgeruimd: ${staleError.message}`);
|
|
963
|
+
}
|
|
624
964
|
return {
|
|
625
965
|
ok: false,
|
|
626
966
|
bufferId,
|
|
@@ -660,8 +1000,10 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
660
1000
|
// Zonder optimale vulling wordt er niet bijgemengd in een laag.
|
|
661
1001
|
maxBeladingenPerLayer: rules && !rules.optimalFill ? 1 : (settings?.max_beladingen_per_layer ?? 2),
|
|
662
1002
|
neverMixProductIds: neverMix,
|
|
663
|
-
|
|
664
|
-
|
|
1003
|
+
// Binnen een bufferorder mag alles samen op een kar: de grenzen voor
|
|
1004
|
+
// aantal orders en klanten gelden dan niet.
|
|
1005
|
+
maxOrders: isBufferOrder ? null : rules?.maxOrders ?? null,
|
|
1006
|
+
maxCustomers: isBufferOrder ? null : rules?.maxCustomers ?? settings?.max_customers_per_cart ?? null,
|
|
665
1007
|
oneCustomerPerLayer: Boolean(settings?.max_one_customer_per_layer),
|
|
666
1008
|
ownCarrierFromLayers: settings?.own_cart_from_layers ?? null,
|
|
667
1009
|
sortOrder: "none",
|
|
@@ -719,24 +1061,93 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
719
1061
|
carts: plannedCarts,
|
|
720
1062
|
};
|
|
721
1063
|
}
|
|
722
|
-
|
|
1064
|
+
const persistentIdentities = assignPersistentCartIdentities(plannedCarts, itemToOrder, {
|
|
1065
|
+
shared: isBufferOrder || isSupplement,
|
|
1066
|
+
startNumber: cartNumberStart,
|
|
1067
|
+
existingCount: frozenCartCount,
|
|
1068
|
+
});
|
|
1069
|
+
assertPersistentCartIdentities(persistentIdentities, orderIds);
|
|
1070
|
+
// Houd de fysieke afmetingenindeling aan dezelfde definitieve karidentiteit.
|
|
1071
|
+
const dimensionCartsByPlanIndex = dimensionPlan
|
|
1072
|
+
? plannedCarts.map((cart) => dimensionPlan?.get(cart.cart_number) ?? null)
|
|
1073
|
+
: null;
|
|
1074
|
+
if (dimensionPlan) {
|
|
1075
|
+
const normalizedDimensionPlan = new Map();
|
|
1076
|
+
plannedCarts.forEach((cart, index) => {
|
|
1077
|
+
const dimensionCart = dimensionCartsByPlanIndex?.[index] ?? null;
|
|
1078
|
+
const identity = persistentIdentities[index];
|
|
1079
|
+
if (!dimensionCart || !identity)
|
|
1080
|
+
return;
|
|
1081
|
+
cart.cart_number = identity.cartNumber;
|
|
1082
|
+
dimensionCart.cart_number = identity.cartNumber;
|
|
1083
|
+
normalizedDimensionPlan.set(index, dimensionCart);
|
|
1084
|
+
});
|
|
1085
|
+
dimensionPlan = normalizedDimensionPlan;
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
plannedCarts.forEach((cart, index) => {
|
|
1089
|
+
const identity = persistentIdentities[index];
|
|
1090
|
+
if (identity)
|
|
1091
|
+
cart.cart_number = identity.cartNumber;
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
if (plannedCarts.length === 0 && orderIds.length > 0) {
|
|
1095
|
+
const reason = `Karrenindeling ongeldig voor order(s) ${orderRefs.join(", ")}: ` +
|
|
1096
|
+
"een vrij te geven order moet minimaal één fysieke kar hebben.";
|
|
1097
|
+
const { error: blockError } = await sb.rpc("logistics_plan_block", {
|
|
1098
|
+
p_order_ids: orderIds,
|
|
1099
|
+
p_reason: reason,
|
|
1100
|
+
p_details: { bufferId, path: useDimensions ? "afmetingen" : "belading", carts: 0 },
|
|
1101
|
+
});
|
|
1102
|
+
if (blockError)
|
|
1103
|
+
warnings.push(`Blokkade kon niet worden vastgelegd: ${blockError.message}`);
|
|
1104
|
+
return {
|
|
1105
|
+
ok: false,
|
|
1106
|
+
bufferId,
|
|
1107
|
+
orderIds,
|
|
1108
|
+
cartsCreated: 0,
|
|
1109
|
+
replacedManualCount,
|
|
1110
|
+
usedFloridayPacking,
|
|
1111
|
+
warnings: [...warnings, reason],
|
|
1112
|
+
skipped: "blocked",
|
|
1113
|
+
blockedReason: reason,
|
|
1114
|
+
beladingSources,
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
// Opruimen op buffer EN op order. Verhuist een order naar een andere buffer,
|
|
1118
|
+
// dan blijven de karren van de oude buffer anders als weeskar staan en telt
|
|
1119
|
+
// Controle karren ze dubbel. Bevroren karren (geprint, gescand, gecontroleerd)
|
|
1120
|
+
// blijven altijd staan.
|
|
1121
|
+
const keepFilter = keepCartIds.length > 0 ? `(${keepCartIds.join(",")})` : null;
|
|
1122
|
+
const clearExistingCarts = async () => {
|
|
1123
|
+
const ids = orderIds.filter(Boolean);
|
|
1124
|
+
const del = () => {
|
|
1125
|
+
const q = sb.from("order_carts").delete();
|
|
1126
|
+
return keepFilter ? q.not("id", "in", keepFilter) : q;
|
|
1127
|
+
};
|
|
1128
|
+
await del().eq("buffer_id", bufferId);
|
|
1129
|
+
if (ids.length > 0) {
|
|
1130
|
+
await del().in("order_id", ids);
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
await clearExistingCarts();
|
|
723
1134
|
// In blokken wegschrijven: per ladingdrager een aparte insert is bij grote
|
|
724
1135
|
// orders veel te traag.
|
|
725
1136
|
let created = 0;
|
|
726
1137
|
const CHUNK = 100;
|
|
727
1138
|
for (let start = 0; start < plannedCarts.length; start += CHUNK) {
|
|
728
1139
|
const chunk = plannedCarts.slice(start, start + CHUNK);
|
|
729
|
-
const cartRows = chunk.map((cart) => {
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
orderIdsOnCart.add(oid);
|
|
1140
|
+
const cartRows = chunk.map((cart, chunkIndex) => {
|
|
1141
|
+
const cartIndex = start + chunkIndex;
|
|
1142
|
+
const identity = persistentIdentities[cartIndex];
|
|
1143
|
+
if (!identity) {
|
|
1144
|
+
throw new Error(`Karrenindeling ongeldig: identiteit ontbreekt voor kar ${cartIndex + 1}.`);
|
|
735
1145
|
}
|
|
736
1146
|
return {
|
|
737
|
-
order_id:
|
|
1147
|
+
order_id: identity.ownerOrderId,
|
|
738
1148
|
buffer_id: bufferId,
|
|
739
|
-
cart_number:
|
|
1149
|
+
cart_number: identity.cartNumber,
|
|
1150
|
+
total_carts: identity.totalCarts,
|
|
740
1151
|
is_auto: true,
|
|
741
1152
|
is_complete: cart.is_complete,
|
|
742
1153
|
source: usedFloridayPacking ? "floriday" : "auto",
|
|
@@ -753,18 +1164,23 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
753
1164
|
const { data: insertedCarts, error } = await sb
|
|
754
1165
|
.from("order_carts")
|
|
755
1166
|
.insert(cartRows)
|
|
756
|
-
.select("id, cart_number");
|
|
1167
|
+
.select("id, order_id, cart_number");
|
|
757
1168
|
if (error || !Array.isArray(insertedCarts)) {
|
|
758
1169
|
const message = `Ladingdragers konden niet worden opgeslagen: ${error?.message ?? "onbekende fout"}`;
|
|
759
1170
|
console.error(`[logistiek-engine] buffer ${bufferId}: ${message}`);
|
|
760
|
-
await
|
|
1171
|
+
await clearExistingCarts();
|
|
761
1172
|
throw new Error(message);
|
|
762
1173
|
}
|
|
763
1174
|
created += insertedCarts.length;
|
|
764
|
-
const
|
|
1175
|
+
const identityKey = (orderId, cartNumber) => `${orderId == null ? "shared" : String(orderId)}:${Number(cartNumber)}`;
|
|
1176
|
+
const idByIdentity = new Map(insertedCarts.map((c) => [identityKey(c.order_id, c.cart_number), String(c.id)]));
|
|
765
1177
|
const itemRows = [];
|
|
766
|
-
for (
|
|
767
|
-
const
|
|
1178
|
+
for (let chunkIndex = 0; chunkIndex < chunk.length; chunkIndex++) {
|
|
1179
|
+
const cart = chunk[chunkIndex];
|
|
1180
|
+
const identity = persistentIdentities[start + chunkIndex];
|
|
1181
|
+
const cartId = identity
|
|
1182
|
+
? idByIdentity.get(identityKey(identity.ownerOrderId, identity.cartNumber))
|
|
1183
|
+
: undefined;
|
|
768
1184
|
if (!cartId)
|
|
769
1185
|
continue;
|
|
770
1186
|
for (const i of cart.items) {
|
|
@@ -774,7 +1190,7 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
774
1190
|
order_item_id: i.order_item_id.includes("::") ? i.order_item_id.split("::")[0] : i.order_item_id,
|
|
775
1191
|
product_id: i.product_id,
|
|
776
1192
|
product_name: i.product_name,
|
|
777
|
-
crates: Math.
|
|
1193
|
+
crates: Math.round(i.crates),
|
|
778
1194
|
boxes: i.boxes ?? null,
|
|
779
1195
|
weight_kg: i.weight_kg ?? null,
|
|
780
1196
|
plants_per_crate: i.plants_per_crate,
|
|
@@ -799,10 +1215,126 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
799
1215
|
(itemError.details ? ` — ${itemError.details}` : "") +
|
|
800
1216
|
(itemError.hint ? ` (${itemError.hint})` : "");
|
|
801
1217
|
console.error(`[logistiek-engine] buffer ${bufferId}: ${message}`);
|
|
802
|
-
await
|
|
1218
|
+
await clearExistingCarts();
|
|
803
1219
|
throw new Error(message);
|
|
804
1220
|
}
|
|
805
1221
|
}
|
|
1222
|
+
// Fysieke laagindeling per kar. Alleen de afmetingenmethode levert posities;
|
|
1223
|
+
// bij belading blijft de indeling leeg ("nog geen laagindeling aangeleverd").
|
|
1224
|
+
if (dimensionPlan) {
|
|
1225
|
+
for (let chunkIndex = 0; chunkIndex < chunk.length; chunkIndex++) {
|
|
1226
|
+
const cart = chunk[chunkIndex];
|
|
1227
|
+
const identity = persistentIdentities[start + chunkIndex];
|
|
1228
|
+
const cartId = identity
|
|
1229
|
+
? idByIdentity.get(identityKey(identity.ownerOrderId, identity.cartNumber))
|
|
1230
|
+
: undefined;
|
|
1231
|
+
const dimCart = dimensionPlan.get(start + chunkIndex);
|
|
1232
|
+
if (!cartId || !dimCart)
|
|
1233
|
+
continue;
|
|
1234
|
+
const rows = layerPositionRows(cartId, dimCart);
|
|
1235
|
+
// Atomair vervangen: eerst de vorige indeling van deze kar weg.
|
|
1236
|
+
await sb.from("order_cart_layer_positions").delete().eq("cart_id", cartId);
|
|
1237
|
+
for (let s = 0; s < rows.length; s += 500) {
|
|
1238
|
+
const slice = rows.slice(s, s + 500);
|
|
1239
|
+
if (slice.length === 0)
|
|
1240
|
+
continue;
|
|
1241
|
+
const { error: posError } = await sb
|
|
1242
|
+
.from("order_cart_layer_positions")
|
|
1243
|
+
.upsert(slice, { onConflict: "cart_id,layer,keg_id" });
|
|
1244
|
+
if (posError) {
|
|
1245
|
+
// Een halve indeling is misleidend: de volledige buffer verwijderen
|
|
1246
|
+
// en de wachtrij laten falen, zodat niets als geldig wordt getoond.
|
|
1247
|
+
await clearExistingCarts();
|
|
1248
|
+
throw new Error(`De fysieke laagindeling van kar ${cart.cart_number} kon niet worden opgeslagen: ${posError.message}`);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
const expectedPositions = rows.length;
|
|
1252
|
+
const { count: storedPositions, error: countError } = await sb
|
|
1253
|
+
.from("order_cart_layer_positions")
|
|
1254
|
+
.select("id", { count: "exact", head: true })
|
|
1255
|
+
.eq("cart_id", cartId);
|
|
1256
|
+
if (countError || storedPositions !== expectedPositions) {
|
|
1257
|
+
await clearExistingCarts();
|
|
1258
|
+
throw new Error(`De fysieke indeling van kar ${cart.cart_number} is niet volledig opgeslagen: ` +
|
|
1259
|
+
`${expectedPositions} fusten verwacht, ${storedPositions ?? 0} opgeslagen.`);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
// Sluitcontrole: elke geschreven kar moet de actieve methode én het bijbehorende
|
|
1265
|
+
// versienummer dragen. Zo kan een oudere rekenwijze (bijvoorbeeld een nog niet
|
|
1266
|
+
// bijgewerkte publicatie) nooit stilzwijgend een indeling achterlaten.
|
|
1267
|
+
{
|
|
1268
|
+
const expectedMethod = useDimensions ? "afmetingen" : "belading";
|
|
1269
|
+
const expectedVersion = useDimensions ? DIMENSION_PLANNER_VERSION : LOAD_CARRIER_PLANNER_VERSION;
|
|
1270
|
+
const verifyQuery = sb
|
|
1271
|
+
.from("order_carts")
|
|
1272
|
+
.select("cart_number, layout_method, algorithm_version")
|
|
1273
|
+
.eq("buffer_id", bufferId);
|
|
1274
|
+
// Bevroren karren zijn met een eerdere ronde geschreven en blijven buiten
|
|
1275
|
+
// deze controle: hun identiteit mag nooit meer wijzigen.
|
|
1276
|
+
const { data: writtenCarts, error: verifyError } = keepFilter
|
|
1277
|
+
? await verifyQuery.not("id", "in", keepFilter)
|
|
1278
|
+
: await verifyQuery;
|
|
1279
|
+
const rows = Array.isArray(writtenCarts) ? writtenCarts : [];
|
|
1280
|
+
const wrong = rows.filter((c) => String(c.layout_method ?? "") !== expectedMethod ||
|
|
1281
|
+
Number(c.algorithm_version) !== expectedVersion);
|
|
1282
|
+
if (verifyError || wrong.length > 0) {
|
|
1283
|
+
const reason = verifyError
|
|
1284
|
+
? `Karrenindeling kon niet worden gecontroleerd: ${verifyError.message}`
|
|
1285
|
+
: `Karrenindeling is niet volgens de ingestelde werkwijze "${expectedMethod}" vastgelegd ` +
|
|
1286
|
+
`(${wrong.length} van ${rows.length} karren). Er is niets ingedeeld.`;
|
|
1287
|
+
console.error(`[logistiek-engine] buffer ${bufferId}: ${reason}`);
|
|
1288
|
+
await clearExistingCarts();
|
|
1289
|
+
await sb.rpc("logistics_plan_block", {
|
|
1290
|
+
p_order_ids: orderIds,
|
|
1291
|
+
p_reason: reason,
|
|
1292
|
+
p_details: { bufferId, path: "verificatie", expectedMethod, expectedVersion },
|
|
1293
|
+
});
|
|
1294
|
+
return {
|
|
1295
|
+
ok: false,
|
|
1296
|
+
bufferId,
|
|
1297
|
+
orderIds,
|
|
1298
|
+
cartsCreated: 0,
|
|
1299
|
+
replacedManualCount,
|
|
1300
|
+
usedFloridayPacking,
|
|
1301
|
+
warnings: [...warnings, reason],
|
|
1302
|
+
skipped: "blocked",
|
|
1303
|
+
blockedReason: reason,
|
|
1304
|
+
beladingSources,
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
// Sluitcontrole op de buffer: elk lid moet na afloop een fysieke kar hebben,
|
|
1309
|
+
// eigen kar of een gedeelde bufferkar met een van zijn orderregels. Zonder kar
|
|
1310
|
+
// krijgt dat lid geen pickdocument, en dat is nooit een geldige uitkomst.
|
|
1311
|
+
{
|
|
1312
|
+
const membersToCheck = allOrderRows.map((o) => String(o.id));
|
|
1313
|
+
const stillMissing = await bufferOrdersWithoutCarts(sb, bufferId, membersToCheck);
|
|
1314
|
+
if (stillMissing.length > 0) {
|
|
1315
|
+
const numberById = new Map(allOrderRows.map((o) => [String(o.id), String(o.order_number ?? o.id)]));
|
|
1316
|
+
const refs = stillMissing.map((id) => numberById.get(id) ?? id);
|
|
1317
|
+
const reason = `Bufferindeling onvolledig: order(s) ${refs.join(", ")} hebben geen enkele kar ` +
|
|
1318
|
+
`en zouden dus geen pickdocument krijgen.`;
|
|
1319
|
+
console.error(`[logistiek-engine] buffer ${bufferId}: ${reason}`);
|
|
1320
|
+
await sb.rpc("logistics_plan_block", {
|
|
1321
|
+
p_order_ids: stillMissing,
|
|
1322
|
+
p_reason: reason,
|
|
1323
|
+
p_details: { bufferId, path: "bufferdekking", missing: refs },
|
|
1324
|
+
});
|
|
1325
|
+
return {
|
|
1326
|
+
ok: false,
|
|
1327
|
+
bufferId,
|
|
1328
|
+
orderIds,
|
|
1329
|
+
cartsCreated: created,
|
|
1330
|
+
replacedManualCount,
|
|
1331
|
+
usedFloridayPacking,
|
|
1332
|
+
warnings: [...warnings, reason],
|
|
1333
|
+
skipped: "blocked",
|
|
1334
|
+
blockedReason: reason,
|
|
1335
|
+
beladingSources,
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
806
1338
|
}
|
|
807
1339
|
return {
|
|
808
1340
|
ok: true,
|