@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
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// src/lib/dimensionData.server.ts — afmetingen ophalen en controleren
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Zet plannerinvoer om naar fysieke afmetingen. Er wordt nooit een afmeting
|
|
5
|
+
// verzonnen: ontbreekt er een gegeven, dan komt het als leesbare regel terug
|
|
6
|
+
// en wordt er niets ingedeeld.
|
|
7
|
+
// ============================================================================
|
|
8
|
+
const posNum = (v) => {
|
|
9
|
+
const n = Number(v);
|
|
10
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
11
|
+
};
|
|
12
|
+
/** Bruikbare afmetingen van een ladingdrager, of de ontbrekende velden. */
|
|
13
|
+
export function carrierDimensions(row) {
|
|
14
|
+
const missing = [];
|
|
15
|
+
if (!row) {
|
|
16
|
+
return {
|
|
17
|
+
carrier: null,
|
|
18
|
+
missing: [
|
|
19
|
+
{
|
|
20
|
+
subject: "ladingdrager",
|
|
21
|
+
name: "onbekend",
|
|
22
|
+
field: "ladingdrager",
|
|
23
|
+
message: "De ladingdrager kon niet worden gevonden in het beheerscherm Ladingdragers.",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const name = row.name ?? row.code;
|
|
29
|
+
const length = posNum(row.layer_length_cm);
|
|
30
|
+
const width = posNum(row.layer_width_cm);
|
|
31
|
+
const height = posNum(row.max_height_cm);
|
|
32
|
+
if (length == null)
|
|
33
|
+
missing.push({ subject: "ladingdrager", name, field: "lengte", message: missingText("ladingdrager", name, "lengte") });
|
|
34
|
+
if (width == null)
|
|
35
|
+
missing.push({ subject: "ladingdrager", name, field: "breedte", message: missingText("ladingdrager", name, "breedte") });
|
|
36
|
+
if (height == null)
|
|
37
|
+
missing.push({ subject: "ladingdrager", name, field: "hoogte", message: missingText("ladingdrager", name, "hoogte") });
|
|
38
|
+
if (missing.length > 0)
|
|
39
|
+
return { carrier: null, missing };
|
|
40
|
+
return {
|
|
41
|
+
carrier: {
|
|
42
|
+
id: row.id ?? null,
|
|
43
|
+
code: row.code,
|
|
44
|
+
name,
|
|
45
|
+
length_cm: length,
|
|
46
|
+
width_cm: width,
|
|
47
|
+
height_cm: height,
|
|
48
|
+
max_weight_kg: row.max_weight_kg ?? null,
|
|
49
|
+
},
|
|
50
|
+
missing,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function missingText(subject, name, field) {
|
|
54
|
+
return `De karrenindeling kan niet worden berekend. De ${field} van ${subject} ${name} ontbreekt. Vul eerst de ${field} in.`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Haalt fust- en planthoogtegegevens op bij de producten van de plannerinvoer.
|
|
58
|
+
* Groepsleden gebruiken hun eigen product; regels zonder product_id kunnen niet
|
|
59
|
+
* op afmetingen worden ingedeeld.
|
|
60
|
+
*
|
|
61
|
+
* Ontbreekt alleen de planthoogte, dan wordt die afgeleid uit de belading:
|
|
62
|
+
* de bruikbare hoogte van de ladingdrager gedeeld door het aantal lagen dat
|
|
63
|
+
* er volgens de belading op hoort, min de hoogte van het fust. Lukt dat niet
|
|
64
|
+
* (geen ladingdragerhoogte, geen lagen, of een uitkomst van nul of minder),
|
|
65
|
+
* dan blijft het een ontbrekend gegeven en wordt er niets ingedeeld.
|
|
66
|
+
*/
|
|
67
|
+
export async function buildDimensionItems(sb, inputs, carrier,
|
|
68
|
+
/** Aantal lagen dat volgens het beheerscherm op deze ladingdrager past. */
|
|
69
|
+
carrierLayers) {
|
|
70
|
+
const missing = [];
|
|
71
|
+
const derived = [];
|
|
72
|
+
const items = [];
|
|
73
|
+
const productIds = [...new Set(inputs.map((i) => i.product_id).filter((v) => Boolean(v)))];
|
|
74
|
+
const dimById = new Map();
|
|
75
|
+
if (productIds.length > 0) {
|
|
76
|
+
const { data } = await sb
|
|
77
|
+
.from("logistics_product_dimensions")
|
|
78
|
+
.select("product_id, product_name, plants_per_crate, fust_type_id, fust_name, fust_length_mm, fust_width_mm, fust_height_mm, plant_height_cm, height_source, dimension_source, missing_reason")
|
|
79
|
+
.in("product_id", productIds);
|
|
80
|
+
const safeRows = Array.isArray(data) ? data : [];
|
|
81
|
+
for (const d of safeRows)
|
|
82
|
+
dimById.set(String(d.product_id), d);
|
|
83
|
+
}
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
const addMissing = (m) => {
|
|
86
|
+
const key = `${m.subject}|${m.name}|${m.field}`;
|
|
87
|
+
if (seen.has(key))
|
|
88
|
+
return;
|
|
89
|
+
seen.add(key);
|
|
90
|
+
missing.push(m);
|
|
91
|
+
};
|
|
92
|
+
for (const input of inputs) {
|
|
93
|
+
if (input.plants <= 0)
|
|
94
|
+
continue;
|
|
95
|
+
const label = input.product_name || "onbekend product";
|
|
96
|
+
const dim = input.product_id ? dimById.get(input.product_id) : null;
|
|
97
|
+
if (!dim) {
|
|
98
|
+
addMissing({
|
|
99
|
+
subject: "product",
|
|
100
|
+
name: label,
|
|
101
|
+
field: "productkoppeling",
|
|
102
|
+
message: `De karrenindeling kan niet worden berekend. De orderregel ${label} is niet aan een product ` +
|
|
103
|
+
`gekoppeld, dus de afmetingen zijn onbekend.`,
|
|
104
|
+
});
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const name = dim.product_name ?? label;
|
|
108
|
+
const l = posNum(dim.fust_length_mm);
|
|
109
|
+
const w = posNum(dim.fust_width_mm);
|
|
110
|
+
const h = posNum(dim.fust_height_mm);
|
|
111
|
+
let plantHeight = posNum(dim.plant_height_cm);
|
|
112
|
+
// Planthoogte ontbreekt, maar fust- en dragermaten zijn er wel: afleiden
|
|
113
|
+
// uit het aantal lagen dat op de drager hoort. Eerst de belading van het
|
|
114
|
+
// product zelf; past dat fysiek niet (fust hoger dan de laaghoogte), dan
|
|
115
|
+
// het aantal lagen van de ladingdrager uit het beheerscherm.
|
|
116
|
+
if (plantHeight == null && h != null && carrier && carrier.height_cm > 0) {
|
|
117
|
+
const productLayers = Math.round(Number(input.layers_per_cart));
|
|
118
|
+
const carrierLayerCount = Math.round(Number(carrierLayers));
|
|
119
|
+
const kandidaten = [];
|
|
120
|
+
if (Number.isFinite(productLayers) && productLayers > 0)
|
|
121
|
+
kandidaten.push({ layers: productLayers, bron: "de belading van het product" });
|
|
122
|
+
if (Number.isFinite(carrierLayerCount) && carrierLayerCount > 0)
|
|
123
|
+
kandidaten.push({ layers: carrierLayerCount, bron: `de lagen van ${carrier.name}` });
|
|
124
|
+
for (const kandidaat of kandidaten) {
|
|
125
|
+
const perLayer = carrier.height_cm / kandidaat.layers;
|
|
126
|
+
const afgeleid = Number((perLayer - h / 10).toFixed(1));
|
|
127
|
+
if (afgeleid > 0) {
|
|
128
|
+
plantHeight = afgeleid;
|
|
129
|
+
derived.push(`De planthoogte van ${name} was niet ingevuld en is berekend uit ${kandidaat.bron}: ` +
|
|
130
|
+
`${kandidaat.layers} lagen op ${carrier.name} (${carrier.height_cm} cm) min de fusthoogte ` +
|
|
131
|
+
`(${round1(h / 10)} cm) geeft ${afgeleid} cm.`);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (l == null || w == null || h == null || plantHeight == null) {
|
|
137
|
+
addMissing({
|
|
138
|
+
subject: l == null && w == null && h == null ? "product" : "fust",
|
|
139
|
+
name,
|
|
140
|
+
field: plantHeight == null && l != null ? "planthoogte" : "fusttype",
|
|
141
|
+
message: dim.missing_reason ??
|
|
142
|
+
`De karrenindeling kan niet worden berekend. De afmetingen van ${name} zijn onvolledig.`,
|
|
143
|
+
});
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const ppc = Math.max(1, Math.round(input.plants_per_crate || dim.plants_per_crate || 1));
|
|
147
|
+
const crates = Math.max(1, Math.ceil(input.plants / ppc));
|
|
148
|
+
items.push({
|
|
149
|
+
order_item_id: input.order_item_id,
|
|
150
|
+
order_id: input.order_id ?? null,
|
|
151
|
+
customer_id: input.customer_id ?? null,
|
|
152
|
+
product_id: input.product_id,
|
|
153
|
+
product_name: input.product_name,
|
|
154
|
+
crates,
|
|
155
|
+
plants_per_crate: ppc,
|
|
156
|
+
crate_length_cm: l / 10,
|
|
157
|
+
crate_width_cm: w / 10,
|
|
158
|
+
crate_height_cm: h / 10,
|
|
159
|
+
plant_height_cm: plantHeight,
|
|
160
|
+
weight_per_crate: input.weight_per_crate ?? null,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return { items, missing, derived };
|
|
164
|
+
}
|
|
165
|
+
function round1(n) {
|
|
166
|
+
return Number(n.toFixed(1));
|
|
167
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export declare const DIMENSION_PLANNER_VERSION = 5;
|
|
2
|
+
export interface DimensionItem {
|
|
3
|
+
order_item_id: string;
|
|
4
|
+
order_id: string | null;
|
|
5
|
+
customer_id: string | null;
|
|
6
|
+
product_id: string | null;
|
|
7
|
+
product_name: string;
|
|
8
|
+
/** Aantal fusten van dit product. */
|
|
9
|
+
crates: number;
|
|
10
|
+
plants_per_crate: number;
|
|
11
|
+
/** Afmetingen van één fust in cm. */
|
|
12
|
+
crate_length_cm: number;
|
|
13
|
+
crate_width_cm: number;
|
|
14
|
+
crate_height_cm: number;
|
|
15
|
+
/** Hoogte van de plant/het product boven het fust, in cm. */
|
|
16
|
+
plant_height_cm: number;
|
|
17
|
+
weight_per_crate?: number | null;
|
|
18
|
+
}
|
|
19
|
+
export interface DimensionCarrier {
|
|
20
|
+
id?: string | null;
|
|
21
|
+
code: string;
|
|
22
|
+
name: string;
|
|
23
|
+
/** Bruikbare afmetingen van de ladingdrager in cm. */
|
|
24
|
+
length_cm: number;
|
|
25
|
+
width_cm: number;
|
|
26
|
+
height_cm: number;
|
|
27
|
+
/** Optionele bovengrens uit de werkwijze. */
|
|
28
|
+
max_layers?: number | null;
|
|
29
|
+
max_orders?: number | null;
|
|
30
|
+
max_customers?: number | null;
|
|
31
|
+
max_weight_kg?: number | null;
|
|
32
|
+
}
|
|
33
|
+
export interface DimensionPlacement {
|
|
34
|
+
order_item_id: string;
|
|
35
|
+
order_id: string | null;
|
|
36
|
+
product_id: string | null;
|
|
37
|
+
product_name: string;
|
|
38
|
+
crates: number;
|
|
39
|
+
plants: number;
|
|
40
|
+
plants_per_crate: number;
|
|
41
|
+
weight_kg: number | null;
|
|
42
|
+
/** Rotatie waarmee de fusten op deze laag staan. */
|
|
43
|
+
rotated: boolean;
|
|
44
|
+
}
|
|
45
|
+
/** Eén fust op zijn werkelijke plek binnen een laag. Alle maten in cm. */
|
|
46
|
+
export interface DimensionKegPosition {
|
|
47
|
+
keg_id: string;
|
|
48
|
+
order_item_id: string;
|
|
49
|
+
order_id: string | null;
|
|
50
|
+
product_id: string | null;
|
|
51
|
+
label: string;
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
rotation: number;
|
|
57
|
+
}
|
|
58
|
+
export interface DimensionLayer {
|
|
59
|
+
layer_index: number;
|
|
60
|
+
height_cm: number;
|
|
61
|
+
/** Benutting van het laagoppervlak in procenten. */
|
|
62
|
+
area_used_pct: number;
|
|
63
|
+
/** Bruikbare afmetingen van de laag in cm. */
|
|
64
|
+
layer_length_cm: number;
|
|
65
|
+
layer_width_cm: number;
|
|
66
|
+
placements: DimensionPlacement[];
|
|
67
|
+
positions: DimensionKegPosition[];
|
|
68
|
+
}
|
|
69
|
+
export interface DimensionCart {
|
|
70
|
+
cart_number: number;
|
|
71
|
+
carrier_code: string;
|
|
72
|
+
carrier_id: string | null;
|
|
73
|
+
height_used_cm: number;
|
|
74
|
+
height_available_cm: number;
|
|
75
|
+
occupancy_pct: number;
|
|
76
|
+
is_complete: boolean;
|
|
77
|
+
weight_kg: number | null;
|
|
78
|
+
layers: DimensionLayer[];
|
|
79
|
+
}
|
|
80
|
+
export interface DimensionPlanResult {
|
|
81
|
+
carts: DimensionCart[];
|
|
82
|
+
warnings: string[];
|
|
83
|
+
/** Gevuld als er niets ingedeeld kan worden; dan is carts leeg. */
|
|
84
|
+
error?: string;
|
|
85
|
+
}
|
|
86
|
+
export interface PlacedRect {
|
|
87
|
+
rotated: boolean;
|
|
88
|
+
/** Linkerbovenhoek en werkelijke maten in de gekozen oriëntatie (cm). */
|
|
89
|
+
x: number;
|
|
90
|
+
y: number;
|
|
91
|
+
width: number;
|
|
92
|
+
height: number;
|
|
93
|
+
}
|
|
94
|
+
export declare function planByDimensions(items: DimensionItem[], carrier: DimensionCarrier): DimensionPlanResult;
|
|
95
|
+
//# sourceMappingURL=dimensionPlanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dimensionPlanner.d.ts","sourceRoot":"","sources":["../../src/lib/dimensionPlanner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// src/lib/dimensionPlanner.ts — karrenindeling op basis van fysieke afmetingen
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Pure functie: geen database, geen netwerk, deterministisch.
|
|
5
|
+
//
|
|
6
|
+
// Werkwijze:
|
|
7
|
+
// 1. Per fust wordt de laaghoogte bepaald: fusthoogte + planthoogte.
|
|
8
|
+
// 2. Een laag wordt 2D ingedeeld op de bruikbare lengte x breedte van de
|
|
9
|
+
// ladingdrager. Elk fust wordt ook 90 graden gedraaid geprobeerd.
|
|
10
|
+
// 3. Lagen worden gestapeld tot de bruikbare hoogte van de drager op is.
|
|
11
|
+
// 4. Is de drager vol, dan volgt een nieuwe drager.
|
|
12
|
+
//
|
|
13
|
+
// Er wordt nooit een afmeting verzonnen: ontbrekende gegevens worden vooraf
|
|
14
|
+
// gecontroleerd door de aanroeper (zie dimensionData / collectMissingDimensions).
|
|
15
|
+
// ============================================================================
|
|
16
|
+
export const DIMENSION_PLANNER_VERSION = 5;
|
|
17
|
+
class LayerSpace {
|
|
18
|
+
constructor(length, width) {
|
|
19
|
+
this.length = length;
|
|
20
|
+
this.width = width;
|
|
21
|
+
this.shelves = [];
|
|
22
|
+
this.totalDepth = 0;
|
|
23
|
+
this.usedArea = 0;
|
|
24
|
+
}
|
|
25
|
+
/** Probeert één fust te plaatsen. Geeft de plaatsing terug, of null. */
|
|
26
|
+
place(l, w) {
|
|
27
|
+
const options = [
|
|
28
|
+
{ a: l, b: w, rotated: false },
|
|
29
|
+
{ a: w, b: l, rotated: true },
|
|
30
|
+
];
|
|
31
|
+
for (const o of options) {
|
|
32
|
+
if (o.a > this.length || o.b > this.width)
|
|
33
|
+
continue;
|
|
34
|
+
for (let i = 0; i < this.shelves.length; i++) {
|
|
35
|
+
const shelf = this.shelves[i];
|
|
36
|
+
const last = i === this.shelves.length - 1;
|
|
37
|
+
const fitsLength = shelf.usedLength + o.a <= this.length + 1e-9;
|
|
38
|
+
if (!fitsLength)
|
|
39
|
+
continue;
|
|
40
|
+
if (o.b <= shelf.depth + 1e-9) {
|
|
41
|
+
const rect = { rotated: o.rotated, x: shelf.usedLength, y: shelf.y, width: o.a, height: o.b };
|
|
42
|
+
shelf.usedLength += o.a;
|
|
43
|
+
this.usedArea += o.a * o.b;
|
|
44
|
+
return rect;
|
|
45
|
+
}
|
|
46
|
+
if (last && this.totalDepth - shelf.depth + o.b <= this.width + 1e-9) {
|
|
47
|
+
const rect = { rotated: o.rotated, x: shelf.usedLength, y: shelf.y, width: o.a, height: o.b };
|
|
48
|
+
this.totalDepth = this.totalDepth - shelf.depth + o.b;
|
|
49
|
+
shelf.depth = o.b;
|
|
50
|
+
shelf.usedLength += o.a;
|
|
51
|
+
this.usedArea += o.a * o.b;
|
|
52
|
+
return rect;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (this.totalDepth + o.b <= this.width + 1e-9) {
|
|
56
|
+
const rect = { rotated: o.rotated, x: 0, y: this.totalDepth, width: o.a, height: o.b };
|
|
57
|
+
this.shelves.push({ y: this.totalDepth, depth: o.b, usedLength: o.a });
|
|
58
|
+
this.totalDepth += o.b;
|
|
59
|
+
this.usedArea += o.a * o.b;
|
|
60
|
+
return rect;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
get areaPct() {
|
|
66
|
+
const total = this.length * this.width;
|
|
67
|
+
return total > 0 ? Number(((100 * this.usedArea) / total).toFixed(1)) : 0;
|
|
68
|
+
}
|
|
69
|
+
get isEmpty() {
|
|
70
|
+
return this.shelves.length === 0;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function planByDimensions(items, carrier) {
|
|
74
|
+
const warnings = [];
|
|
75
|
+
if (!(carrier.length_cm > 0) || !(carrier.width_cm > 0) || !(carrier.height_cm > 0)) {
|
|
76
|
+
return {
|
|
77
|
+
carts: [],
|
|
78
|
+
warnings,
|
|
79
|
+
error: `De afmetingen van ladingdrager ${carrier.name} zijn niet volledig ingevuld (lengte, breedte en hoogte).`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const batches = items
|
|
83
|
+
.filter((i) => i.crates > 0)
|
|
84
|
+
.map((item) => ({
|
|
85
|
+
item,
|
|
86
|
+
left: Math.max(0, Math.round(item.crates)),
|
|
87
|
+
height: item.crate_height_cm + item.plant_height_cm,
|
|
88
|
+
}));
|
|
89
|
+
for (const b of batches) {
|
|
90
|
+
const fitsFlat = b.item.crate_length_cm <= carrier.length_cm && b.item.crate_width_cm <= carrier.width_cm;
|
|
91
|
+
const fitsTurned = b.item.crate_width_cm <= carrier.length_cm && b.item.crate_length_cm <= carrier.width_cm;
|
|
92
|
+
if (!fitsFlat && !fitsTurned) {
|
|
93
|
+
return {
|
|
94
|
+
carts: [],
|
|
95
|
+
warnings,
|
|
96
|
+
error: `Het fust van ${b.item.product_name} (${b.item.crate_length_cm} x ${b.item.crate_width_cm} cm) ` +
|
|
97
|
+
`past niet op ladingdrager ${carrier.name} (${carrier.length_cm} x ${carrier.width_cm} cm).`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (b.height > carrier.height_cm) {
|
|
101
|
+
return {
|
|
102
|
+
carts: [],
|
|
103
|
+
warnings,
|
|
104
|
+
error: `Eén laag van ${b.item.product_name} is ${round1(b.height)} cm hoog en past niet in de ` +
|
|
105
|
+
`bruikbare hoogte van ladingdrager ${carrier.name} (${carrier.height_cm} cm).`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const carts = [];
|
|
110
|
+
let guard = 0;
|
|
111
|
+
while (batches.some((b) => b.left > 0)) {
|
|
112
|
+
if (++guard > 5000) {
|
|
113
|
+
return { carts: [], warnings, error: "De indeling op afmetingen kwam niet tot een einde." };
|
|
114
|
+
}
|
|
115
|
+
const cartOrders = new Set();
|
|
116
|
+
const cartCustomers = new Set();
|
|
117
|
+
const layers = [];
|
|
118
|
+
let heightLeft = carrier.height_cm;
|
|
119
|
+
while (heightLeft > 0) {
|
|
120
|
+
if (carrier.max_layers != null && layers.length >= carrier.max_layers)
|
|
121
|
+
break;
|
|
122
|
+
// Hoogste fusten eerst; bij gelijke hoogte het grootste grondvlak eerst.
|
|
123
|
+
const pool = batches
|
|
124
|
+
.filter((b) => b.left > 0)
|
|
125
|
+
.filter((b) => allowedOnCart(b, cartOrders, cartCustomers, carrier))
|
|
126
|
+
.filter((b) => b.height <= heightLeft + 1e-9)
|
|
127
|
+
.sort((a, b) => b.height - a.height ||
|
|
128
|
+
b.item.crate_length_cm * b.item.crate_width_cm - a.item.crate_length_cm * a.item.crate_width_cm);
|
|
129
|
+
if (pool.length === 0)
|
|
130
|
+
break;
|
|
131
|
+
const space = new LayerSpace(carrier.length_cm, carrier.width_cm);
|
|
132
|
+
const perItem = new Map();
|
|
133
|
+
const positions = [];
|
|
134
|
+
let layerHeight = 0;
|
|
135
|
+
for (const b of pool) {
|
|
136
|
+
while (b.left > 0) {
|
|
137
|
+
// Een laag mag niet hoger worden dan wat er nog op de drager past.
|
|
138
|
+
const newHeight = Math.max(layerHeight, b.height);
|
|
139
|
+
if (newHeight > heightLeft + 1e-9)
|
|
140
|
+
break;
|
|
141
|
+
const placed = space.place(b.item.crate_length_cm, b.item.crate_width_cm);
|
|
142
|
+
if (!placed)
|
|
143
|
+
break;
|
|
144
|
+
layerHeight = newHeight;
|
|
145
|
+
b.left -= 1;
|
|
146
|
+
const acc = perItem.get(b) ?? { crates: 0, rotated: placed.rotated };
|
|
147
|
+
acc.crates += 1;
|
|
148
|
+
perItem.set(b, acc);
|
|
149
|
+
const seq = positions.length + 1;
|
|
150
|
+
positions.push({
|
|
151
|
+
keg_id: `L${layers.length + 1}-${seq}`,
|
|
152
|
+
order_item_id: b.item.order_item_id,
|
|
153
|
+
order_id: b.item.order_id,
|
|
154
|
+
product_id: b.item.product_id,
|
|
155
|
+
label: `${b.item.product_name} — fust ${seq} (laag ${layers.length + 1})` +
|
|
156
|
+
(placed.rotated ? " — gedraaid" : ""),
|
|
157
|
+
x: round1(placed.x),
|
|
158
|
+
y: round1(placed.y),
|
|
159
|
+
width: round1(placed.width),
|
|
160
|
+
height: round1(placed.height),
|
|
161
|
+
rotation: 0,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (space.isEmpty || perItem.size === 0)
|
|
166
|
+
break;
|
|
167
|
+
const placements = [];
|
|
168
|
+
for (const [batch, acc] of perItem) {
|
|
169
|
+
const item = batch.item;
|
|
170
|
+
if (item.order_id)
|
|
171
|
+
cartOrders.add(item.order_id);
|
|
172
|
+
if (item.customer_id)
|
|
173
|
+
cartCustomers.add(item.customer_id);
|
|
174
|
+
placements.push({
|
|
175
|
+
order_item_id: item.order_item_id,
|
|
176
|
+
order_id: item.order_id,
|
|
177
|
+
product_id: item.product_id,
|
|
178
|
+
product_name: item.product_name,
|
|
179
|
+
crates: acc.crates,
|
|
180
|
+
plants: acc.crates * Math.max(1, item.plants_per_crate),
|
|
181
|
+
plants_per_crate: Math.max(1, item.plants_per_crate),
|
|
182
|
+
weight_kg: item.weight_per_crate != null ? Number((item.weight_per_crate * acc.crates).toFixed(2)) : null,
|
|
183
|
+
rotated: acc.rotated,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
layers.push({
|
|
187
|
+
layer_index: layers.length + 1,
|
|
188
|
+
height_cm: round1(layerHeight),
|
|
189
|
+
area_used_pct: space.areaPct,
|
|
190
|
+
layer_length_cm: carrier.length_cm,
|
|
191
|
+
layer_width_cm: carrier.width_cm,
|
|
192
|
+
placements,
|
|
193
|
+
positions,
|
|
194
|
+
});
|
|
195
|
+
heightLeft = round1(heightLeft - layerHeight);
|
|
196
|
+
}
|
|
197
|
+
if (layers.length === 0) {
|
|
198
|
+
return {
|
|
199
|
+
carts: [],
|
|
200
|
+
warnings,
|
|
201
|
+
error: `Er kon geen enkele laag worden gevuld op ladingdrager ${carrier.name}. ` +
|
|
202
|
+
`Controleer de afmetingen van de fusten en van de ladingdrager.`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const usedHeight = round1(layers.reduce((s, l) => s + l.height_cm, 0));
|
|
206
|
+
const weights = layers.flatMap((l) => l.placements.map((p) => p.weight_kg));
|
|
207
|
+
carts.push({
|
|
208
|
+
cart_number: carts.length + 1,
|
|
209
|
+
carrier_code: carrier.code,
|
|
210
|
+
carrier_id: carrier.id ?? null,
|
|
211
|
+
height_used_cm: usedHeight,
|
|
212
|
+
height_available_cm: carrier.height_cm,
|
|
213
|
+
occupancy_pct: Number(((100 * usedHeight) / carrier.height_cm).toFixed(1)),
|
|
214
|
+
is_complete: false,
|
|
215
|
+
weight_kg: weights.some((w) => w != null)
|
|
216
|
+
? Number(weights.reduce((s, w) => s + (w ?? 0), 0).toFixed(2))
|
|
217
|
+
: null,
|
|
218
|
+
layers,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
// Alle dragers behalve de laatste zijn vol geladen.
|
|
222
|
+
carts.forEach((c, idx) => {
|
|
223
|
+
c.is_complete = idx < carts.length - 1 || c.occupancy_pct >= 99.9;
|
|
224
|
+
});
|
|
225
|
+
const expectedCrates = batches.reduce((sum, batch) => sum + Math.max(0, Math.round(batch.item.crates)), 0);
|
|
226
|
+
const plannedCrates = carts.reduce((cartSum, cart) => cartSum + cart.layers.reduce((layerSum, layer) => layerSum + layer.positions.length, 0), 0);
|
|
227
|
+
if (plannedCrates !== expectedCrates) {
|
|
228
|
+
return {
|
|
229
|
+
carts: [],
|
|
230
|
+
warnings,
|
|
231
|
+
error: `De fysieke indeling is onvolledig: ${expectedCrates} fusten verwacht, maar ${plannedCrates} ` +
|
|
232
|
+
`fustposities berekend. Er is niets ingedeeld.`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return { carts, warnings };
|
|
236
|
+
}
|
|
237
|
+
function allowedOnCart(batch, orders, customers, carrier) {
|
|
238
|
+
const oid = batch.item.order_id;
|
|
239
|
+
const cid = batch.item.customer_id;
|
|
240
|
+
if (carrier.max_orders != null && oid && !orders.has(oid) && orders.size >= carrier.max_orders)
|
|
241
|
+
return false;
|
|
242
|
+
if (carrier.max_customers != null && cid && !customers.has(cid) && customers.size >= carrier.max_customers)
|
|
243
|
+
return false;
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
function round1(n) {
|
|
247
|
+
return Number(n.toFixed(1));
|
|
248
|
+
}
|