@flowselections/logistiek-engine 1.0.53 → 1.0.90
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/AfrondWachtrijCard.d.ts +2 -0
- package/dist-lib/components/AfrondWachtrijCard.d.ts.map +1 -0
- package/dist-lib/components/AfrondWachtrijCard.js +27 -0
- package/dist-lib/components/BeladingsafwijkingenCard.d.ts +2 -0
- package/dist-lib/components/BeladingsafwijkingenCard.d.ts.map +1 -0
- package/dist-lib/components/BeladingsafwijkingenCard.js +44 -0
- package/dist-lib/components/FlowCanvas.d.ts +6 -0
- package/dist-lib/components/FlowCanvas.d.ts.map +1 -1
- package/dist-lib/components/FlowCanvas.js +79 -18
- package/dist-lib/components/FlowOrders.d.ts.map +1 -1
- package/dist-lib/components/FlowOrders.js +59 -13
- package/dist-lib/components/FlowPage.d.ts.map +1 -1
- package/dist-lib/components/FlowPage.js +175 -6
- package/dist-lib/components/GeblokkeerdeOrdersCard.d.ts.map +1 -1
- package/dist-lib/components/GeblokkeerdeOrdersCard.js +21 -2
- package/dist-lib/components/KardekkingCard.d.ts +2 -0
- package/dist-lib/components/KardekkingCard.d.ts.map +1 -0
- package/dist-lib/components/KardekkingCard.js +29 -0
- package/dist-lib/components/LogistiekEnginePage.d.ts.map +1 -1
- package/dist-lib/components/LogistiekEnginePage.js +7 -1
- package/dist-lib/components/LogistiekEngineQueuePage.d.ts.map +1 -1
- package/dist-lib/components/LogistiekEngineQueuePage.js +4 -2
- package/dist-lib/components/NodeTracePanel.d.ts +4 -1
- package/dist-lib/components/NodeTracePanel.d.ts.map +1 -1
- package/dist-lib/components/NodeTracePanel.js +8 -2
- package/dist-lib/components/OrderStopBanner.d.ts +9 -2
- package/dist-lib/components/OrderStopBanner.d.ts.map +1 -1
- package/dist-lib/components/OrderStopBanner.js +27 -13
- package/dist-lib/components/OrderTraceDetails.d.ts.map +1 -1
- package/dist-lib/components/OrderTraceDetails.js +2 -1
- package/dist-lib/components/PlanWachtrijPage.d.ts +2 -0
- package/dist-lib/components/PlanWachtrijPage.d.ts.map +1 -0
- package/dist-lib/components/PlanWachtrijPage.js +99 -0
- package/dist-lib/components/PlannerDiffCard.d.ts +2 -0
- package/dist-lib/components/PlannerDiffCard.d.ts.map +1 -0
- package/dist-lib/components/PlannerDiffCard.js +25 -0
- package/dist-lib/components/RegiePage.d.ts.map +1 -1
- package/dist-lib/components/RegiePage.js +21 -13
- package/dist-lib/components/TaakGezondheidCard.d.ts +2 -0
- package/dist-lib/components/TaakGezondheidCard.d.ts.map +1 -0
- package/dist-lib/components/TaakGezondheidCard.js +31 -0
- package/dist-lib/hooks/useAfrondWachtrij.d.ts +28 -0
- package/dist-lib/hooks/useAfrondWachtrij.d.ts.map +1 -0
- package/dist-lib/hooks/useAfrondWachtrij.js +88 -0
- package/dist-lib/hooks/useBeladingsafwijkingen.d.ts +39 -0
- package/dist-lib/hooks/useBeladingsafwijkingen.d.ts.map +1 -0
- package/dist-lib/hooks/useBeladingsafwijkingen.js +119 -0
- package/dist-lib/hooks/useGeblokkeerdeOrders.d.ts +7 -0
- package/dist-lib/hooks/useGeblokkeerdeOrders.d.ts.map +1 -1
- package/dist-lib/hooks/useGeblokkeerdeOrders.js +28 -2
- package/dist-lib/hooks/useLogisticsTrace.js +1 -1
- package/dist-lib/hooks/useOrderTrace.js +1 -1
- package/dist-lib/hooks/useOrderVersies.d.ts.map +1 -1
- package/dist-lib/hooks/useOrderVersies.js +17 -3
- package/dist-lib/hooks/usePlanWachtrij.d.ts +33 -0
- package/dist-lib/hooks/usePlanWachtrij.d.ts.map +1 -0
- package/dist-lib/hooks/usePlanWachtrij.js +104 -0
- package/dist-lib/hooks/useRecalcJob.js +1 -1
- package/dist-lib/hooks/useShippingNoteDiagnose.d.ts +20 -0
- package/dist-lib/hooks/useShippingNoteDiagnose.d.ts.map +1 -0
- package/dist-lib/hooks/useShippingNoteDiagnose.js +52 -0
- package/dist-lib/hooks/useTaakGezondheid.d.ts +16 -0
- package/dist-lib/hooks/useTaakGezondheid.d.ts.map +1 -0
- package/dist-lib/hooks/useTaakGezondheid.js +43 -0
- package/dist-lib/hooks/useWerktijden.js +1 -1
- package/dist-lib/hooks/useWerkwijzeStatus.js +1 -1
- package/dist-lib/index.d.ts +10 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +10 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +3147 -159
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +3147 -159
- 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 +3147 -159
- package/dist-lib/integrations/supabase/moduleClient.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/previewAuthStorage.d.ts +1 -1
- package/dist-lib/integrations/supabase/previewAuthStorage.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/previewAuthStorage.js +8 -1
- package/dist-lib/integrations/supabase/types.d.ts +3183 -161
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/attachAuthToken.js +1 -1
- package/dist-lib/lib/bufferDecision.test.d.ts +2 -0
- package/dist-lib/lib/bufferDecision.test.d.ts.map +1 -0
- package/dist-lib/lib/bufferDecision.test.js +104 -0
- package/dist-lib/lib/cartCoverage.functions.d.ts +30820 -0
- package/dist-lib/lib/cartCoverage.functions.d.ts.map +1 -0
- package/dist-lib/lib/cartCoverage.functions.js +20 -0
- package/dist-lib/lib/floridayDocuments.functions.d.ts +8979 -15
- package/dist-lib/lib/floridayDocuments.functions.d.ts.map +1 -1
- package/dist-lib/lib/floridayDocuments.functions.js +5 -5
- package/dist-lib/lib/floridayPush.functions.d.ts +3147 -159
- package/dist-lib/lib/floridayPush.functions.d.ts.map +1 -1
- package/dist-lib/lib/floridayPush.functions.js +3 -3
- package/dist-lib/lib/flowJourney.d.ts +40 -0
- package/dist-lib/lib/flowJourney.d.ts.map +1 -0
- package/dist-lib/lib/flowJourney.js +156 -0
- package/dist-lib/lib/flowNodes.d.ts +3 -0
- package/dist-lib/lib/flowNodes.d.ts.map +1 -1
- package/dist-lib/lib/flowNodes.js +6 -0
- package/dist-lib/lib/flowRoute.d.ts +11 -1
- package/dist-lib/lib/flowRoute.d.ts.map +1 -1
- package/dist-lib/lib/flowRoute.js +343 -40
- package/dist-lib/lib/flowTree.d.ts +86 -1
- package/dist-lib/lib/flowTree.d.ts.map +1 -1
- package/dist-lib/lib/flowTree.js +391 -2
- package/dist-lib/lib/foutTekst.d.ts +4 -0
- package/dist-lib/lib/foutTekst.d.ts.map +1 -0
- package/dist-lib/lib/foutTekst.js +43 -0
- package/dist-lib/lib/handlers/logistics-tick.server.d.ts +2 -0
- package/dist-lib/lib/handlers/logistics-tick.server.d.ts.map +1 -1
- package/dist-lib/lib/handlers/logistics-tick.server.js +56 -0
- package/dist-lib/lib/handlers/logistics-tick.test.d.ts +2 -0
- package/dist-lib/lib/handlers/logistics-tick.test.d.ts.map +1 -0
- package/dist-lib/lib/handlers/logistics-tick.test.js +63 -0
- package/dist-lib/lib/loadCarrierPlanning.functions.d.ts +14965 -25
- package/dist-lib/lib/loadCarrierPlanning.functions.d.ts.map +1 -1
- package/dist-lib/lib/loadCarrierPlanning.functions.js +7 -7
- package/dist-lib/lib/loadCarrierPlanning.server.d.ts +24 -12
- package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -1
- package/dist-lib/lib/loadCarrierPlanning.server.js +490 -211
- package/dist-lib/lib/logisticsRecalcJob.functions.d.ts +5986 -10
- package/dist-lib/lib/logisticsRecalcJob.functions.d.ts.map +1 -1
- package/dist-lib/lib/logisticsRecalcJob.functions.js +4 -4
- package/dist-lib/lib/logisticsRecalcJob.server.d.ts.map +1 -1
- package/dist-lib/lib/logisticsRecalcJob.server.js +17 -2
- package/dist-lib/lib/logisticsRecalculate.functions.d.ts +3147 -159
- package/dist-lib/lib/logisticsRecalculate.functions.d.ts.map +1 -1
- package/dist-lib/lib/logisticsRecalculate.functions.js +2 -2
- package/dist-lib/lib/modulePrompts.d.ts.map +1 -1
- package/dist-lib/lib/modulePrompts.js +3436 -3325
- package/dist-lib/lib/nodeTrace.d.ts +4 -0
- package/dist-lib/lib/nodeTrace.d.ts.map +1 -1
- package/dist-lib/lib/nodeTrace.js +6 -15
- package/dist-lib/lib/planQueue.server.d.ts.map +1 -1
- package/dist-lib/lib/planQueue.server.js +24 -3
- package/dist-lib/lib/plannerDiff.d.ts +37 -0
- package/dist-lib/lib/plannerDiff.d.ts.map +1 -0
- package/dist-lib/lib/plannerDiff.functions.d.ts +30821 -0
- package/dist-lib/lib/plannerDiff.functions.d.ts.map +1 -0
- package/dist-lib/lib/plannerDiff.functions.js +21 -0
- package/dist-lib/lib/plannerDiff.js +74 -0
- package/dist-lib/lib/plannerDiff.server.d.ts +5 -0
- package/dist-lib/lib/plannerDiff.server.d.ts.map +1 -0
- package/dist-lib/lib/plannerDiff.server.js +60 -0
- package/dist-lib/lib/plannerDiff.test.d.ts +2 -0
- package/dist-lib/lib/plannerDiff.test.d.ts.map +1 -0
- package/dist-lib/lib/plannerDiff.test.js +41 -0
- package/dist-lib/lib/resolveBuffer.test.d.ts +2 -0
- package/dist-lib/lib/resolveBuffer.test.d.ts.map +1 -0
- package/dist-lib/lib/resolveBuffer.test.js +38 -0
- package/dist-lib/lib/ruleEngineSchema.d.ts.map +1 -1
- package/dist-lib/lib/ruleEngineSchema.js +52 -0
- package/dist-lib/lib/stopReden.d.ts +43 -0
- package/dist-lib/lib/stopReden.d.ts.map +1 -0
- package/dist-lib/lib/stopReden.js +197 -0
- package/dist-lib/lib/taskCounts.d.ts +2 -0
- package/dist-lib/lib/taskCounts.d.ts.map +1 -0
- package/dist-lib/lib/taskCounts.js +15 -0
- package/dist-lib/lib/taskCounts.test.d.ts +2 -0
- package/dist-lib/lib/taskCounts.test.d.ts.map +1 -0
- package/dist-lib/lib/taskCounts.test.js +12 -0
- package/dist-lib/lib/trace.server.d.ts.map +1 -1
- package/dist-lib/lib/trace.server.js +3 -0
- package/dist-lib/lib/vakUitleg.d.ts.map +1 -1
- package/dist-lib/lib/vakUitleg.js +2 -1
- package/package.json +12 -9
|
@@ -12,124 +12,40 @@ import { buildDimensionItems, carrierDimensions } from "./dimensionData.server";
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
// Buffer
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (Array.isArray(value)) {
|
|
19
|
-
for (const entry of value) {
|
|
20
|
-
const normalized = normalizeBufferPart(entry);
|
|
21
|
-
if (normalized)
|
|
22
|
-
return normalized;
|
|
23
|
-
}
|
|
24
|
-
return "";
|
|
25
|
-
}
|
|
26
|
-
if (typeof value === "object") {
|
|
27
|
-
const o = value;
|
|
28
|
-
return normalizeBufferPart(o.gln ?? o.locationGln ?? o.warehouseId ?? o.id ?? o.name ?? o.location ?? o.address?.addressLine);
|
|
29
|
-
}
|
|
30
|
-
return String(value).trim().toLowerCase().replace(/\s+/g, " ");
|
|
31
|
-
}
|
|
32
|
-
function pickFirstString(...values) {
|
|
33
|
-
for (const value of values) {
|
|
34
|
-
const normalized = normalizeBufferPart(value);
|
|
35
|
-
if (normalized)
|
|
36
|
-
return normalized;
|
|
37
|
-
}
|
|
38
|
-
return "";
|
|
39
|
-
}
|
|
40
|
-
export function buildBufferKey(order, mode) {
|
|
41
|
-
const cf = (order.custom_fields ?? {});
|
|
42
|
-
const pickupDate = pickFirstString(cf.pickup_moment, cf.pickup_date, cf.pickup_datetime, order.delivery_date);
|
|
43
|
-
const location = pickFirstString(cf.delivery_location_gln, cf.destination_gln, cf.region_gln, cf.location_gln, cf.delivery_location_id, cf.delivery_location);
|
|
44
|
-
const customer = normalizeBufferPart(order.customer_id);
|
|
45
|
-
let key;
|
|
46
|
-
switch (mode) {
|
|
47
|
-
case "same_pickup":
|
|
48
|
-
key = `p|${pickupDate}`;
|
|
49
|
-
break;
|
|
50
|
-
case "same_location":
|
|
51
|
-
key = `l|${location}`;
|
|
52
|
-
break;
|
|
53
|
-
case "same_customer":
|
|
54
|
-
key = `c|${customer}|${pickupDate}`;
|
|
55
|
-
break;
|
|
56
|
-
case "never":
|
|
57
|
-
key = `order-${order.id}`;
|
|
58
|
-
break;
|
|
59
|
-
case "same_pickup_and_location":
|
|
60
|
-
default:
|
|
61
|
-
key = `pl|${pickupDate}|${location}`;
|
|
62
|
-
}
|
|
63
|
-
return { key, pickupDate, location };
|
|
64
|
-
}
|
|
15
|
+
// De bufferkeuze is uitsluitend van de database (logistics_apply_buffer_decision,
|
|
16
|
+
// via trigger trg_orders_apply_buffer_decision). Deze code leest die keuze
|
|
17
|
+
// alleen; ze berekent geen eigen sleutel en schrijft nooit orders.buffer_id.
|
|
65
18
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
66
19
|
export async function resolveBufferForOrderInternal(sb, orderId) {
|
|
67
|
-
const
|
|
68
|
-
.from("orders")
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
.
|
|
72
|
-
|
|
20
|
+
const readBuffer = async () => {
|
|
21
|
+
const { data } = await sb.from("orders").select("id, buffer_id").eq("id", orderId).maybeSingle();
|
|
22
|
+
if (!data)
|
|
23
|
+
return { found: false, bufferId: null };
|
|
24
|
+
return { found: true, bufferId: data.buffer_id ?? null };
|
|
25
|
+
};
|
|
26
|
+
const first = await readBuffer();
|
|
27
|
+
if (!first.found)
|
|
28
|
+
return null;
|
|
29
|
+
if (first.bufferId)
|
|
30
|
+
return first.bufferId;
|
|
31
|
+
// Nog geen buffer: laat de database één keer beslissen, en lees opnieuw.
|
|
32
|
+
const { error } = await sb.rpc("logistics_apply_buffer_decision", { p_order_id: orderId });
|
|
33
|
+
if (error) {
|
|
34
|
+
console.warn(`[buffer] bufferbesluit voor order ${orderId} mislukt: ${error.message}`);
|
|
73
35
|
return null;
|
|
74
|
-
const { data: settings } = await sb
|
|
75
|
-
.from("cart_planning_settings")
|
|
76
|
-
.select("buffer_grouping_mode")
|
|
77
|
-
.eq("id", 1)
|
|
78
|
-
.maybeSingle();
|
|
79
|
-
const mode = settings?.buffer_grouping_mode ?? "same_pickup_and_location";
|
|
80
|
-
const cf = { ...(order.custom_fields ?? {}) };
|
|
81
|
-
const locId = cf.delivery_location_id;
|
|
82
|
-
if (typeof locId === "string" && UUID_RE.test(locId) && !cf.delivery_location_gln) {
|
|
83
|
-
const { data: loc } = await sb
|
|
84
|
-
.from("customer_delivery_locations")
|
|
85
|
-
.select("gln, name")
|
|
86
|
-
.eq("id", locId)
|
|
87
|
-
.maybeSingle();
|
|
88
|
-
if (loc?.gln)
|
|
89
|
-
cf.delivery_location_gln = loc.gln;
|
|
90
|
-
if (loc?.name)
|
|
91
|
-
cf.delivery_location_name = loc.name;
|
|
92
|
-
}
|
|
93
|
-
const { key, pickupDate, location } = buildBufferKey({ ...order, custom_fields: cf }, mode);
|
|
94
|
-
let bufferId = null;
|
|
95
|
-
const { data: existing } = await sb
|
|
96
|
-
.from("order_buffers")
|
|
97
|
-
.select("id")
|
|
98
|
-
.eq("buffer_key", key)
|
|
99
|
-
.maybeSingle();
|
|
100
|
-
if (existing?.id) {
|
|
101
|
-
bufferId = existing.id;
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
const { data: inserted, error } = await sb
|
|
105
|
-
.from("order_buffers")
|
|
106
|
-
.insert({
|
|
107
|
-
buffer_key: key,
|
|
108
|
-
grouping_mode: mode,
|
|
109
|
-
pickup_date: pickupDate || null,
|
|
110
|
-
pickup_location: location || null,
|
|
111
|
-
customer_id: mode === "same_customer" ? order.customer_id : null,
|
|
112
|
-
})
|
|
113
|
-
.select("id")
|
|
114
|
-
.single();
|
|
115
|
-
if (error) {
|
|
116
|
-
// Race: iemand anders maakte dezelfde buffer aan.
|
|
117
|
-
const { data: retry } = await sb
|
|
118
|
-
.from("order_buffers")
|
|
119
|
-
.select("id")
|
|
120
|
-
.eq("buffer_key", key)
|
|
121
|
-
.maybeSingle();
|
|
122
|
-
bufferId = retry?.id ?? null;
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
bufferId = inserted?.id ?? null;
|
|
126
|
-
}
|
|
127
36
|
}
|
|
128
|
-
|
|
129
|
-
|
|
37
|
+
const second = await readBuffer();
|
|
38
|
+
if (!second.bufferId) {
|
|
39
|
+
console.warn(`[buffer] order ${orderId} heeft geen buffer volgens de werkwijze (buffer uit of reservering uitgesloten); niet ingedeeld.`);
|
|
130
40
|
}
|
|
131
|
-
return bufferId;
|
|
41
|
+
return second.bufferId;
|
|
132
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Een belading die uit Floriday komt (per regel of, als noodgreep, order-breed) of
|
|
45
|
+
* die een mens bewust heeft vastgezet, is leidend: het aantal lagen per ladingdrager
|
|
46
|
+
* wordt dan exact gevolgd.
|
|
47
|
+
*/
|
|
48
|
+
const isAuthoritative = (s) => s === "handmatig" || s === "floriday_line" || s === "order_items" || s === "floriday_packing";
|
|
133
49
|
const positive = (v) => {
|
|
134
50
|
const n = Number(v);
|
|
135
51
|
return Number.isFinite(n) && n > 0 ? Math.round(n) : null;
|
|
@@ -151,17 +67,58 @@ export function beladingFromFloridayPacking(packing) {
|
|
|
151
67
|
return null;
|
|
152
68
|
return { plants_per_crate: ppc, crates_per_layer: cpl, layers_per_cart: lpc };
|
|
153
69
|
}
|
|
154
|
-
/**
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Leest per artikel de Floriday-verpakking van de orderregel zelf uit
|
|
72
|
+
* `orders.custom_fields.floriday_lines`. Dit is de enige bron die per regel klopt;
|
|
73
|
+
* `custom_fields.floriday_packing` geldt voor de hele order.
|
|
74
|
+
*/
|
|
75
|
+
export function beladingPerRegelUitOrder(customFields) {
|
|
76
|
+
const out = new Map();
|
|
77
|
+
const cf = (customFields ?? {});
|
|
78
|
+
const lines = Array.isArray(cf.floriday_lines) ? cf.floriday_lines : [];
|
|
79
|
+
for (const raw of lines) {
|
|
80
|
+
const line = (raw ?? {});
|
|
81
|
+
const tradeItemId = line.trade_item_id ? String(line.trade_item_id) : null;
|
|
82
|
+
if (!tradeItemId)
|
|
83
|
+
continue;
|
|
84
|
+
const belading = beladingFromFloridayPacking(line.packing);
|
|
85
|
+
if (belading)
|
|
86
|
+
out.set(tradeItemId, belading);
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
/** Waarden van de orderregel zelf, of null als de regel niets heeft. */
|
|
91
|
+
function beladingVanRegel(row) {
|
|
92
|
+
if (positive(row.plants_per_crate) == null &&
|
|
93
|
+
positive(row.crates_per_layer) == null &&
|
|
94
|
+
positive(row.layers_per_cart) == null) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
plants_per_crate: positive(row.plants_per_crate) ?? 1,
|
|
99
|
+
crates_per_layer: positive(row.crates_per_layer) ?? 1,
|
|
100
|
+
layers_per_cart: positive(row.layers_per_cart) ?? 1,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Beladingbronnen in vaste volgorde:
|
|
105
|
+
* 1. handmatig vastgezette regel (packing_locked_at);
|
|
106
|
+
* 2. de Floriday-verpakking van díe regel (op artikel gekoppeld);
|
|
107
|
+
* 3. de eigen waarden van de bestelregel;
|
|
108
|
+
* 4. het product;
|
|
109
|
+
* 5. de order-brede Floriday-verpakking — uitsluitend als allerlaatste noodgreep
|
|
110
|
+
* voor een regel die van 1 t/m 4 niets heeft;
|
|
111
|
+
* 6. anders onbekend: er wordt niet gegokt, de indeling blokkeert.
|
|
112
|
+
*/
|
|
113
|
+
function resolveBelading(row, packing, linePacking = null) {
|
|
114
|
+
const fromRow = beladingVanRegel(row);
|
|
115
|
+
// 1. Handmatig vastgezet: niemand overschrijft dat, ook Floriday niet.
|
|
116
|
+
if (row.packing_locked_at && fromRow)
|
|
117
|
+
return { belading: fromRow, source: "handmatig" };
|
|
118
|
+
// 2. De Floriday-verpakking van deze regel.
|
|
119
|
+
if (linePacking)
|
|
120
|
+
return { belading: linePacking, source: "floriday_line" };
|
|
121
|
+
// 3. De eigen waarden van de bestelregel.
|
|
165
122
|
if (fromRow)
|
|
166
123
|
return { belading: fromRow, source: "order_items" };
|
|
167
124
|
const p = row.products ?? null;
|
|
@@ -172,8 +129,13 @@ function resolveBelading(row, packing) {
|
|
|
172
129
|
layers_per_cart: positive(p.layers_per_cart) ?? 1,
|
|
173
130
|
}
|
|
174
131
|
: null;
|
|
132
|
+
// 4. Het product.
|
|
175
133
|
if (fromProduct)
|
|
176
134
|
return { belading: fromProduct, source: "product" };
|
|
135
|
+
// 5. Allerlaatste noodgreep: de order-brede Floriday-verpakking.
|
|
136
|
+
if (packing)
|
|
137
|
+
return { belading: packing, source: "floriday_packing" };
|
|
138
|
+
// 6. Onbekend.
|
|
177
139
|
return { belading: { plants_per_crate: 1, crates_per_layer: 1, layers_per_cart: 1 }, source: "geen" };
|
|
178
140
|
}
|
|
179
141
|
export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
|
|
@@ -197,8 +159,15 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
|
|
|
197
159
|
groupSet = new Set((groups ?? []).map((g) => g.id));
|
|
198
160
|
}
|
|
199
161
|
for (const row of rows) {
|
|
200
|
-
const
|
|
201
|
-
const
|
|
162
|
+
const orderKey = row.order_id ? String(row.order_id) : null;
|
|
163
|
+
const packing = orderKey ? options.packingByOrder?.get(orderKey) ?? null : null;
|
|
164
|
+
const lineMap = orderKey ? options.packingByOrderLine?.get(orderKey) ?? null : null;
|
|
165
|
+
const linePacking = lineMap
|
|
166
|
+
? lineMap.get(String(row.product_id ?? "")) ??
|
|
167
|
+
lineMap.get(String(row.products?.floriday_trade_item_id ?? "")) ??
|
|
168
|
+
null
|
|
169
|
+
: null;
|
|
170
|
+
const { belading: base, source } = resolveBelading(row, packing, linePacking);
|
|
202
171
|
const plants = Math.max(0, Number(row.quantity ?? 0));
|
|
203
172
|
const groupNameKey = String(row.product_name ?? "")
|
|
204
173
|
.replace(/^\[Groep\]\s*/i, "")
|
|
@@ -218,7 +187,7 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
|
|
|
218
187
|
product_name: row.product_name ?? "Onbekend",
|
|
219
188
|
plants,
|
|
220
189
|
...base,
|
|
221
|
-
packing_authoritative: source
|
|
190
|
+
packing_authoritative: isAuthoritative(source),
|
|
222
191
|
weight_per_crate: row.products?.weight != null ? Number(row.products.weight) : null,
|
|
223
192
|
});
|
|
224
193
|
continue;
|
|
@@ -241,7 +210,7 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
|
|
|
241
210
|
product_name: row.product_name ?? "Onbekend",
|
|
242
211
|
plants,
|
|
243
212
|
...base,
|
|
244
|
-
packing_authoritative: source
|
|
213
|
+
packing_authoritative: isAuthoritative(source),
|
|
245
214
|
weight_per_crate: null,
|
|
246
215
|
});
|
|
247
216
|
continue;
|
|
@@ -259,21 +228,36 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
|
|
|
259
228
|
}
|
|
260
229
|
const resolved = memberList.map((m) => {
|
|
261
230
|
const p = prodByTrade.get(String(m.trade_item_id));
|
|
262
|
-
// Een
|
|
263
|
-
if (
|
|
264
|
-
return { m, p, belading: { ...base }, source
|
|
231
|
+
// Een handmatig vastgezette regel blijft leidend voor de hele groep.
|
|
232
|
+
if (source === "handmatig") {
|
|
233
|
+
return { m, p, belading: { ...base }, source };
|
|
234
|
+
}
|
|
235
|
+
// 1. De Floriday-verpakking van dít lid.
|
|
236
|
+
const eigenLijn = lineMap?.get(String(m.trade_item_id)) ?? (p ? lineMap?.get(String(p.id)) ?? null : null);
|
|
237
|
+
if (eigenLijn)
|
|
238
|
+
return { m, p, belading: { ...eigenLijn }, source: "floriday_line" };
|
|
239
|
+
// 2. Het artikel zelf. Nooit de belading van de groepsregel of van een ander lid.
|
|
265
240
|
const known = p &&
|
|
266
241
|
(positive(p.plants_per_crate) != null ||
|
|
267
242
|
positive(p.crates_per_layer) != null ||
|
|
268
243
|
positive(p.layers_per_cart) != null);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
244
|
+
if (known) {
|
|
245
|
+
return {
|
|
246
|
+
m,
|
|
247
|
+
p,
|
|
248
|
+
belading: {
|
|
249
|
+
plants_per_crate: positive(p.plants_per_crate) ?? 1,
|
|
250
|
+
crates_per_layer: positive(p.crates_per_layer) ?? 1,
|
|
251
|
+
layers_per_cart: positive(p.layers_per_cart) ?? 1,
|
|
252
|
+
},
|
|
253
|
+
source: "product",
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// 3. Allerlaatste noodgreep: de order-brede Floriday-verpakking van de order.
|
|
257
|
+
if (packing)
|
|
258
|
+
return { m, p, belading: { ...packing }, source: "floriday_packing" };
|
|
259
|
+
// 4. Onbekend: er wordt niet gegokt, de indeling blokkeert.
|
|
260
|
+
return { m, p, belading: { ...base }, source: "geen" };
|
|
277
261
|
});
|
|
278
262
|
const weightSum = resolved.reduce((s, r) => s + Number(r.m.quantity ?? 1), 0) || resolved.length;
|
|
279
263
|
const shares = [];
|
|
@@ -302,7 +286,7 @@ export async function expandOrderItemsForPlanner(sb, rows, options = {}) {
|
|
|
302
286
|
product_name: `${row.product_name ?? "Groep"} — ${r.p?.product ?? r.m.trade_item_id}`,
|
|
303
287
|
plants: shares[idx],
|
|
304
288
|
...r.belading,
|
|
305
|
-
packing_authoritative: r.source
|
|
289
|
+
packing_authoritative: isAuthoritative(r.source),
|
|
306
290
|
weight_per_crate: r.p?.weight != null ? Number(r.p.weight) : null,
|
|
307
291
|
});
|
|
308
292
|
});
|
|
@@ -569,6 +553,53 @@ export function dimensionCartsToPlannedUnits(carts) {
|
|
|
569
553
|
// ---------------------------------------------------------------------------
|
|
570
554
|
/** Maximaal aantal ladingdragers dat één orderbuffer mag opleveren. */
|
|
571
555
|
const MAX_CARTS_PER_BUFFER = 500;
|
|
556
|
+
/** Statussen waarbij een order niet meer ingedeeld hoeft te worden. */
|
|
557
|
+
const TERMINALE_STATUSSEN = new Set([
|
|
558
|
+
"verzonden",
|
|
559
|
+
"afgehandeld",
|
|
560
|
+
"geannuleerd",
|
|
561
|
+
"cancelled",
|
|
562
|
+
"geleverd",
|
|
563
|
+
"afgerond",
|
|
564
|
+
]);
|
|
565
|
+
/** Reservering of al afgehandelde order: niet plannen, ook niet blokkeren. */
|
|
566
|
+
function isReservationOrTerminal(order) {
|
|
567
|
+
if (!order)
|
|
568
|
+
return true;
|
|
569
|
+
if (order.is_reservation === true)
|
|
570
|
+
return true;
|
|
571
|
+
const status = String(order.status ?? "").trim().toLowerCase();
|
|
572
|
+
return TERMINALE_STATUSSEN.has(status);
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Duidelijk teveel identieke bestelregels wijst op een synchronisatie die
|
|
576
|
+
* regels toevoegt in plaats van bijwerkt. Dan niet rekenen: een indeling op
|
|
577
|
+
* opgeblazen aantallen is fout én loopt in een timeout.
|
|
578
|
+
*/
|
|
579
|
+
const DUPES_MIN_REGELS = 50;
|
|
580
|
+
const DUPES_FACTOR = 3;
|
|
581
|
+
function telDubbeleRegels(rows) {
|
|
582
|
+
const groepen = new Map();
|
|
583
|
+
const producten = new Set();
|
|
584
|
+
for (const r of rows) {
|
|
585
|
+
const product = String(r["product_id"] ?? r["product_name"] ?? "");
|
|
586
|
+
producten.add(product);
|
|
587
|
+
const sleutel = [
|
|
588
|
+
product,
|
|
589
|
+
r["quantity"] ?? 0,
|
|
590
|
+
r["plants_per_crate"] ?? 0,
|
|
591
|
+
r["crates_per_layer"] ?? 0,
|
|
592
|
+
r["layers_per_cart"] ?? 0,
|
|
593
|
+
].join("|");
|
|
594
|
+
groepen.set(sleutel, (groepen.get(sleutel) ?? 0) + 1);
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
regels: rows.length,
|
|
598
|
+
uniek: groepen.size,
|
|
599
|
+
kopieen: rows.length - groepen.size,
|
|
600
|
+
producten: producten.size,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
572
603
|
/**
|
|
573
604
|
* Bepaalt de definitieve, persistente identiteit van iedere fysieke kar.
|
|
574
605
|
* Bij een bufferorder met meerdere orders zijn alle karren gedeelde
|
|
@@ -662,7 +693,62 @@ export async function bufferOrdersWithoutCarts(sb, bufferId, orderIds) {
|
|
|
662
693
|
}
|
|
663
694
|
return ids.filter((id) => !withCarts.has(id));
|
|
664
695
|
}
|
|
696
|
+
/**
|
|
697
|
+
* Auditpunt 2: één buffer wordt nooit door twee processen tegelijk herschreven.
|
|
698
|
+
*
|
|
699
|
+
* De lees-verwijder-herschrijf-cyclus hieronder loopt over veel losse aanroepen
|
|
700
|
+
* en past dus niet in één databasetransactie; een advisory xact lock zou meteen
|
|
701
|
+
* na de eerste aanroep weer vrijvallen. Daarom een kortlopende lease met
|
|
702
|
+
* vervaltijd (logistics_plan_lock_acquire), die wél geldt over de planwachtrij,
|
|
703
|
+
* de massa-herberekening en een openstaand scherm heen.
|
|
704
|
+
*
|
|
705
|
+
* Lukt de vergrendeling niet, dan volgt een nette "wordt al bijgewerkt"-uitkomst
|
|
706
|
+
* in plaats van een tweede, botsende herberekening.
|
|
707
|
+
*/
|
|
665
708
|
export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
709
|
+
// Een proefberekening schrijft niets weg en hoeft dus niets te vergrendelen.
|
|
710
|
+
if (options.dryRun)
|
|
711
|
+
return replanBufferUnlocked(sb, bufferId, options);
|
|
712
|
+
const lockKey = `buffer:${bufferId}`;
|
|
713
|
+
let held = false;
|
|
714
|
+
try {
|
|
715
|
+
const { data, error } = await sb.rpc("logistics_plan_lock_acquire", {
|
|
716
|
+
_key: lockKey,
|
|
717
|
+
_owner: "logistiek-engine",
|
|
718
|
+
_ttl_seconds: 600,
|
|
719
|
+
});
|
|
720
|
+
held = !error && data === true;
|
|
721
|
+
}
|
|
722
|
+
catch {
|
|
723
|
+
held = false;
|
|
724
|
+
}
|
|
725
|
+
if (!held) {
|
|
726
|
+
return {
|
|
727
|
+
ok: true,
|
|
728
|
+
bufferId,
|
|
729
|
+
orderIds: [],
|
|
730
|
+
cartsCreated: 0,
|
|
731
|
+
replacedManualCount: 0,
|
|
732
|
+
usedFloridayPacking: false,
|
|
733
|
+
warnings: [BUFFER_BUSY_MESSAGE],
|
|
734
|
+
skipped: "locked",
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
try {
|
|
738
|
+
return await replanBufferUnlocked(sb, bufferId, options);
|
|
739
|
+
}
|
|
740
|
+
finally {
|
|
741
|
+
try {
|
|
742
|
+
await sb.rpc("logistics_plan_lock_release", { _key: lockKey, _owner: "logistiek-engine" });
|
|
743
|
+
}
|
|
744
|
+
catch {
|
|
745
|
+
// Vrijgeven mag nooit de uitkomst omgooien; de lease verloopt vanzelf.
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
/** Herbruikbare melding als een buffer al door een ander proces wordt bijgewerkt. */
|
|
750
|
+
export const BUFFER_BUSY_MESSAGE = "Deze orderbuffer wordt op dit moment al bijgewerkt; probeer het zo opnieuw.";
|
|
751
|
+
async function replanBufferUnlocked(sb, bufferId, options = {}) {
|
|
666
752
|
const warnings = [];
|
|
667
753
|
// Bevroren karren blijven staan; al het overige van de héle buffer wordt
|
|
668
754
|
// samen ingedeeld, zodat orders van de buffer op dezelfde karren komen.
|
|
@@ -670,11 +756,17 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
670
756
|
const isSupplement = keepCartIds.length > 0;
|
|
671
757
|
const { data: orders } = await sb
|
|
672
758
|
.from("orders")
|
|
673
|
-
.select("id, order_number, customer_id, custom_fields, plan_locked_at")
|
|
759
|
+
.select("id, order_number, customer_id, custom_fields, plan_locked_at, status, is_reservation")
|
|
674
760
|
.eq("buffer_id", bufferId)
|
|
675
761
|
.is("deleted_at", null);
|
|
676
762
|
const allOrderRows = Array.isArray(orders) ? orders : [];
|
|
677
|
-
|
|
763
|
+
// Reserveringen en al afgeronde orders horen niet in de indeling thuis. Ze
|
|
764
|
+
// leveren nooit karren op en zouden anders de hele buffer laten blokkeren.
|
|
765
|
+
const orderRows = allOrderRows.filter((o) => !isReservationOrTerminal(o));
|
|
766
|
+
const overgeslagen = allOrderRows.length - orderRows.length;
|
|
767
|
+
if (overgeslagen > 0) {
|
|
768
|
+
warnings.push(`${overgeslagen} order(s) in deze buffer zijn overgeslagen: reservering of al afgehandeld.`);
|
|
769
|
+
}
|
|
678
770
|
const orderIds = orderRows.map((o) => o.id);
|
|
679
771
|
// Binnen een bufferorder mogen orders en klanten altijd samen op een kar.
|
|
680
772
|
const isBufferOrder = orderRows.length > 1;
|
|
@@ -777,22 +869,86 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
777
869
|
const { data: items } = await sb
|
|
778
870
|
.from("order_items")
|
|
779
871
|
.select("id, order_id, product_id, product_name, quantity, plants_per_crate, crates_per_layer, layers_per_cart, " +
|
|
780
|
-
"
|
|
872
|
+
"packing_locked_at, packing_locked_by, " +
|
|
873
|
+
"products(plants_per_crate, crates_per_layer, layers_per_cart, weight, floriday_trade_item_id)")
|
|
781
874
|
.in("order_id", idList);
|
|
875
|
+
// Duidelijk teveel identieke bestelregels: niet rekenen maar blokkeren.
|
|
876
|
+
// Een indeling op opgeblazen aantallen is fout en loopt in een timeout.
|
|
877
|
+
{
|
|
878
|
+
const perOrder = new Map();
|
|
879
|
+
for (const it of Array.isArray(items) ? items : []) {
|
|
880
|
+
const id = String(it.order_id ?? "");
|
|
881
|
+
const lijst = perOrder.get(id) ?? [];
|
|
882
|
+
lijst.push(it);
|
|
883
|
+
perOrder.set(id, lijst);
|
|
884
|
+
}
|
|
885
|
+
const verdacht = [];
|
|
886
|
+
for (const [id, rijen] of perOrder) {
|
|
887
|
+
const telling = telDubbeleRegels(rijen);
|
|
888
|
+
if (telling.regels >= DUPES_MIN_REGELS && telling.regels > telling.uniek * DUPES_FACTOR) {
|
|
889
|
+
const order = orderRows.find((o) => String(o.id) === id);
|
|
890
|
+
verdacht.push({ orderId: id, orderRef: String(order?.order_number ?? id), telling });
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (verdacht.length > 0) {
|
|
894
|
+
const reason = "Dubbele bestelregels gevonden: " +
|
|
895
|
+
verdacht
|
|
896
|
+
.map((v) => `${v.orderRef} heeft ${v.telling.regels} bestelregels voor ${v.telling.producten} product(en) ` +
|
|
897
|
+
`(${v.telling.kopieen} kopie(ën))`)
|
|
898
|
+
.join("; ") +
|
|
899
|
+
". Er is niets ingedeeld; de kopieën moeten eerst worden opgeruimd en de bron ervan hersteld.";
|
|
900
|
+
console.error(`[logistiek-engine] buffer ${bufferId} geblokkeerd: ${reason}`);
|
|
901
|
+
if (!options.dryRun) {
|
|
902
|
+
const { error: blockError } = await sb.rpc("logistics_plan_block", {
|
|
903
|
+
p_order_ids: orderIds,
|
|
904
|
+
p_reason: reason,
|
|
905
|
+
p_details: {
|
|
906
|
+
reason_code: "dubbele_regels",
|
|
907
|
+
bufferId,
|
|
908
|
+
path: "typescript",
|
|
909
|
+
orders: verdacht.map((v) => ({ orderRef: v.orderRef, ...v.telling })),
|
|
910
|
+
},
|
|
911
|
+
});
|
|
912
|
+
if (blockError)
|
|
913
|
+
warnings.push(`Blokkade kon niet worden vastgelegd: ${blockError.message}`);
|
|
914
|
+
}
|
|
915
|
+
return {
|
|
916
|
+
ok: false,
|
|
917
|
+
bufferId,
|
|
918
|
+
orderIds,
|
|
919
|
+
cartsCreated: 0,
|
|
920
|
+
replacedManualCount: 0,
|
|
921
|
+
usedFloridayPacking: false,
|
|
922
|
+
warnings: [...warnings, reason],
|
|
923
|
+
skipped: "blocked",
|
|
924
|
+
blockedReason: reason,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
}
|
|
782
928
|
const customerByOrder = new Map(orderRows.map((o) => [o.id, o.customer_id ?? null]));
|
|
783
929
|
const rawRows = (Array.isArray(items) ? items : []).map((it) => ({
|
|
784
930
|
...it,
|
|
785
931
|
customer_id: customerByOrder.get(it.order_id) ?? null,
|
|
786
932
|
}));
|
|
787
|
-
// Floriday-verpakking
|
|
933
|
+
// Order-brede Floriday-verpakking: alleen nog als allerlaatste noodgreep.
|
|
788
934
|
const packingByOrder = new Map();
|
|
935
|
+
// Floriday-verpakking per orderregel, gekoppeld op artikel. Dit is de bron die
|
|
936
|
+
// per regel klopt en gaat voor op de order-brede waarde.
|
|
937
|
+
const packingByOrderLine = new Map();
|
|
789
938
|
for (const o of orderRows) {
|
|
790
939
|
const belading = beladingFromFloridayPacking(o.custom_fields?.floriday_packing);
|
|
791
940
|
if (belading)
|
|
792
941
|
packingByOrder.set(String(o.id), belading);
|
|
942
|
+
const perRegel = beladingPerRegelUitOrder(o.custom_fields);
|
|
943
|
+
if (perRegel.size > 0)
|
|
944
|
+
packingByOrderLine.set(String(o.id), perRegel);
|
|
793
945
|
}
|
|
794
946
|
const sources = new Map();
|
|
795
|
-
let inputs = await expandOrderItemsForPlanner(sb, rawRows, {
|
|
947
|
+
let inputs = await expandOrderItemsForPlanner(sb, rawRows, {
|
|
948
|
+
packingByOrder,
|
|
949
|
+
packingByOrderLine,
|
|
950
|
+
sources,
|
|
951
|
+
});
|
|
796
952
|
// Wat al op een bevroren kar ligt, wordt niet opnieuw ingedeeld.
|
|
797
953
|
let frozenCartCount = 0;
|
|
798
954
|
let cartNumberStart = 1;
|
|
@@ -922,8 +1078,17 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
922
1078
|
// daarom verwijderd.)
|
|
923
1079
|
const solo = orderRows.length === 1 ? orderRows[0] : null;
|
|
924
1080
|
const soloPacking = solo ? beladingFromFloridayPacking(solo.custom_fields?.floriday_packing) : null;
|
|
1081
|
+
// Het overname-pad legt één verpakking over de hele order. Dat mag alleen als
|
|
1082
|
+
// elke regel toch al exact die verpakking heeft; anders rekent de gewone
|
|
1083
|
+
// planner, die per regel met de eigen belading werkt.
|
|
1084
|
+
const uniformeBelading = inputs.length > 0 &&
|
|
1085
|
+
soloPacking != null &&
|
|
1086
|
+
inputs.every((i) => i.plants_per_crate === soloPacking.plants_per_crate &&
|
|
1087
|
+
i.crates_per_layer === soloPacking.crates_per_layer &&
|
|
1088
|
+
i.layers_per_cart === soloPacking.layers_per_cart);
|
|
925
1089
|
const takeoverProfile = profileFor(profiles, options.carrierType ?? defaultProfile.code);
|
|
926
|
-
const takeoverAllowed = takeoverProfile.capacity_model === "layered" || takeoverProfile.capacity_model === "stacked"
|
|
1090
|
+
const takeoverAllowed = (takeoverProfile.capacity_model === "layered" || takeoverProfile.capacity_model === "stacked") &&
|
|
1091
|
+
uniformeBelading;
|
|
927
1092
|
if (useDimensions) {
|
|
928
1093
|
// Globale methode: indelen op basis van fysieke afmetingen.
|
|
929
1094
|
const carrierRow = profiles.find((p) => p.code === (options.carrierType ?? defaultProfile.code)) ?? null;
|
|
@@ -1047,8 +1212,13 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
1047
1212
|
}
|
|
1048
1213
|
// Veiligheidsgrens: een onrealistisch grote uitkomst wordt niet afgekapt maar
|
|
1049
1214
|
// geblokkeerd. Een half plan laat planten stilzwijgend uit de pickstroom vallen.
|
|
1050
|
-
|
|
1051
|
-
|
|
1215
|
+
// Auditpunt 2: de grens telt cumulatief. Bij aanvullen na bevroren karren
|
|
1216
|
+
// tellen die bevroren karren mee, zodat de grens niet in kleine stapjes
|
|
1217
|
+
// omzeild kan worden.
|
|
1218
|
+
const totalCartsAfterPlan = plannedCarts.length + frozenCartCount;
|
|
1219
|
+
if (totalCartsAfterPlan > MAX_CARTS_PER_BUFFER) {
|
|
1220
|
+
const reason = `Deze orderbuffer komt uit op ${totalCartsAfterPlan} ladingdragers ` +
|
|
1221
|
+
`(${frozenCartCount} al vastgelegd + ${plannedCarts.length} nieuw), meer dan de grens van ` +
|
|
1052
1222
|
`${MAX_CARTS_PER_BUFFER}. Er is niets ingedeeld; controleer de aantallen of de belading van de producten.`;
|
|
1053
1223
|
console.error(`[logistiek-engine] buffer ${bufferId} geblokkeerd: ${reason}`);
|
|
1054
1224
|
if (!options.dryRun) {
|
|
@@ -1059,7 +1229,9 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
1059
1229
|
reason_code: "kargrens_overschreden",
|
|
1060
1230
|
bufferId,
|
|
1061
1231
|
path: "typescript",
|
|
1062
|
-
carts:
|
|
1232
|
+
carts: totalCartsAfterPlan,
|
|
1233
|
+
newCarts: plannedCarts.length,
|
|
1234
|
+
frozenCarts: frozenCartCount,
|
|
1063
1235
|
limit: MAX_CARTS_PER_BUFFER,
|
|
1064
1236
|
beladingSources,
|
|
1065
1237
|
},
|
|
@@ -1166,37 +1338,167 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
1166
1338
|
await del().in("order_id", ids);
|
|
1167
1339
|
}
|
|
1168
1340
|
};
|
|
1169
|
-
|
|
1170
|
-
//
|
|
1171
|
-
//
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1341
|
+
// ---------------------------------------------------------------------------
|
|
1342
|
+
// Vergelijken in plaats van wissen. Een kar die exact hetzelfde blijft, blijft
|
|
1343
|
+
// staan: zelfde rij, zelfde id, zelfde barcode. Alleen echte verschillen worden
|
|
1344
|
+
// geschreven. Dit voorkomt dat elke herberekening honderdduizenden karren en
|
|
1345
|
+
// logboekregels produceert.
|
|
1346
|
+
// ---------------------------------------------------------------------------
|
|
1347
|
+
const getal = (v) => v === null || v === undefined || v === "" ? "" : Number.isFinite(Number(v)) ? Number(v).toFixed(3) : String(v);
|
|
1348
|
+
const itemSig = (r) => [
|
|
1349
|
+
String(r["order_item_id"] ?? ""),
|
|
1350
|
+
String(r["product_id"] ?? ""),
|
|
1351
|
+
getal(r["crates"]),
|
|
1352
|
+
getal(r["boxes"]),
|
|
1353
|
+
getal(r["weight_kg"]),
|
|
1354
|
+
getal(r["plants_per_crate"]),
|
|
1355
|
+
getal(r["crates_per_layer"]),
|
|
1356
|
+
getal(r["layers_per_cart"]),
|
|
1357
|
+
getal(r["layer_fraction"]),
|
|
1358
|
+
getal(r["layer_share_pct"]),
|
|
1359
|
+
getal(r["layer_index"]),
|
|
1360
|
+
getal(r["crates_in_layer"]),
|
|
1361
|
+
].join("|");
|
|
1362
|
+
const karSig = (row, items) => [
|
|
1363
|
+
String(row["order_id"] ?? "gedeeld"),
|
|
1364
|
+
getal(row["cart_number"]),
|
|
1365
|
+
getal(row["total_carts"]),
|
|
1366
|
+
String(row["carrier_type"] ?? ""),
|
|
1367
|
+
String(row["carrier_type_id"] ?? ""),
|
|
1368
|
+
getal(row["max_layers"]),
|
|
1369
|
+
getal(row["total_layers_used"]),
|
|
1370
|
+
getal(row["occupancy_pct"]),
|
|
1371
|
+
getal(row["weight_kg"]),
|
|
1372
|
+
String(row["layout_method"] ?? ""),
|
|
1373
|
+
getal(row["algorithm_version"]),
|
|
1374
|
+
String(row["is_complete"] ?? ""),
|
|
1375
|
+
String(row["source"] ?? ""),
|
|
1376
|
+
].join("|") +
|
|
1377
|
+
"#" +
|
|
1378
|
+
items.map(itemSig).sort().join(";");
|
|
1379
|
+
const gewenst = plannedCarts.map((cart, cartIndex) => {
|
|
1380
|
+
const identity = persistentIdentities[cartIndex];
|
|
1381
|
+
if (!identity) {
|
|
1382
|
+
throw new Error(`Karrenindeling ongeldig: identiteit ontbreekt voor kar ${cartIndex + 1}.`);
|
|
1383
|
+
}
|
|
1384
|
+
const row = {
|
|
1385
|
+
order_id: identity.ownerOrderId,
|
|
1386
|
+
buffer_id: bufferId,
|
|
1387
|
+
cart_number: identity.cartNumber,
|
|
1388
|
+
total_carts: identity.totalCarts,
|
|
1389
|
+
is_auto: true,
|
|
1390
|
+
is_complete: cart.is_complete,
|
|
1391
|
+
source: usedFloridayPacking ? "floriday" : "auto",
|
|
1392
|
+
occupancy_pct: cart.occupancy_pct,
|
|
1393
|
+
total_layers_used: cart.total_layers_used,
|
|
1394
|
+
carrier_type: cart.carrier_type,
|
|
1395
|
+
carrier_type_id: cart.carrier_type_id,
|
|
1396
|
+
max_layers: cart.max_layers,
|
|
1397
|
+
weight_kg: cart.weight_kg,
|
|
1398
|
+
algorithm_version: useDimensions ? DIMENSION_PLANNER_VERSION : LOAD_CARRIER_PLANNER_VERSION,
|
|
1399
|
+
layout_method: useDimensions ? "afmetingen" : "belading",
|
|
1400
|
+
};
|
|
1401
|
+
const items = cart.items.map((i) => ({
|
|
1402
|
+
order_item_id: i.order_item_id.includes("::") ? i.order_item_id.split("::")[0] : i.order_item_id,
|
|
1403
|
+
product_id: i.product_id,
|
|
1404
|
+
product_name: i.product_name,
|
|
1405
|
+
crates: Math.round(i.crates),
|
|
1406
|
+
boxes: i.boxes ?? null,
|
|
1407
|
+
weight_kg: i.weight_kg ?? null,
|
|
1408
|
+
plants_per_crate: i.plants_per_crate,
|
|
1409
|
+
crates_per_layer: i.crates_per_layer,
|
|
1410
|
+
layers_per_cart: i.layers_per_cart,
|
|
1411
|
+
layer_fraction: i.layer_fraction,
|
|
1412
|
+
layer_share_pct: i.layer_share_pct,
|
|
1413
|
+
layer_index: i.layer_index ?? null,
|
|
1414
|
+
crates_in_layer: i.crates_in_layer ?? null,
|
|
1415
|
+
}));
|
|
1416
|
+
return { planIndex: cartIndex, row, items, sig: karSig(row, items) };
|
|
1417
|
+
});
|
|
1418
|
+
// Bestaande (niet-bevroren) karren binnen dezelfde scope inlezen.
|
|
1419
|
+
const scopeOr = orderIds.length > 0
|
|
1420
|
+
? `buffer_id.eq.${bufferId},order_id.in.(${orderIds.join(",")})`
|
|
1421
|
+
: `buffer_id.eq.${bufferId}`;
|
|
1422
|
+
const { data: huidigeKarren, error: huidigeError } = await sb
|
|
1423
|
+
.from("order_carts")
|
|
1424
|
+
.select("id, order_id, buffer_id, cart_number, total_carts, is_complete, source, occupancy_pct, " +
|
|
1425
|
+
"total_layers_used, carrier_type, carrier_type_id, max_layers, weight_kg, algorithm_version, layout_method")
|
|
1426
|
+
.or(scopeOr);
|
|
1427
|
+
if (huidigeError) {
|
|
1428
|
+
throw new Error(`Bestaande karren konden niet worden vergeleken: ${huidigeError.message}`);
|
|
1429
|
+
}
|
|
1430
|
+
const bevroren = new Set(keepCartIds.map(String));
|
|
1431
|
+
const vergelijkbaar = (Array.isArray(huidigeKarren) ? huidigeKarren : []).filter((c) => !bevroren.has(String(c.id)));
|
|
1432
|
+
const huidigeIds = vergelijkbaar.map((c) => String(c.id));
|
|
1433
|
+
const huidigeItems = new Map();
|
|
1434
|
+
for (let s = 0; s < huidigeIds.length; s += 200) {
|
|
1435
|
+
const slice = huidigeIds.slice(s, s + 200);
|
|
1436
|
+
if (slice.length === 0)
|
|
1437
|
+
continue;
|
|
1438
|
+
const { data: rijen, error: itemsError } = await sb
|
|
1439
|
+
.from("order_cart_items")
|
|
1440
|
+
.select("cart_id, order_item_id, product_id, crates, boxes, weight_kg, plants_per_crate, " +
|
|
1441
|
+
"crates_per_layer, layers_per_cart, layer_fraction, layer_share_pct, layer_index, crates_in_layer")
|
|
1442
|
+
.in("cart_id", slice);
|
|
1443
|
+
if (itemsError) {
|
|
1444
|
+
throw new Error(`Inhoud van bestaande karren kon niet worden gelezen: ${itemsError.message}`);
|
|
1445
|
+
}
|
|
1446
|
+
for (const r of Array.isArray(rijen) ? rijen : []) {
|
|
1447
|
+
const id = String(r.cart_id);
|
|
1448
|
+
const lijst = huidigeItems.get(id) ?? [];
|
|
1449
|
+
lijst.push(r);
|
|
1450
|
+
huidigeItems.set(id, lijst);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
const beschikbaarPerSig = new Map();
|
|
1454
|
+
for (const c of vergelijkbaar) {
|
|
1455
|
+
const id = String(c.id);
|
|
1456
|
+
const sig = karSig(c, huidigeItems.get(id) ?? []);
|
|
1457
|
+
const lijst = beschikbaarPerSig.get(sig) ?? [];
|
|
1458
|
+
lijst.push(id);
|
|
1459
|
+
beschikbaarPerSig.set(sig, lijst);
|
|
1460
|
+
}
|
|
1461
|
+
const behouden = new Map(); // planIndex -> bestaande cart id
|
|
1462
|
+
const nieuw = [];
|
|
1463
|
+
for (const g of gewenst) {
|
|
1464
|
+
const kandidaten = beschikbaarPerSig.get(g.sig);
|
|
1465
|
+
const hergebruik = kandidaten && kandidaten.length > 0 ? kandidaten.shift() : undefined;
|
|
1466
|
+
if (hergebruik)
|
|
1467
|
+
behouden.set(g.planIndex, hergebruik);
|
|
1468
|
+
else
|
|
1469
|
+
nieuw.push(g);
|
|
1470
|
+
}
|
|
1471
|
+
const teVerwijderen = [];
|
|
1472
|
+
for (const lijst of beschikbaarPerSig.values())
|
|
1473
|
+
teVerwijderen.push(...lijst);
|
|
1474
|
+
if (teVerwijderen.length > 0) {
|
|
1475
|
+
for (let s = 0; s < teVerwijderen.length; s += 200) {
|
|
1476
|
+
const slice = teVerwijderen.slice(s, s + 200);
|
|
1477
|
+
// Alleen voor karren die echt verdwijnen: openstaande printopdrachten en barcodes vervallen.
|
|
1478
|
+
await sb
|
|
1479
|
+
.from("print_jobs")
|
|
1480
|
+
.update({ status: "geannuleerd", foutmelding: "Vervallen: kar verwijderd bij herindeling", updated_at: new Date().toISOString() })
|
|
1481
|
+
.in("status", ["wachtend", "bezig"])
|
|
1482
|
+
.in("payload->>cart_id", slice);
|
|
1483
|
+
await sb.from("pakbon_barcode_inhoud").delete().in("cart_id", slice);
|
|
1484
|
+
await sb.from("order_cart_layer_positions").delete().in("cart_id", slice);
|
|
1485
|
+
const { error: delError } = await sb.from("order_carts").delete().in("id", slice);
|
|
1486
|
+
if (delError) {
|
|
1487
|
+
throw new Error(`Verouderde karren konden niet worden opgeruimd: ${delError.message}`);
|
|
1181
1488
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
weight_kg: cart.weight_kg,
|
|
1196
|
-
algorithm_version: useDimensions ? DIMENSION_PLANNER_VERSION : LOAD_CARRIER_PLANNER_VERSION,
|
|
1197
|
-
layout_method: useDimensions ? "afmetingen" : "belading",
|
|
1198
|
-
};
|
|
1199
|
-
});
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
let created = behouden.size;
|
|
1492
|
+
const idByPlanIndex = new Map(behouden);
|
|
1493
|
+
if (nieuw.length === 0) {
|
|
1494
|
+
console.log(`[logistiek-engine] buffer ${bufferId}: indeling ongewijzigd, ${behouden.size} kar(ren) behouden, ` +
|
|
1495
|
+
`${teVerwijderen.length} verouderde kar(ren) verwijderd.`);
|
|
1496
|
+
}
|
|
1497
|
+
// Alleen de echt nieuwe karren wegschrijven, in blokken.
|
|
1498
|
+
const CHUNK = 40;
|
|
1499
|
+
for (let start = 0; start < nieuw.length; start += CHUNK) {
|
|
1500
|
+
const chunk = nieuw.slice(start, start + CHUNK);
|
|
1501
|
+
const cartRows = chunk.map((g) => g.row);
|
|
1200
1502
|
const { data: insertedCarts, error } = await sb
|
|
1201
1503
|
.from("order_carts")
|
|
1202
1504
|
.insert(cartRows)
|
|
@@ -1211,33 +1513,13 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
1211
1513
|
const identityKey = (orderId, cartNumber) => `${orderId == null ? "shared" : String(orderId)}:${Number(cartNumber)}`;
|
|
1212
1514
|
const idByIdentity = new Map(insertedCarts.map((c) => [identityKey(c.order_id, c.cart_number), String(c.id)]));
|
|
1213
1515
|
const itemRows = [];
|
|
1214
|
-
for (
|
|
1215
|
-
const
|
|
1216
|
-
const identity = persistentIdentities[start + chunkIndex];
|
|
1217
|
-
const cartId = identity
|
|
1218
|
-
? idByIdentity.get(identityKey(identity.ownerOrderId, identity.cartNumber))
|
|
1219
|
-
: undefined;
|
|
1516
|
+
for (const g of chunk) {
|
|
1517
|
+
const cartId = idByIdentity.get(identityKey(g.row["order_id"], g.row["cart_number"]));
|
|
1220
1518
|
if (!cartId)
|
|
1221
1519
|
continue;
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
// Samengestelde sleutels van groepsleden bestaan niet als orderregel.
|
|
1226
|
-
order_item_id: i.order_item_id.includes("::") ? i.order_item_id.split("::")[0] : i.order_item_id,
|
|
1227
|
-
product_id: i.product_id,
|
|
1228
|
-
product_name: i.product_name,
|
|
1229
|
-
crates: Math.round(i.crates),
|
|
1230
|
-
boxes: i.boxes ?? null,
|
|
1231
|
-
weight_kg: i.weight_kg ?? null,
|
|
1232
|
-
plants_per_crate: i.plants_per_crate,
|
|
1233
|
-
crates_per_layer: i.crates_per_layer,
|
|
1234
|
-
layers_per_cart: i.layers_per_cart,
|
|
1235
|
-
layer_fraction: i.layer_fraction,
|
|
1236
|
-
layer_share_pct: i.layer_share_pct,
|
|
1237
|
-
layer_index: i.layer_index ?? null,
|
|
1238
|
-
crates_in_layer: i.crates_in_layer ?? null,
|
|
1239
|
-
});
|
|
1240
|
-
}
|
|
1520
|
+
idByPlanIndex.set(g.planIndex, cartId);
|
|
1521
|
+
for (const i of g.items)
|
|
1522
|
+
itemRows.push({ ...i, cart_id: cartId });
|
|
1241
1523
|
}
|
|
1242
1524
|
for (let s = 0; s < itemRows.length; s += 500) {
|
|
1243
1525
|
const slice = itemRows.slice(s, s + 500);
|
|
@@ -1258,14 +1540,11 @@ export async function replanBufferInternal(sb, bufferId, options = {}) {
|
|
|
1258
1540
|
// Fysieke laagindeling per kar. Alleen de afmetingenmethode levert posities;
|
|
1259
1541
|
// bij belading blijft de indeling leeg ("nog geen laagindeling aangeleverd").
|
|
1260
1542
|
if (dimensionPlan) {
|
|
1261
|
-
for (
|
|
1262
|
-
const
|
|
1263
|
-
const
|
|
1264
|
-
const
|
|
1265
|
-
|
|
1266
|
-
: undefined;
|
|
1267
|
-
const dimCart = dimensionPlan.get(start + chunkIndex);
|
|
1268
|
-
if (!cartId || !dimCart)
|
|
1543
|
+
for (const g of chunk) {
|
|
1544
|
+
const cartId = idByPlanIndex.get(g.planIndex);
|
|
1545
|
+
const dimCart = dimensionPlan.get(g.planIndex);
|
|
1546
|
+
const cart = plannedCarts[g.planIndex];
|
|
1547
|
+
if (!cartId || !dimCart || !cart)
|
|
1269
1548
|
continue;
|
|
1270
1549
|
const rows = layerPositionRows(cartId, dimCart);
|
|
1271
1550
|
// Atomair vervangen: eerst de vorige indeling van deze kar weg.
|