@flowselections/logistiek-engine 1.0.26 → 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 +37 -0
- package/dist-lib/lib/dimensionData.server.d.ts.map +1 -0
- package/dist-lib/lib/dimensionData.server.js +167 -0
- package/dist-lib/lib/dimensionPlanner.d.ts +95 -0
- package/dist-lib/lib/dimensionPlanner.d.ts.map +1 -0
- package/dist-lib/lib/dimensionPlanner.js +248 -0
- 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/loadCarrierPlanner.d.ts +3 -0
- package/dist-lib/lib/loadCarrierPlanner.d.ts.map +1 -1
- 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 +46 -0
- package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -1
- package/dist-lib/lib/loadCarrierPlanning.server.js +673 -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 +2567 -1563
- 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 +83 -51
- package/dist-lib/lib/ruleEngineWizard.d.ts.map +1 -1
- package/dist-lib/lib/ruleEngineWizard.js +6 -6
- package/package.json +1 -1
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// ============================================================================
|
|
8
8
|
import { planLoadUnits, LOAD_CARRIER_PLANNER_VERSION, } from "./loadCarrierPlanner";
|
|
9
9
|
import { fallbackCarrierProfile } from "./loadCarrierTypes";
|
|
10
|
+
import { planByDimensions, DIMENSION_PLANNER_VERSION } from "./dimensionPlanner";
|
|
11
|
+
import { buildDimensionItems, carrierDimensions } from "./dimensionData.server";
|
|
10
12
|
// ---------------------------------------------------------------------------
|
|
11
13
|
// Buffer
|
|
12
14
|
// ---------------------------------------------------------------------------
|
|
@@ -180,6 +182,16 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
|
|
|
180
182
|
.map((r) => r.product_id)
|
|
181
183
|
.filter((id) => typeof id === "string" && UUID_RE.test(id));
|
|
182
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
|
+
}
|
|
183
195
|
if (groupIds.length > 0) {
|
|
184
196
|
const { data: groups } = await sb.from("product_groups").select("id").in("id", groupIds);
|
|
185
197
|
groupSet = new Set((groups ?? []).map((g) => g.id));
|
|
@@ -188,8 +200,14 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
|
|
|
188
200
|
const packing = row.order_id ? options.packingByOrder?.get(String(row.order_id)) ?? null : null;
|
|
189
201
|
const { belading: base, source } = resolveBelading(row, packing);
|
|
190
202
|
const plants = Math.max(0, Number(row.quantity ?? 0));
|
|
191
|
-
const
|
|
192
|
-
|
|
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]");
|
|
193
211
|
if (!isGroup) {
|
|
194
212
|
options.sources?.set(String(row.id), source);
|
|
195
213
|
out.push({
|
|
@@ -205,11 +223,13 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
|
|
|
205
223
|
});
|
|
206
224
|
continue;
|
|
207
225
|
}
|
|
208
|
-
const { data: members } =
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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: [] };
|
|
213
233
|
const memberList = Array.isArray(members) ? members : [];
|
|
214
234
|
if (memberList.length === 0) {
|
|
215
235
|
options.sources?.set(String(row.id), source);
|
|
@@ -328,6 +348,7 @@ export async function loadBeladingRules(sb) {
|
|
|
328
348
|
maxLayers: k.fill_layers_as_much_as_fits ? null : numOrNull(k.max_layers),
|
|
329
349
|
maxBoxes: k.fill_boxes_as_much_as_fits ? null : numOrNull(k.max_boxes_per_pallet),
|
|
330
350
|
optimalFill: k.optimal_fill !== false,
|
|
351
|
+
layoutMethod: k.cart_layout_method === "afmetingen" ? "afmetingen" : "belading",
|
|
331
352
|
};
|
|
332
353
|
}
|
|
333
354
|
/** Past de grenzen uit de werkwijze toe op het capaciteitsprofiel van een ladingdrager. */
|
|
@@ -414,19 +435,246 @@ export function planByFloridayPacking(inputs, profile, packing) {
|
|
|
414
435
|
return carts;
|
|
415
436
|
}
|
|
416
437
|
// ---------------------------------------------------------------------------
|
|
438
|
+
// Indeling op afmetingen omzetten naar het bestaande opslagmodel
|
|
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
|
+
}
|
|
507
|
+
export function dimensionCartsToPlannedUnits(carts) {
|
|
508
|
+
return carts.map((cart) => {
|
|
509
|
+
const layerCount = cart.layers.length || 1;
|
|
510
|
+
const items = [];
|
|
511
|
+
for (const layer of cart.layers) {
|
|
512
|
+
const cratesInLayer = layer.placements.reduce((s, p) => s + p.crates, 0) || 1;
|
|
513
|
+
for (const p of layer.placements) {
|
|
514
|
+
const fraction = p.crates / cratesInLayer;
|
|
515
|
+
items.push({
|
|
516
|
+
order_item_id: p.order_item_id,
|
|
517
|
+
order_id: p.order_id,
|
|
518
|
+
product_id: p.product_id,
|
|
519
|
+
product_name: p.product_name,
|
|
520
|
+
crates: p.crates,
|
|
521
|
+
plants: p.plants,
|
|
522
|
+
boxes: p.crates,
|
|
523
|
+
weight_kg: p.weight_kg,
|
|
524
|
+
plants_per_crate: p.plants_per_crate,
|
|
525
|
+
crates_per_layer: cratesInLayer,
|
|
526
|
+
layers_per_cart: layerCount,
|
|
527
|
+
layer_fraction: Number(fraction.toFixed(3)),
|
|
528
|
+
layer_share_pct: Number(((100 * fraction) / layerCount).toFixed(2)),
|
|
529
|
+
layer_index: layer.layer_index,
|
|
530
|
+
crates_in_layer: p.crates,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
cart_number: cart.cart_number,
|
|
536
|
+
carrier_type: cart.carrier_code,
|
|
537
|
+
carrier_type_id: cart.carrier_id,
|
|
538
|
+
source: "auto",
|
|
539
|
+
is_complete: cart.is_complete,
|
|
540
|
+
occupancy_pct: cart.occupancy_pct,
|
|
541
|
+
total_layers_used: layerCount,
|
|
542
|
+
max_layers: layerCount,
|
|
543
|
+
weight_kg: cart.weight_kg,
|
|
544
|
+
items,
|
|
545
|
+
};
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
// ---------------------------------------------------------------------------
|
|
417
549
|
// Herplannen van een buffer
|
|
418
550
|
// ---------------------------------------------------------------------------
|
|
419
551
|
/** Maximaal aantal ladingdragers dat één orderbuffer mag opleveren. */
|
|
420
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
|
+
}
|
|
421
646
|
export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
422
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;
|
|
423
652
|
const { data: orders } = await sb
|
|
424
653
|
.from("orders")
|
|
425
|
-
.select("id, customer_id, custom_fields, plan_locked_at")
|
|
654
|
+
.select("id, order_number, customer_id, custom_fields, plan_locked_at")
|
|
426
655
|
.eq("buffer_id", bufferId)
|
|
427
656
|
.is("deleted_at", null);
|
|
428
|
-
const
|
|
657
|
+
const allOrderRows = Array.isArray(orders) ? orders : [];
|
|
658
|
+
const orderRows = allOrderRows;
|
|
429
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;
|
|
430
678
|
if (orderRows.some((o) => o.plan_locked_at)) {
|
|
431
679
|
return {
|
|
432
680
|
ok: true,
|
|
@@ -439,6 +687,66 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
439
687
|
skipped: "locked",
|
|
440
688
|
};
|
|
441
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
|
+
}
|
|
442
750
|
const idList = orderIds.length > 0 ? orderIds : ["00000000-0000-0000-0000-000000000000"];
|
|
443
751
|
const { data: items } = await sb
|
|
444
752
|
.from("order_items")
|
|
@@ -458,7 +766,63 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
458
766
|
packingByOrder.set(String(o.id), belading);
|
|
459
767
|
}
|
|
460
768
|
const sources = new Map();
|
|
461
|
-
|
|
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
|
+
}
|
|
462
826
|
const itemToOrder = new Map();
|
|
463
827
|
for (const i of inputs)
|
|
464
828
|
if (i.order_id)
|
|
@@ -487,7 +851,8 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
487
851
|
const guardProfile = profileFor(profiles, options.carrierType ?? defaultProfile.code);
|
|
488
852
|
const needsBelading = guardProfile.capacity_model !== "single" && guardProfile.capacity_model !== "none";
|
|
489
853
|
const unknownItems = inputs.filter((i) => sources.get(i.order_item_id) === "geen" && i.plants > 0);
|
|
490
|
-
|
|
854
|
+
const useDimensions = rules?.layoutMethod === "afmetingen";
|
|
855
|
+
if (!useDimensions && needsBelading && unknownItems.length > 0 && !options.dryRun) {
|
|
491
856
|
const reason = `Belading onbekend voor ${unknownItems.length} orderregel(s): geen Floriday-verpakking, ` +
|
|
492
857
|
`geen waarden op de orderregel en geen productinstelling. Er is niets ingedeeld.`;
|
|
493
858
|
console.error(`[logistiek-engine] buffer ${bufferId} geblokkeerd: ${reason}`);
|
|
@@ -518,6 +883,9 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
518
883
|
};
|
|
519
884
|
}
|
|
520
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;
|
|
521
889
|
let usedFloridayPacking = false;
|
|
522
890
|
// Overname-pad: één order in de buffer én een volledige Floriday-verpakking.
|
|
523
891
|
// Dan neemt de engine de verhouding van Floriday over en optimaliseert niet.
|
|
@@ -528,7 +896,86 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
528
896
|
const soloPacking = solo ? beladingFromFloridayPacking(solo.custom_fields?.floriday_packing) : null;
|
|
529
897
|
const takeoverProfile = profileFor(profiles, options.carrierType ?? defaultProfile.code);
|
|
530
898
|
const takeoverAllowed = takeoverProfile.capacity_model === "layered" || takeoverProfile.capacity_model === "stacked";
|
|
531
|
-
if (
|
|
899
|
+
if (useDimensions) {
|
|
900
|
+
// Globale methode: indelen op basis van fysieke afmetingen.
|
|
901
|
+
const carrierRow = profiles.find((p) => p.code === (options.carrierType ?? defaultProfile.code)) ?? null;
|
|
902
|
+
const { carrier, missing: carrierMissing } = carrierDimensions(carrierRow);
|
|
903
|
+
const { items: dimItems, missing: itemMissing, derived: derivedHeights, } = await buildDimensionItems(sb, inputs, carrier, carrierRow?.layers_per_carrier ?? null);
|
|
904
|
+
warnings.push(...derivedHeights);
|
|
905
|
+
const missing = [...carrierMissing, ...itemMissing];
|
|
906
|
+
let planError = null;
|
|
907
|
+
if (missing.length === 0 && carrier) {
|
|
908
|
+
const result = planByDimensions(dimItems, carrier);
|
|
909
|
+
if (result.error)
|
|
910
|
+
planError = result.error;
|
|
911
|
+
else {
|
|
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
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (missing.length > 0 || planError) {
|
|
923
|
+
const reason = planError ?? `Afmetingen onbekend: ${missing.map((m) => m.message).join(" ")}`;
|
|
924
|
+
if (options.dryRun) {
|
|
925
|
+
return {
|
|
926
|
+
ok: false,
|
|
927
|
+
bufferId,
|
|
928
|
+
orderIds,
|
|
929
|
+
cartsCreated: 0,
|
|
930
|
+
replacedManualCount: 0,
|
|
931
|
+
usedFloridayPacking: false,
|
|
932
|
+
warnings: [...warnings, reason],
|
|
933
|
+
skipped: "blocked",
|
|
934
|
+
blockedReason: reason,
|
|
935
|
+
beladingSources,
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
console.error(`[logistiek-engine] buffer ${bufferId} geblokkeerd (afmetingen): ${reason}`);
|
|
939
|
+
const { error: blockError } = await sb.rpc("logistics_plan_block", {
|
|
940
|
+
p_order_ids: orderIds,
|
|
941
|
+
p_reason: reason,
|
|
942
|
+
p_details: {
|
|
943
|
+
bufferId,
|
|
944
|
+
path: "afmetingen",
|
|
945
|
+
carrier: carrierRow?.code ?? null,
|
|
946
|
+
missing: missing.slice(0, 20),
|
|
947
|
+
},
|
|
948
|
+
});
|
|
949
|
+
if (blockError)
|
|
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
|
+
}
|
|
964
|
+
return {
|
|
965
|
+
ok: false,
|
|
966
|
+
bufferId,
|
|
967
|
+
orderIds,
|
|
968
|
+
cartsCreated: 0,
|
|
969
|
+
replacedManualCount: 0,
|
|
970
|
+
usedFloridayPacking: false,
|
|
971
|
+
warnings: [...warnings, reason],
|
|
972
|
+
skipped: "blocked",
|
|
973
|
+
blockedReason: reason,
|
|
974
|
+
beladingSources,
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
else if (solo && soloPacking && takeoverAllowed) {
|
|
532
979
|
usedFloridayPacking = true;
|
|
533
980
|
plannedCarts = planByFloridayPacking(inputs, takeoverProfile, soloPacking);
|
|
534
981
|
console.log(`[logistiek-engine] buffer ${bufferId}: overname Floriday-verpakking ` +
|
|
@@ -553,8 +1000,10 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
553
1000
|
// Zonder optimale vulling wordt er niet bijgemengd in een laag.
|
|
554
1001
|
maxBeladingenPerLayer: rules && !rules.optimalFill ? 1 : (settings?.max_beladingen_per_layer ?? 2),
|
|
555
1002
|
neverMixProductIds: neverMix,
|
|
556
|
-
|
|
557
|
-
|
|
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,
|
|
558
1007
|
oneCustomerPerLayer: Boolean(settings?.max_one_customer_per_layer),
|
|
559
1008
|
ownCarrierFromLayers: settings?.own_cart_from_layers ?? null,
|
|
560
1009
|
sortOrder: "none",
|
|
@@ -612,24 +1061,93 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
612
1061
|
carts: plannedCarts,
|
|
613
1062
|
};
|
|
614
1063
|
}
|
|
615
|
-
|
|
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();
|
|
616
1134
|
// In blokken wegschrijven: per ladingdrager een aparte insert is bij grote
|
|
617
1135
|
// orders veel te traag.
|
|
618
1136
|
let created = 0;
|
|
619
1137
|
const CHUNK = 100;
|
|
620
1138
|
for (let start = 0; start < plannedCarts.length; start += CHUNK) {
|
|
621
1139
|
const chunk = plannedCarts.slice(start, start + CHUNK);
|
|
622
|
-
const cartRows = chunk.map((cart) => {
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
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}.`);
|
|
628
1145
|
}
|
|
629
1146
|
return {
|
|
630
|
-
order_id:
|
|
1147
|
+
order_id: identity.ownerOrderId,
|
|
631
1148
|
buffer_id: bufferId,
|
|
632
|
-
cart_number:
|
|
1149
|
+
cart_number: identity.cartNumber,
|
|
1150
|
+
total_carts: identity.totalCarts,
|
|
633
1151
|
is_auto: true,
|
|
634
1152
|
is_complete: cart.is_complete,
|
|
635
1153
|
source: usedFloridayPacking ? "floriday" : "auto",
|
|
@@ -639,24 +1157,30 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
639
1157
|
carrier_type_id: cart.carrier_type_id,
|
|
640
1158
|
max_layers: cart.max_layers,
|
|
641
1159
|
weight_kg: cart.weight_kg,
|
|
642
|
-
algorithm_version: LOAD_CARRIER_PLANNER_VERSION,
|
|
1160
|
+
algorithm_version: useDimensions ? DIMENSION_PLANNER_VERSION : LOAD_CARRIER_PLANNER_VERSION,
|
|
1161
|
+
layout_method: useDimensions ? "afmetingen" : "belading",
|
|
643
1162
|
};
|
|
644
1163
|
});
|
|
645
1164
|
const { data: insertedCarts, error } = await sb
|
|
646
1165
|
.from("order_carts")
|
|
647
1166
|
.insert(cartRows)
|
|
648
|
-
.select("id, cart_number");
|
|
1167
|
+
.select("id, order_id, cart_number");
|
|
649
1168
|
if (error || !Array.isArray(insertedCarts)) {
|
|
650
1169
|
const message = `Ladingdragers konden niet worden opgeslagen: ${error?.message ?? "onbekende fout"}`;
|
|
651
1170
|
console.error(`[logistiek-engine] buffer ${bufferId}: ${message}`);
|
|
652
|
-
await
|
|
1171
|
+
await clearExistingCarts();
|
|
653
1172
|
throw new Error(message);
|
|
654
1173
|
}
|
|
655
1174
|
created += insertedCarts.length;
|
|
656
|
-
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)]));
|
|
657
1177
|
const itemRows = [];
|
|
658
|
-
for (
|
|
659
|
-
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;
|
|
660
1184
|
if (!cartId)
|
|
661
1185
|
continue;
|
|
662
1186
|
for (const i of cart.items) {
|
|
@@ -666,7 +1190,7 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
666
1190
|
order_item_id: i.order_item_id.includes("::") ? i.order_item_id.split("::")[0] : i.order_item_id,
|
|
667
1191
|
product_id: i.product_id,
|
|
668
1192
|
product_name: i.product_name,
|
|
669
|
-
crates: Math.
|
|
1193
|
+
crates: Math.round(i.crates),
|
|
670
1194
|
boxes: i.boxes ?? null,
|
|
671
1195
|
weight_kg: i.weight_kg ?? null,
|
|
672
1196
|
plants_per_crate: i.plants_per_crate,
|
|
@@ -674,6 +1198,8 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
674
1198
|
layers_per_cart: i.layers_per_cart,
|
|
675
1199
|
layer_fraction: i.layer_fraction,
|
|
676
1200
|
layer_share_pct: i.layer_share_pct,
|
|
1201
|
+
layer_index: i.layer_index ?? null,
|
|
1202
|
+
crates_in_layer: i.crates_in_layer ?? null,
|
|
677
1203
|
});
|
|
678
1204
|
}
|
|
679
1205
|
}
|
|
@@ -689,10 +1215,126 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
689
1215
|
(itemError.details ? ` — ${itemError.details}` : "") +
|
|
690
1216
|
(itemError.hint ? ` (${itemError.hint})` : "");
|
|
691
1217
|
console.error(`[logistiek-engine] buffer ${bufferId}: ${message}`);
|
|
692
|
-
await
|
|
1218
|
+
await clearExistingCarts();
|
|
693
1219
|
throw new Error(message);
|
|
694
1220
|
}
|
|
695
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
|
+
}
|
|
696
1338
|
}
|
|
697
1339
|
return {
|
|
698
1340
|
ok: true,
|