@flowselections/logistiek-engine 1.0.25 → 1.0.27
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/ConsistentieCard.d.ts +2 -0
- package/dist-lib/components/ConsistentieCard.d.ts.map +1 -0
- package/dist-lib/components/ConsistentieCard.js +38 -0
- package/dist-lib/components/ModulePromptsPage.d.ts.map +1 -1
- package/dist-lib/components/ModulePromptsPage.js +11 -3
- package/dist-lib/components/RegiePage.d.ts.map +1 -1
- package/dist-lib/components/RegiePage.js +2 -1
- package/dist-lib/index.d.ts +1 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +1 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +1048 -715
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +1048 -715
- 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 +1048 -715
- package/dist-lib/integrations/supabase/moduleClient.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +1081 -768
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/dimensionData.server.d.ts +27 -0
- package/dist-lib/lib/dimensionData.server.d.ts.map +1 -0
- package/dist-lib/lib/dimensionData.server.js +157 -0
- package/dist-lib/lib/dimensionPlanner.d.ts +70 -0
- package/dist-lib/lib/dimensionPlanner.d.ts.map +1 -0
- package/dist-lib/lib/dimensionPlanner.js +217 -0
- package/dist-lib/lib/floridayDocuments.functions.d.ts +3054 -2055
- package/dist-lib/lib/floridayDocuments.functions.d.ts.map +1 -1
- package/dist-lib/lib/floridayPush.functions.d.ts +1048 -715
- package/dist-lib/lib/floridayPush.functions.d.ts.map +1 -1
- 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 +5105 -3465
- package/dist-lib/lib/loadCarrierPlanning.functions.d.ts.map +1 -1
- package/dist-lib/lib/loadCarrierPlanning.server.d.ts +4 -0
- package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -1
- package/dist-lib/lib/loadCarrierPlanning.server.js +113 -3
- package/dist-lib/lib/logisticsPlanner.d.ts.map +1 -1
- package/dist-lib/lib/logisticsPlanner.js +35 -13
- package/dist-lib/lib/logisticsRecalcJob.functions.d.ts +1983 -1317
- package/dist-lib/lib/logisticsRecalcJob.functions.d.ts.map +1 -1
- package/dist-lib/lib/logisticsRecalculate.functions.d.ts +1048 -715
- package/dist-lib/lib/logisticsRecalculate.functions.d.ts.map +1 -1
- package/dist-lib/lib/modulePrompts.d.ts +19 -3
- package/dist-lib/lib/modulePrompts.d.ts.map +1 -1
- package/dist-lib/lib/modulePrompts.js +1666 -1298
- package/dist-lib/lib/ruleEngineSchema.d.ts.map +1 -1
- package/dist-lib/lib/ruleEngineSchema.js +32 -4
- package/dist-lib/lib/ruleEngineWizard.d.ts.map +1 -1
- package/dist-lib/lib/ruleEngineWizard.js +4 -1
- package/package.json +1 -1
- package/dist-lib/components/rule-engine/FloridayConnectionBanner.d.ts +0 -2
- package/dist-lib/components/rule-engine/FloridayConnectionBanner.d.ts.map +0 -1
- package/dist-lib/components/rule-engine/FloridayConnectionBanner.js +0 -41
- package/dist-lib/integrations/supabase/client.d.ts +0 -20293
- package/dist-lib/integrations/supabase/client.d.ts.map +0 -1
- package/dist-lib/integrations/supabase/client.js +0 -13
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import type { PlannerInputItem } from "./loadCarrierPlanner";
|
|
3
|
+
import type { LoadCarrierTypeRow } from "./loadCarrierTypes";
|
|
4
|
+
import type { DimensionCarrier, DimensionItem } from "./dimensionPlanner";
|
|
5
|
+
export interface MissingDimension {
|
|
6
|
+
/** product | fust | ladingdrager */
|
|
7
|
+
subject: "product" | "fust" | "ladingdrager";
|
|
8
|
+
name: string;
|
|
9
|
+
field: string;
|
|
10
|
+
message: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DimensionDataResult {
|
|
13
|
+
items: DimensionItem[];
|
|
14
|
+
missing: MissingDimension[];
|
|
15
|
+
}
|
|
16
|
+
/** Bruikbare afmetingen van een ladingdrager, of de ontbrekende velden. */
|
|
17
|
+
export declare function carrierDimensions(row: LoadCarrierTypeRow | null | undefined): {
|
|
18
|
+
carrier: DimensionCarrier | null;
|
|
19
|
+
missing: MissingDimension[];
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Haalt fust- en planthoogtegegevens op bij de producten van de plannerinvoer.
|
|
23
|
+
* Groepsleden gebruiken hun eigen product; regels zonder product_id kunnen niet
|
|
24
|
+
* op afmetingen worden ingedeeld.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildDimensionItems(sb: SupabaseClient, inputs: PlannerInputItem[]): Promise<DimensionDataResult>;
|
|
27
|
+
//# sourceMappingURL=dimensionData.server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dimensionData.server.d.ts","sourceRoot":"","sources":["../../src/lib/dimensionData.server.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAE1E,MAAM,WAAW,gBAAgB;IAC/B,oCAAoC;IACpC,OAAO,EAAE,SAAS,GAAG,MAAM,GAAG,cAAc,CAAC;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,OAAO,EAAE,gBAAgB,EAAE,CAAC;CAC7B;AAOD,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,kBAAkB,GAAG,IAAI,GAAG,SAAS,GAAG;IAC7E,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjC,OAAO,EAAE,gBAAgB,EAAE,CAAC;CAC7B,CAsCA;AAMD;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,cAAc,EAClB,MAAM,EAAE,gBAAgB,EAAE,GACzB,OAAO,CAAC,mBAAmB,CAAC,CAuG9B"}
|
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
export async function buildDimensionItems(sb, inputs) {
|
|
62
|
+
const missing = [];
|
|
63
|
+
const items = [];
|
|
64
|
+
const productIds = [...new Set(inputs.map((i) => i.product_id).filter((v) => Boolean(v)))];
|
|
65
|
+
const productById = new Map();
|
|
66
|
+
if (productIds.length > 0) {
|
|
67
|
+
const { data } = await sb
|
|
68
|
+
.from("products")
|
|
69
|
+
.select("id, product, plant_height_cm, fust_type_id, plants_per_crate, weight")
|
|
70
|
+
.in("id", productIds);
|
|
71
|
+
for (const p of data ?? [])
|
|
72
|
+
productById.set(String(p.id), p);
|
|
73
|
+
}
|
|
74
|
+
const fustIds = [
|
|
75
|
+
...new Set([...productById.values()].map((p) => p.fust_type_id).filter((v) => Boolean(v))),
|
|
76
|
+
];
|
|
77
|
+
const fustById = new Map();
|
|
78
|
+
if (fustIds.length > 0) {
|
|
79
|
+
const { data } = await sb
|
|
80
|
+
.from("fust_types")
|
|
81
|
+
.select("id, name, length_mm, width_mm, height_mm")
|
|
82
|
+
.in("id", fustIds);
|
|
83
|
+
for (const f of data ?? [])
|
|
84
|
+
fustById.set(String(f.id), f);
|
|
85
|
+
}
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
const addMissing = (m) => {
|
|
88
|
+
const key = `${m.subject}|${m.name}|${m.field}`;
|
|
89
|
+
if (seen.has(key))
|
|
90
|
+
return;
|
|
91
|
+
seen.add(key);
|
|
92
|
+
missing.push(m);
|
|
93
|
+
};
|
|
94
|
+
for (const input of inputs) {
|
|
95
|
+
if (input.plants <= 0)
|
|
96
|
+
continue;
|
|
97
|
+
const label = input.product_name || "onbekend product";
|
|
98
|
+
const product = input.product_id ? productById.get(input.product_id) : null;
|
|
99
|
+
if (!product) {
|
|
100
|
+
addMissing({
|
|
101
|
+
subject: "product",
|
|
102
|
+
name: label,
|
|
103
|
+
field: "productkoppeling",
|
|
104
|
+
message: `De karrenindeling kan niet worden berekend. De orderregel ${label} is niet aan een product ` +
|
|
105
|
+
`gekoppeld, dus de afmetingen zijn onbekend.`,
|
|
106
|
+
});
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const plantHeight = posNum(product.plant_height_cm);
|
|
110
|
+
if (plantHeight == null) {
|
|
111
|
+
addMissing({
|
|
112
|
+
subject: "product",
|
|
113
|
+
name: product.product ?? label,
|
|
114
|
+
field: "planthoogte",
|
|
115
|
+
message: missingText("product", product.product ?? label, "planthoogte"),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const fust = product.fust_type_id ? fustById.get(String(product.fust_type_id)) : null;
|
|
119
|
+
if (!fust) {
|
|
120
|
+
addMissing({
|
|
121
|
+
subject: "product",
|
|
122
|
+
name: product.product ?? label,
|
|
123
|
+
field: "fusttype",
|
|
124
|
+
message: missingText("product", product.product ?? label, "fusttype"),
|
|
125
|
+
});
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const l = posNum(fust.length_mm);
|
|
129
|
+
const w = posNum(fust.width_mm);
|
|
130
|
+
const h = posNum(fust.height_mm);
|
|
131
|
+
if (l == null)
|
|
132
|
+
addMissing({ subject: "fust", name: fust.name, field: "lengte", message: missingText("fust", fust.name, "lengte") });
|
|
133
|
+
if (w == null)
|
|
134
|
+
addMissing({ subject: "fust", name: fust.name, field: "breedte", message: missingText("fust", fust.name, "breedte") });
|
|
135
|
+
if (h == null)
|
|
136
|
+
addMissing({ subject: "fust", name: fust.name, field: "hoogte", message: missingText("fust", fust.name, "hoogte") });
|
|
137
|
+
if (plantHeight == null || l == null || w == null || h == null)
|
|
138
|
+
continue;
|
|
139
|
+
const ppc = Math.max(1, Math.round(input.plants_per_crate || product.plants_per_crate || 1));
|
|
140
|
+
const crates = Math.max(1, Math.ceil(input.plants / ppc));
|
|
141
|
+
items.push({
|
|
142
|
+
order_item_id: input.order_item_id,
|
|
143
|
+
order_id: input.order_id ?? null,
|
|
144
|
+
customer_id: input.customer_id ?? null,
|
|
145
|
+
product_id: input.product_id,
|
|
146
|
+
product_name: input.product_name,
|
|
147
|
+
crates,
|
|
148
|
+
plants_per_crate: ppc,
|
|
149
|
+
crate_length_cm: l / 10,
|
|
150
|
+
crate_width_cm: w / 10,
|
|
151
|
+
crate_height_cm: h / 10,
|
|
152
|
+
plant_height_cm: plantHeight,
|
|
153
|
+
weight_per_crate: input.weight_per_crate ?? null,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return { items, missing };
|
|
157
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export declare const DIMENSION_PLANNER_VERSION = 4;
|
|
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
|
+
export interface DimensionLayer {
|
|
46
|
+
layer_index: number;
|
|
47
|
+
height_cm: number;
|
|
48
|
+
/** Benutting van het laagoppervlak in procenten. */
|
|
49
|
+
area_used_pct: number;
|
|
50
|
+
placements: DimensionPlacement[];
|
|
51
|
+
}
|
|
52
|
+
export interface DimensionCart {
|
|
53
|
+
cart_number: number;
|
|
54
|
+
carrier_code: string;
|
|
55
|
+
carrier_id: string | null;
|
|
56
|
+
height_used_cm: number;
|
|
57
|
+
height_available_cm: number;
|
|
58
|
+
occupancy_pct: number;
|
|
59
|
+
is_complete: boolean;
|
|
60
|
+
weight_kg: number | null;
|
|
61
|
+
layers: DimensionLayer[];
|
|
62
|
+
}
|
|
63
|
+
export interface DimensionPlanResult {
|
|
64
|
+
carts: DimensionCart[];
|
|
65
|
+
warnings: string[];
|
|
66
|
+
/** Gevuld als er niets ingedeeld kan worden; dan is carts leeg. */
|
|
67
|
+
error?: string;
|
|
68
|
+
}
|
|
69
|
+
export declare function planByDimensions(items: DimensionItem[], carrier: DimensionCarrier): DimensionPlanResult;
|
|
70
|
+
//# sourceMappingURL=dimensionPlanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dimensionPlanner.d.ts","sourceRoot":"","sources":["../../src/lib/dimensionPlanner.ts"],"names":[],"mappings":"AAgBA,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,qCAAqC;IACrC,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,6DAA6D;IAC7D,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,oDAAoD;IACpD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,kBAAkB,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,cAAc,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,mEAAmE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA6ED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,aAAa,EAAE,EACtB,OAAO,EAAE,gBAAgB,GACxB,mBAAmB,CAwJrB"}
|
|
@@ -0,0 +1,217 @@
|
|
|
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 = 4;
|
|
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 gebruikte rotatie 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
|
+
shelf.usedLength += o.a;
|
|
42
|
+
this.usedArea += o.a * o.b;
|
|
43
|
+
return { rotated: o.rotated };
|
|
44
|
+
}
|
|
45
|
+
if (last && this.totalDepth - shelf.depth + o.b <= this.width + 1e-9) {
|
|
46
|
+
this.totalDepth = this.totalDepth - shelf.depth + o.b;
|
|
47
|
+
shelf.depth = o.b;
|
|
48
|
+
shelf.usedLength += o.a;
|
|
49
|
+
this.usedArea += o.a * o.b;
|
|
50
|
+
return { rotated: o.rotated };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (this.totalDepth + o.b <= this.width + 1e-9) {
|
|
54
|
+
this.shelves.push({ depth: o.b, usedLength: o.a });
|
|
55
|
+
this.totalDepth += o.b;
|
|
56
|
+
this.usedArea += o.a * o.b;
|
|
57
|
+
return { rotated: o.rotated };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
get areaPct() {
|
|
63
|
+
const total = this.length * this.width;
|
|
64
|
+
return total > 0 ? Number(((100 * this.usedArea) / total).toFixed(1)) : 0;
|
|
65
|
+
}
|
|
66
|
+
get isEmpty() {
|
|
67
|
+
return this.shelves.length === 0;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function planByDimensions(items, carrier) {
|
|
71
|
+
const warnings = [];
|
|
72
|
+
if (!(carrier.length_cm > 0) || !(carrier.width_cm > 0) || !(carrier.height_cm > 0)) {
|
|
73
|
+
return {
|
|
74
|
+
carts: [],
|
|
75
|
+
warnings,
|
|
76
|
+
error: `De afmetingen van ladingdrager ${carrier.name} zijn niet volledig ingevuld (lengte, breedte en hoogte).`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const batches = items
|
|
80
|
+
.filter((i) => i.crates > 0)
|
|
81
|
+
.map((item) => ({
|
|
82
|
+
item,
|
|
83
|
+
left: Math.max(0, Math.round(item.crates)),
|
|
84
|
+
height: item.crate_height_cm + item.plant_height_cm,
|
|
85
|
+
}));
|
|
86
|
+
for (const b of batches) {
|
|
87
|
+
const fitsFlat = b.item.crate_length_cm <= carrier.length_cm && b.item.crate_width_cm <= carrier.width_cm;
|
|
88
|
+
const fitsTurned = b.item.crate_width_cm <= carrier.length_cm && b.item.crate_length_cm <= carrier.width_cm;
|
|
89
|
+
if (!fitsFlat && !fitsTurned) {
|
|
90
|
+
return {
|
|
91
|
+
carts: [],
|
|
92
|
+
warnings,
|
|
93
|
+
error: `Het fust van ${b.item.product_name} (${b.item.crate_length_cm} x ${b.item.crate_width_cm} cm) ` +
|
|
94
|
+
`past niet op ladingdrager ${carrier.name} (${carrier.length_cm} x ${carrier.width_cm} cm).`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (b.height > carrier.height_cm) {
|
|
98
|
+
return {
|
|
99
|
+
carts: [],
|
|
100
|
+
warnings,
|
|
101
|
+
error: `Eén laag van ${b.item.product_name} is ${round1(b.height)} cm hoog en past niet in de ` +
|
|
102
|
+
`bruikbare hoogte van ladingdrager ${carrier.name} (${carrier.height_cm} cm).`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const carts = [];
|
|
107
|
+
let guard = 0;
|
|
108
|
+
while (batches.some((b) => b.left > 0)) {
|
|
109
|
+
if (++guard > 5000) {
|
|
110
|
+
return { carts: [], warnings, error: "De indeling op afmetingen kwam niet tot een einde." };
|
|
111
|
+
}
|
|
112
|
+
const cartOrders = new Set();
|
|
113
|
+
const cartCustomers = new Set();
|
|
114
|
+
const layers = [];
|
|
115
|
+
let heightLeft = carrier.height_cm;
|
|
116
|
+
while (heightLeft > 0) {
|
|
117
|
+
if (carrier.max_layers != null && layers.length >= carrier.max_layers)
|
|
118
|
+
break;
|
|
119
|
+
// Hoogste fusten eerst; bij gelijke hoogte het grootste grondvlak eerst.
|
|
120
|
+
const pool = batches
|
|
121
|
+
.filter((b) => b.left > 0)
|
|
122
|
+
.filter((b) => allowedOnCart(b, cartOrders, cartCustomers, carrier))
|
|
123
|
+
.filter((b) => b.height <= heightLeft + 1e-9)
|
|
124
|
+
.sort((a, b) => b.height - a.height ||
|
|
125
|
+
b.item.crate_length_cm * b.item.crate_width_cm - a.item.crate_length_cm * a.item.crate_width_cm);
|
|
126
|
+
if (pool.length === 0)
|
|
127
|
+
break;
|
|
128
|
+
const space = new LayerSpace(carrier.length_cm, carrier.width_cm);
|
|
129
|
+
const perItem = new Map();
|
|
130
|
+
let layerHeight = 0;
|
|
131
|
+
for (const b of pool) {
|
|
132
|
+
while (b.left > 0) {
|
|
133
|
+
// Een laag mag niet hoger worden dan wat er nog op de drager past.
|
|
134
|
+
const newHeight = Math.max(layerHeight, b.height);
|
|
135
|
+
if (newHeight > heightLeft + 1e-9)
|
|
136
|
+
break;
|
|
137
|
+
const placed = space.place(b.item.crate_length_cm, b.item.crate_width_cm);
|
|
138
|
+
if (!placed)
|
|
139
|
+
break;
|
|
140
|
+
layerHeight = newHeight;
|
|
141
|
+
b.left -= 1;
|
|
142
|
+
const acc = perItem.get(b) ?? { crates: 0, rotated: placed.rotated };
|
|
143
|
+
acc.crates += 1;
|
|
144
|
+
perItem.set(b, acc);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (space.isEmpty || perItem.size === 0)
|
|
148
|
+
break;
|
|
149
|
+
const placements = [];
|
|
150
|
+
for (const [batch, acc] of perItem) {
|
|
151
|
+
const item = batch.item;
|
|
152
|
+
if (item.order_id)
|
|
153
|
+
cartOrders.add(item.order_id);
|
|
154
|
+
if (item.customer_id)
|
|
155
|
+
cartCustomers.add(item.customer_id);
|
|
156
|
+
placements.push({
|
|
157
|
+
order_item_id: item.order_item_id,
|
|
158
|
+
order_id: item.order_id,
|
|
159
|
+
product_id: item.product_id,
|
|
160
|
+
product_name: item.product_name,
|
|
161
|
+
crates: acc.crates,
|
|
162
|
+
plants: acc.crates * Math.max(1, item.plants_per_crate),
|
|
163
|
+
plants_per_crate: Math.max(1, item.plants_per_crate),
|
|
164
|
+
weight_kg: item.weight_per_crate != null ? Number((item.weight_per_crate * acc.crates).toFixed(2)) : null,
|
|
165
|
+
rotated: acc.rotated,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
layers.push({
|
|
169
|
+
layer_index: layers.length + 1,
|
|
170
|
+
height_cm: round1(layerHeight),
|
|
171
|
+
area_used_pct: space.areaPct,
|
|
172
|
+
placements,
|
|
173
|
+
});
|
|
174
|
+
heightLeft = round1(heightLeft - layerHeight);
|
|
175
|
+
}
|
|
176
|
+
if (layers.length === 0) {
|
|
177
|
+
return {
|
|
178
|
+
carts: [],
|
|
179
|
+
warnings,
|
|
180
|
+
error: `Er kon geen enkele laag worden gevuld op ladingdrager ${carrier.name}. ` +
|
|
181
|
+
`Controleer de afmetingen van de fusten en van de ladingdrager.`,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const usedHeight = round1(layers.reduce((s, l) => s + l.height_cm, 0));
|
|
185
|
+
const weights = layers.flatMap((l) => l.placements.map((p) => p.weight_kg));
|
|
186
|
+
carts.push({
|
|
187
|
+
cart_number: carts.length + 1,
|
|
188
|
+
carrier_code: carrier.code,
|
|
189
|
+
carrier_id: carrier.id ?? null,
|
|
190
|
+
height_used_cm: usedHeight,
|
|
191
|
+
height_available_cm: carrier.height_cm,
|
|
192
|
+
occupancy_pct: Number(((100 * usedHeight) / carrier.height_cm).toFixed(1)),
|
|
193
|
+
is_complete: false,
|
|
194
|
+
weight_kg: weights.some((w) => w != null)
|
|
195
|
+
? Number(weights.reduce((s, w) => s + (w ?? 0), 0).toFixed(2))
|
|
196
|
+
: null,
|
|
197
|
+
layers,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
// Alle dragers behalve de laatste zijn vol geladen.
|
|
201
|
+
carts.forEach((c, idx) => {
|
|
202
|
+
c.is_complete = idx < carts.length - 1 || c.occupancy_pct >= 99.9;
|
|
203
|
+
});
|
|
204
|
+
return { carts, warnings };
|
|
205
|
+
}
|
|
206
|
+
function allowedOnCart(batch, orders, customers, carrier) {
|
|
207
|
+
const oid = batch.item.order_id;
|
|
208
|
+
const cid = batch.item.customer_id;
|
|
209
|
+
if (carrier.max_orders != null && oid && !orders.has(oid) && orders.size >= carrier.max_orders)
|
|
210
|
+
return false;
|
|
211
|
+
if (carrier.max_customers != null && cid && !customers.has(cid) && customers.size >= carrier.max_customers)
|
|
212
|
+
return false;
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
function round1(n) {
|
|
216
|
+
return Number(n.toFixed(1));
|
|
217
|
+
}
|