@flowselections/logistiek-engine 1.0.1

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.
Files changed (175) hide show
  1. package/README.md +24 -0
  2. package/dist-lib/_core-safelist.d.ts +2 -0
  3. package/dist-lib/_core-safelist.d.ts.map +1 -0
  4. package/dist-lib/_core-safelist.js +15 -0
  5. package/dist-lib/components/BeladingIndelingCard.d.ts +2 -0
  6. package/dist-lib/components/BeladingIndelingCard.d.ts.map +1 -0
  7. package/dist-lib/components/BeladingIndelingCard.js +77 -0
  8. package/dist-lib/components/BufferDebugCard.d.ts +19 -0
  9. package/dist-lib/components/BufferDebugCard.d.ts.map +1 -0
  10. package/dist-lib/components/BufferDebugCard.js +69 -0
  11. package/dist-lib/components/CapaciteitMeldingenCard.d.ts +2 -0
  12. package/dist-lib/components/CapaciteitMeldingenCard.d.ts.map +1 -0
  13. package/dist-lib/components/CapaciteitMeldingenCard.js +40 -0
  14. package/dist-lib/components/LadingdragersPage.d.ts +2 -0
  15. package/dist-lib/components/LadingdragersPage.d.ts.map +1 -0
  16. package/dist-lib/components/LadingdragersPage.js +102 -0
  17. package/dist-lib/components/LogistiekEngineLogo.d.ts +6 -0
  18. package/dist-lib/components/LogistiekEngineLogo.d.ts.map +1 -0
  19. package/dist-lib/components/LogistiekEngineLogo.js +4 -0
  20. package/dist-lib/components/LogistiekEnginePage.d.ts +2 -0
  21. package/dist-lib/components/LogistiekEnginePage.d.ts.map +1 -0
  22. package/dist-lib/components/LogistiekEnginePage.js +53 -0
  23. package/dist-lib/components/ModulePromptsPage.d.ts +2 -0
  24. package/dist-lib/components/ModulePromptsPage.d.ts.map +1 -0
  25. package/dist-lib/components/ModulePromptsPage.js +58 -0
  26. package/dist-lib/components/RegiePage.d.ts +2 -0
  27. package/dist-lib/components/RegiePage.d.ts.map +1 -0
  28. package/dist-lib/components/RegiePage.js +82 -0
  29. package/dist-lib/components/RuleEnginePage.d.ts +7 -0
  30. package/dist-lib/components/RuleEnginePage.d.ts.map +1 -0
  31. package/dist-lib/components/RuleEnginePage.js +114 -0
  32. package/dist-lib/components/StrategieDialog.d.ts +11 -0
  33. package/dist-lib/components/StrategieDialog.d.ts.map +1 -0
  34. package/dist-lib/components/StrategieDialog.js +38 -0
  35. package/dist-lib/components/VertragingMeldingenCard.d.ts +2 -0
  36. package/dist-lib/components/VertragingMeldingenCard.d.ts.map +1 -0
  37. package/dist-lib/components/VertragingMeldingenCard.js +32 -0
  38. package/dist-lib/components/VoorraadMeldingenCard.d.ts +2 -0
  39. package/dist-lib/components/VoorraadMeldingenCard.d.ts.map +1 -0
  40. package/dist-lib/components/VoorraadMeldingenCard.js +26 -0
  41. package/dist-lib/components/rule-engine/CollapsibleGroup.d.ts +12 -0
  42. package/dist-lib/components/rule-engine/CollapsibleGroup.d.ts.map +1 -0
  43. package/dist-lib/components/rule-engine/CollapsibleGroup.js +11 -0
  44. package/dist-lib/components/rule-engine/ConditionsEditor.d.ts +8 -0
  45. package/dist-lib/components/rule-engine/ConditionsEditor.d.ts.map +1 -0
  46. package/dist-lib/components/rule-engine/ConditionsEditor.js +31 -0
  47. package/dist-lib/components/rule-engine/FieldHelp.d.ts +14 -0
  48. package/dist-lib/components/rule-engine/FieldHelp.d.ts.map +1 -0
  49. package/dist-lib/components/rule-engine/FieldHelp.js +18 -0
  50. package/dist-lib/components/rule-engine/FloridayConnectionBanner.d.ts +2 -0
  51. package/dist-lib/components/rule-engine/FloridayConnectionBanner.d.ts.map +1 -0
  52. package/dist-lib/components/rule-engine/FloridayConnectionBanner.js +41 -0
  53. package/dist-lib/components/rule-engine/ImpactPanel.d.ts +5 -0
  54. package/dist-lib/components/rule-engine/ImpactPanel.d.ts.map +1 -0
  55. package/dist-lib/components/rule-engine/ImpactPanel.js +25 -0
  56. package/dist-lib/components/rule-engine/RuleFieldInput.d.ts +9 -0
  57. package/dist-lib/components/rule-engine/RuleFieldInput.d.ts.map +1 -0
  58. package/dist-lib/components/rule-engine/RuleFieldInput.js +80 -0
  59. package/dist-lib/components/rule-engine/VersionsPanel.d.ts +10 -0
  60. package/dist-lib/components/rule-engine/VersionsPanel.d.ts.map +1 -0
  61. package/dist-lib/components/rule-engine/VersionsPanel.js +16 -0
  62. package/dist-lib/components/settings/LogistiekEngineSettingsCard.d.ts +2 -0
  63. package/dist-lib/components/settings/LogistiekEngineSettingsCard.d.ts.map +1 -0
  64. package/dist-lib/components/settings/LogistiekEngineSettingsCard.js +22 -0
  65. package/dist-lib/hooks/useCapaciteitMeldingen.d.ts +20 -0
  66. package/dist-lib/hooks/useCapaciteitMeldingen.d.ts.map +1 -0
  67. package/dist-lib/hooks/useCapaciteitMeldingen.js +46 -0
  68. package/dist-lib/hooks/useDocumentLayouts.d.ts +13 -0
  69. package/dist-lib/hooks/useDocumentLayouts.d.ts.map +1 -0
  70. package/dist-lib/hooks/useDocumentLayouts.js +54 -0
  71. package/dist-lib/hooks/useLadingdragers.d.ts +16 -0
  72. package/dist-lib/hooks/useLadingdragers.d.ts.map +1 -0
  73. package/dist-lib/hooks/useLadingdragers.js +86 -0
  74. package/dist-lib/hooks/useLogistiekStrategieen.d.ts +29 -0
  75. package/dist-lib/hooks/useLogistiekStrategieen.d.ts.map +1 -0
  76. package/dist-lib/hooks/useLogistiekStrategieen.js +111 -0
  77. package/dist-lib/hooks/useLogistiekTaken.d.ts +89 -0
  78. package/dist-lib/hooks/useLogistiekTaken.d.ts.map +1 -0
  79. package/dist-lib/hooks/useLogistiekTaken.js +314 -0
  80. package/dist-lib/hooks/usePickstations.d.ts +10 -0
  81. package/dist-lib/hooks/usePickstations.d.ts.map +1 -0
  82. package/dist-lib/hooks/usePickstations.js +51 -0
  83. package/dist-lib/hooks/useRecalcJob.d.ts +24 -0
  84. package/dist-lib/hooks/useRecalcJob.d.ts.map +1 -0
  85. package/dist-lib/hooks/useRecalcJob.js +83 -0
  86. package/dist-lib/hooks/useRuleEngine.d.ts +48 -0
  87. package/dist-lib/hooks/useRuleEngine.d.ts.map +1 -0
  88. package/dist-lib/hooks/useRuleEngine.js +222 -0
  89. package/dist-lib/hooks/useVertragingMeldingen.d.ts +29 -0
  90. package/dist-lib/hooks/useVertragingMeldingen.d.ts.map +1 -0
  91. package/dist-lib/hooks/useVertragingMeldingen.js +68 -0
  92. package/dist-lib/hooks/useVoorraadMeldingen.d.ts +30 -0
  93. package/dist-lib/hooks/useVoorraadMeldingen.d.ts.map +1 -0
  94. package/dist-lib/hooks/useVoorraadMeldingen.js +70 -0
  95. package/dist-lib/index.d.ts +36 -0
  96. package/dist-lib/index.d.ts.map +1 -0
  97. package/dist-lib/index.js +52 -0
  98. package/dist-lib/integrations/supabase/auth-attacher.d.ts +2 -0
  99. package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -0
  100. package/dist-lib/integrations/supabase/auth-attacher.js +12 -0
  101. package/dist-lib/integrations/supabase/auth-middleware.d.ts +19781 -0
  102. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -0
  103. package/dist-lib/integrations/supabase/auth-middleware.js +52 -0
  104. package/dist-lib/integrations/supabase/client.d.ts +19777 -0
  105. package/dist-lib/integrations/supabase/client.d.ts.map +1 -0
  106. package/dist-lib/integrations/supabase/client.js +13 -0
  107. package/dist-lib/integrations/supabase/client.server.d.ts +19777 -0
  108. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -0
  109. package/dist-lib/integrations/supabase/client.server.js +30 -0
  110. package/dist-lib/integrations/supabase/types.d.ts +20355 -0
  111. package/dist-lib/integrations/supabase/types.d.ts.map +1 -0
  112. package/dist-lib/integrations/supabase/types.js +78 -0
  113. package/dist-lib/lib/bufferMatching.d.ts +26 -0
  114. package/dist-lib/lib/bufferMatching.d.ts.map +1 -0
  115. package/dist-lib/lib/bufferMatching.js +73 -0
  116. package/dist-lib/lib/bulkLoadCarrierPlanning.server.d.ts +23 -0
  117. package/dist-lib/lib/bulkLoadCarrierPlanning.server.d.ts.map +1 -0
  118. package/dist-lib/lib/bulkLoadCarrierPlanning.server.js +126 -0
  119. package/dist-lib/lib/floridayDocuments.functions.d.ts +59415 -0
  120. package/dist-lib/lib/floridayDocuments.functions.d.ts.map +1 -0
  121. package/dist-lib/lib/floridayDocuments.functions.js +68 -0
  122. package/dist-lib/lib/floridayDocuments.server.d.ts +51 -0
  123. package/dist-lib/lib/floridayDocuments.server.d.ts.map +1 -0
  124. package/dist-lib/lib/floridayDocuments.server.js +109 -0
  125. package/dist-lib/lib/loadCarrierPlanner.d.ts +71 -0
  126. package/dist-lib/lib/loadCarrierPlanner.d.ts.map +1 -0
  127. package/dist-lib/lib/loadCarrierPlanner.js +374 -0
  128. package/dist-lib/lib/loadCarrierPlanning.functions.d.ts +98984 -0
  129. package/dist-lib/lib/loadCarrierPlanning.functions.d.ts.map +1 -0
  130. package/dist-lib/lib/loadCarrierPlanning.functions.js +106 -0
  131. package/dist-lib/lib/loadCarrierPlanning.server.d.ts +46 -0
  132. package/dist-lib/lib/loadCarrierPlanning.server.d.ts.map +1 -0
  133. package/dist-lib/lib/loadCarrierPlanning.server.js +515 -0
  134. package/dist-lib/lib/loadCarrierTaskSync.server.d.ts +3 -0
  135. package/dist-lib/lib/loadCarrierTaskSync.server.d.ts.map +1 -0
  136. package/dist-lib/lib/loadCarrierTaskSync.server.js +56 -0
  137. package/dist-lib/lib/loadCarrierTypes.d.ts +19 -0
  138. package/dist-lib/lib/loadCarrierTypes.d.ts.map +1 -0
  139. package/dist-lib/lib/loadCarrierTypes.js +41 -0
  140. package/dist-lib/lib/logisticsPlanner.d.ts +56 -0
  141. package/dist-lib/lib/logisticsPlanner.d.ts.map +1 -0
  142. package/dist-lib/lib/logisticsPlanner.js +273 -0
  143. package/dist-lib/lib/logisticsRecalcJob.functions.d.ts +39575 -0
  144. package/dist-lib/lib/logisticsRecalcJob.functions.d.ts.map +1 -0
  145. package/dist-lib/lib/logisticsRecalcJob.functions.js +30 -0
  146. package/dist-lib/lib/logisticsRecalcJob.server.d.ts +32 -0
  147. package/dist-lib/lib/logisticsRecalcJob.server.d.ts.map +1 -0
  148. package/dist-lib/lib/logisticsRecalcJob.server.js +116 -0
  149. package/dist-lib/lib/logisticsRecalculate.functions.d.ts +19792 -0
  150. package/dist-lib/lib/logisticsRecalculate.functions.d.ts.map +1 -0
  151. package/dist-lib/lib/logisticsRecalculate.functions.js +35 -0
  152. package/dist-lib/lib/logisticsTasks.d.ts +23 -0
  153. package/dist-lib/lib/logisticsTasks.d.ts.map +1 -0
  154. package/dist-lib/lib/logisticsTasks.js +92 -0
  155. package/dist-lib/lib/modulePrompts.d.ts +31 -0
  156. package/dist-lib/lib/modulePrompts.d.ts.map +1 -0
  157. package/dist-lib/lib/modulePrompts.js +1195 -0
  158. package/dist-lib/lib/ruleEngineImpact.d.ts +27 -0
  159. package/dist-lib/lib/ruleEngineImpact.d.ts.map +1 -0
  160. package/dist-lib/lib/ruleEngineImpact.js +257 -0
  161. package/dist-lib/lib/ruleEngineSchema.d.ts +69 -0
  162. package/dist-lib/lib/ruleEngineSchema.d.ts.map +1 -0
  163. package/dist-lib/lib/ruleEngineSchema.js +899 -0
  164. package/dist-lib/lib/ruleEngineWizard.d.ts +17 -0
  165. package/dist-lib/lib/ruleEngineWizard.d.ts.map +1 -0
  166. package/dist-lib/lib/ruleEngineWizard.js +204 -0
  167. package/dist-lib/lib/utils.d.ts +3 -0
  168. package/dist-lib/lib/utils.d.ts.map +1 -0
  169. package/dist-lib/lib/utils.js +5 -0
  170. package/dist-lib/lib/validationSchemas.d.ts +15 -0
  171. package/dist-lib/lib/validationSchemas.d.ts.map +1 -0
  172. package/dist-lib/lib/validationSchemas.js +25 -0
  173. package/dist-lib/styles.css +1 -0
  174. package/package.json +113 -0
  175. package/public/logo.png +0 -0
@@ -0,0 +1,374 @@
1
+ // ============================================================================
2
+ // src/lib/loadCarrierPlanner.ts — beladingindeling voor ALLE ladingdragers
3
+ // ============================================================================
4
+ // Pure functie: geen database, geen netwerk, volledig synchroon en
5
+ // deterministisch. Zelfde algoritme als de oorspronkelijke karrenindeling,
6
+ // maar met een capaciteitsprofiel per ladingdrager als extra invoer.
7
+ //
8
+ // Twee onafhankelijke capaciteitsassen per ladingdrager:
9
+ // - verticaal : totalHeightPct <= 100
10
+ // - horizontaal: crateShare per laag <= 1
11
+ // ============================================================================
12
+ export const LOAD_CARRIER_PLANNER_VERSION = 2;
13
+ const EPS = 1e-9;
14
+ function clampInt(value, min) {
15
+ const n = typeof value === "string" ? Number(value) : value;
16
+ return Math.max(min, Math.round(Number.isFinite(n) ? n : min));
17
+ }
18
+ /** Effectieve lagen: het minimum van de productbelading en de ladingdrager. */
19
+ function effectiveLayers(item, carrier) {
20
+ const carrierLayers = Math.max(1, Math.round(carrier.layers_per_carrier || 1));
21
+ if (carrier.capacity_model === "single" || carrier.capacity_model === "none")
22
+ return 1;
23
+ return Math.max(1, Math.min(item.layers_per_cart, carrierLayers));
24
+ }
25
+ /** Effectieve fusten per laag: begrensd door de ladingdrager. */
26
+ function effectiveCratesPerLayer(item, carrier) {
27
+ const cap = carrier.max_crates_per_layer;
28
+ const base = Math.max(1, item.crates_per_layer);
29
+ if (typeof cap === "number" && cap > 0)
30
+ return Math.max(1, Math.min(base, Math.round(cap)));
31
+ return base;
32
+ }
33
+ // ---------------------------------------------------------------------------
34
+ // Plaatsingsregels
35
+ // ---------------------------------------------------------------------------
36
+ function placeAsFreshLayer(cart, batch, batchIdx, crates) {
37
+ if (cart.totalHeightPct + batch.layerHeightPct > 100 + EPS)
38
+ return false;
39
+ cart.layers.push({
40
+ heightPct: batch.layerHeightPct,
41
+ crateShare: crates / batch.cratesPerLayer,
42
+ entries: [{ batchIdx, crates }],
43
+ });
44
+ cart.totalHeightPct += batch.layerHeightPct;
45
+ return true;
46
+ }
47
+ function placeInLayer(cart, batch, batchIdx, crates, r) {
48
+ const partyShare = crates / batch.cratesPerLayer;
49
+ const maxShare = r.opts.maxPartySharePerLayerPct / 100;
50
+ // REGEL 1 — één partij mag nooit meer dan X% van een laag beslaan
51
+ if (partyShare > maxShare + EPS)
52
+ return false;
53
+ // REGEL 2 — never-mix producten en niet-mengbare ladingdragers
54
+ const neverMix = r.opts.carrier.allow_mixing === false ||
55
+ (batch.input.product_id ? r.opts.neverMixProductIds.has(batch.input.product_id) : false);
56
+ if (neverMix)
57
+ return false;
58
+ for (const layer of cart.layers) {
59
+ // REGEL 3 — max aantal verschillende beladingen per laag
60
+ if (layer.entries.length >= r.opts.maxBeladingenPerLayer)
61
+ continue;
62
+ // REGEL 4 — dezelfde partij niet twee keer in dezelfde laag
63
+ if (layer.entries.some((e) => e.batchIdx === batchIdx))
64
+ continue;
65
+ // REGEL 5 — vloeroppervlak van de laag mag niet over 100%
66
+ if (layer.crateShare + partyShare > 1 + EPS)
67
+ continue;
68
+ // REGEL 6 — hoogte: bij mengen telt de HOOGSTE partij
69
+ const newHeight = Math.max(layer.heightPct, batch.layerHeightPct);
70
+ const heightDelta = newHeight - layer.heightPct;
71
+ if (cart.totalHeightPct + heightDelta > 100 + EPS)
72
+ continue;
73
+ layer.entries.push({ batchIdx, crates });
74
+ layer.crateShare += partyShare;
75
+ layer.heightPct = newHeight;
76
+ cart.totalHeightPct += heightDelta;
77
+ return true;
78
+ }
79
+ return false;
80
+ }
81
+ /** Extra grenzen die niet over hoogte of oppervlak gaan. */
82
+ function acceptsBatch(cart, batch, crates, r) {
83
+ const carrier = r.opts.carrier;
84
+ const orderId = batch.input.order_id ?? null;
85
+ if (orderId && !cart.orderIds.has(orderId) && r.opts.maxOrders && cart.orderIds.size >= r.opts.maxOrders) {
86
+ return false;
87
+ }
88
+ const customerId = batch.input.customer_id ?? null;
89
+ if (customerId &&
90
+ !cart.customerIds.has(customerId) &&
91
+ r.opts.maxCustomers &&
92
+ cart.customerIds.size >= r.opts.maxCustomers) {
93
+ return false;
94
+ }
95
+ if (typeof carrier.max_boxes === "number" && carrier.max_boxes > 0) {
96
+ if (cart.boxes + crates > carrier.max_boxes)
97
+ return false;
98
+ }
99
+ if (typeof carrier.max_weight_kg === "number" && carrier.max_weight_kg > 0) {
100
+ const perCrate = Number(batch.input.weight_per_crate ?? 0) || 0;
101
+ if (perCrate > 0 && cart.weightKg + perCrate * crates > carrier.max_weight_kg + EPS)
102
+ return false;
103
+ }
104
+ return true;
105
+ }
106
+ function registerPlacement(cart, batch, crates) {
107
+ if (batch.input.order_id)
108
+ cart.orderIds.add(batch.input.order_id);
109
+ if (batch.input.customer_id)
110
+ cart.customerIds.add(batch.input.customer_id);
111
+ cart.boxes += crates;
112
+ cart.weightKg += (Number(batch.input.weight_per_crate ?? 0) || 0) * crates;
113
+ }
114
+ function tryPlaceBatch(cart, batch, batchIdx, r) {
115
+ const cpl = batch.cratesPerLayer;
116
+ let placed = 0;
117
+ let remaining = batch.cratesRemaining;
118
+ // 1) zo veel mogelijk VOLLE eigen lagen
119
+ while (remaining >= cpl && cart.totalHeightPct + batch.layerHeightPct <= 100 + EPS) {
120
+ if (!acceptsBatch(cart, batch, cpl, r))
121
+ break;
122
+ if (!placeAsFreshLayer(cart, batch, batchIdx, cpl))
123
+ break;
124
+ registerPlacement(cart, batch, cpl);
125
+ remaining -= cpl;
126
+ placed += cpl;
127
+ }
128
+ // 2) het restant: eerst bijmengen, anders eigen (deels lege) laag
129
+ if (remaining > 0) {
130
+ const partial = Math.min(remaining, Math.ceil(cpl * 0.5));
131
+ if (acceptsBatch(cart, batch, partial, r) && placeInLayer(cart, batch, batchIdx, partial, r)) {
132
+ registerPlacement(cart, batch, partial);
133
+ remaining -= partial;
134
+ placed += partial;
135
+ }
136
+ else if (acceptsBatch(cart, batch, remaining, r) && placeAsFreshLayer(cart, batch, batchIdx, remaining)) {
137
+ registerPlacement(cart, batch, remaining);
138
+ placed += remaining;
139
+ remaining = 0;
140
+ }
141
+ }
142
+ return placed;
143
+ }
144
+ // ---------------------------------------------------------------------------
145
+ // Samenvatten
146
+ // ---------------------------------------------------------------------------
147
+ function summarizeCart(cart, rests, cartNumber, r) {
148
+ const byBatch = new Map();
149
+ for (const layer of cart.layers) {
150
+ const shareSum = layer.entries.reduce((s, e) => s + e.crates, 0);
151
+ for (const e of layer.entries) {
152
+ const cur = byBatch.get(e.batchIdx) ?? { crates: 0, layerFraction: 0, heightPct: 0 };
153
+ const ratio = shareSum > 0 ? e.crates / shareSum : 0;
154
+ cur.crates += e.crates;
155
+ cur.layerFraction += ratio;
156
+ cur.heightPct += layer.heightPct * ratio;
157
+ byBatch.set(e.batchIdx, cur);
158
+ }
159
+ }
160
+ const totalHeight = cart.totalHeightPct;
161
+ const items = [];
162
+ let weight = 0;
163
+ for (const [batchIdx, agg] of byBatch) {
164
+ const input = rests[batchIdx].input;
165
+ const perCrate = Number(input.weight_per_crate ?? 0) || 0;
166
+ weight += perCrate * agg.crates;
167
+ items.push({
168
+ order_item_id: input.order_item_id,
169
+ order_id: input.order_id ?? null,
170
+ product_id: input.product_id,
171
+ product_name: input.product_name,
172
+ crates: agg.crates,
173
+ plants: agg.crates * input.plants_per_crate,
174
+ boxes: agg.crates,
175
+ weight_kg: perCrate > 0 ? Math.round(perCrate * agg.crates * 100) / 100 : null,
176
+ plants_per_crate: input.plants_per_crate,
177
+ crates_per_layer: rests[batchIdx].cratesPerLayer,
178
+ layers_per_cart: Math.max(1, Math.round(100 / rests[batchIdx].layerHeightPct)),
179
+ layer_fraction: totalHeight > 0 ? agg.heightPct / totalHeight : 0,
180
+ layer_share_pct: agg.heightPct,
181
+ });
182
+ }
183
+ return {
184
+ cart_number: cartNumber,
185
+ carrier_type: r.opts.carrier.code,
186
+ carrier_type_id: r.opts.carrier.id ?? null,
187
+ source: "auto",
188
+ is_complete: Math.abs(cart.totalHeightPct - 100) < 1e-6,
189
+ occupancy_pct: Math.round(cart.totalHeightPct * 100) / 100,
190
+ total_layers_used: cart.layers.length,
191
+ max_layers: Math.max(1, Math.round(r.opts.carrier.layers_per_carrier || 1)),
192
+ weight_kg: weight > 0 ? Math.round(weight * 100) / 100 : null,
193
+ items,
194
+ };
195
+ }
196
+ function newCart() {
197
+ return {
198
+ layers: [],
199
+ totalHeightPct: 0,
200
+ boxes: 0,
201
+ weightKg: 0,
202
+ orderIds: new Set(),
203
+ customerIds: new Set(),
204
+ };
205
+ }
206
+ function fullCart(input, crates, cratesPerLayer, layers, cartNumber, r) {
207
+ const perCrate = Number(input.weight_per_crate ?? 0) || 0;
208
+ return {
209
+ cart_number: cartNumber,
210
+ carrier_type: r.opts.carrier.code,
211
+ carrier_type_id: r.opts.carrier.id ?? null,
212
+ source: "auto",
213
+ is_complete: true,
214
+ occupancy_pct: 100,
215
+ total_layers_used: layers,
216
+ max_layers: Math.max(1, Math.round(r.opts.carrier.layers_per_carrier || 1)),
217
+ weight_kg: perCrate > 0 ? Math.round(perCrate * crates * 100) / 100 : null,
218
+ items: [
219
+ {
220
+ order_item_id: input.order_item_id,
221
+ order_id: input.order_id ?? null,
222
+ product_id: input.product_id,
223
+ product_name: input.product_name,
224
+ crates,
225
+ plants: crates * input.plants_per_crate,
226
+ boxes: crates,
227
+ weight_kg: perCrate > 0 ? Math.round(perCrate * crates * 100) / 100 : null,
228
+ plants_per_crate: input.plants_per_crate,
229
+ crates_per_layer: cratesPerLayer,
230
+ layers_per_cart: layers,
231
+ layer_fraction: 1,
232
+ layer_share_pct: 100,
233
+ },
234
+ ],
235
+ };
236
+ }
237
+ function sortInputs(items, sortOrder) {
238
+ const list = [...items];
239
+ switch (sortOrder) {
240
+ case "customer":
241
+ return list.sort((a, b) => String(a.customer_id ?? "").localeCompare(String(b.customer_id ?? "")));
242
+ case "product":
243
+ return list.sort((a, b) => a.product_name.localeCompare(b.product_name));
244
+ case "size":
245
+ return list.sort((a, b) => b.plants_per_crate - a.plants_per_crate);
246
+ case "none":
247
+ return list;
248
+ default:
249
+ return list;
250
+ }
251
+ }
252
+ // ---------------------------------------------------------------------------
253
+ // Hoofdfunctie
254
+ // ---------------------------------------------------------------------------
255
+ export function planLoadUnits(inputs, options) {
256
+ const carrier = options.carrier;
257
+ const r = {
258
+ opts: {
259
+ carrier,
260
+ maxPartySharePerLayerPct: options.maxPartySharePerLayerPct ?? 50,
261
+ maxBeladingenPerLayer: options.maxBeladingenPerLayer ?? 2,
262
+ neverMixProductIds: options.neverMixProductIds ?? new Set(),
263
+ maxOrders: options.maxOrders ?? null,
264
+ maxCustomers: options.maxCustomers ?? null,
265
+ oneCustomerPerLayer: options.oneCustomerPerLayer ?? false,
266
+ ownCarrierFromLayers: options.ownCarrierFromLayers ?? null,
267
+ sortOrder: options.sortOrder ?? "none",
268
+ },
269
+ };
270
+ const warnings = [];
271
+ const carts = [];
272
+ const items = sortInputs((Array.isArray(inputs) ? inputs : [])
273
+ .map((i) => ({
274
+ ...i,
275
+ plants: Math.max(0, Math.round(Number(i.plants) || 0)),
276
+ plants_per_crate: clampInt(i.plants_per_crate, 1),
277
+ crates_per_layer: clampInt(i.crates_per_layer, 1),
278
+ layers_per_cart: clampInt(i.layers_per_cart, 1),
279
+ }))
280
+ .filter((i) => i.plants > 0), r.opts.sortOrder);
281
+ // --- Model "none": elke partij is één losse eenheid, geen menging ---------
282
+ if (carrier.capacity_model === "none") {
283
+ let n = 0;
284
+ for (const it of items) {
285
+ const crates = Math.ceil(it.plants / it.plants_per_crate);
286
+ carts.push(fullCart(it, crates, crates, 1, ++n, r));
287
+ }
288
+ return { carts, warnings };
289
+ }
290
+ const rests = [];
291
+ let cartNumber = 0;
292
+ // --- STAP A: volledige ladingdragers per artikel --------------------------
293
+ for (const it of items) {
294
+ const layers = effectiveLayers(it, carrier);
295
+ const cpl = effectiveCratesPerLayer(it, carrier);
296
+ let cratesPerCart = cpl * layers;
297
+ if (typeof carrier.max_boxes === "number" && carrier.max_boxes > 0) {
298
+ cratesPerCart = Math.min(cratesPerCart, carrier.max_boxes);
299
+ }
300
+ const totalCrates = Math.ceil(it.plants / it.plants_per_crate);
301
+ const fullCarts = Math.floor(totalCrates / cratesPerCart);
302
+ let remainderCrates = totalCrates - fullCarts * cratesPerCart;
303
+ for (let c = 0; c < fullCarts; c++) {
304
+ carts.push(fullCart(it, cratesPerCart, cpl, layers, ++cartNumber, r));
305
+ }
306
+ // Partij die zelfstandig genoeg volle lagen vult krijgt een eigen drager.
307
+ const own = r.opts.ownCarrierFromLayers;
308
+ if (own && own > 0 && remainderCrates >= cpl * own) {
309
+ const usedLayers = Math.ceil(remainderCrates / cpl);
310
+ const unit = fullCart(it, remainderCrates, cpl, usedLayers, ++cartNumber, r);
311
+ unit.is_complete = false;
312
+ unit.occupancy_pct = Math.round((usedLayers / layers) * 10000) / 100;
313
+ unit.items[0].layer_share_pct = unit.occupancy_pct;
314
+ carts.push(unit);
315
+ remainderCrates = 0;
316
+ }
317
+ if (remainderCrates > 0) {
318
+ rests.push({
319
+ input: it,
320
+ cratesRemaining: remainderCrates,
321
+ layerHeightPct: 100 / layers,
322
+ cratesPerLayer: cpl,
323
+ beladingKey: `${it.plants_per_crate}x${cpl}x${layers}`,
324
+ });
325
+ }
326
+ }
327
+ // --- Sorteren van de restpartijen: first-fit decreasing -------------------
328
+ rests.sort((a, b) => {
329
+ if (a.beladingKey !== b.beladingKey)
330
+ return a.beladingKey.localeCompare(b.beladingKey);
331
+ return b.cratesRemaining - a.cratesRemaining;
332
+ });
333
+ // --- STAP B: de hoofdlus --------------------------------------------------
334
+ const openCarts = [];
335
+ for (let bi = 0; bi < rests.length; bi++) {
336
+ const batch = rests[bi];
337
+ let guard = 0;
338
+ while (batch.cratesRemaining > 0 && guard++ < 1000) {
339
+ let placed = 0;
340
+ for (const cart of openCarts) {
341
+ placed = tryPlaceBatch(cart, batch, bi, r);
342
+ if (placed > 0) {
343
+ batch.cratesRemaining -= placed;
344
+ break;
345
+ }
346
+ }
347
+ if (placed === 0) {
348
+ const cart = newCart();
349
+ placed = tryPlaceBatch(cart, batch, bi, r);
350
+ if (placed === 0) {
351
+ warnings.push(`Partij "${batch.input.product_name}" past niet volgens de veiligheidsregels van ${carrier.name} — geplaatst als losse ladingdrager.`);
352
+ cart.layers.push({
353
+ heightPct: 100,
354
+ crateShare: 1,
355
+ entries: [{ batchIdx: bi, crates: batch.cratesRemaining }],
356
+ });
357
+ cart.totalHeightPct = 100;
358
+ registerPlacement(cart, batch, batch.cratesRemaining);
359
+ placed = batch.cratesRemaining;
360
+ }
361
+ openCarts.push(cart);
362
+ batch.cratesRemaining -= placed;
363
+ }
364
+ }
365
+ }
366
+ for (const cart of openCarts) {
367
+ carts.push(summarizeCart(cart, rests, ++cartNumber, r));
368
+ }
369
+ // Doornummeren zodat de volgorde altijd 1..n is.
370
+ carts.forEach((c, idx) => {
371
+ c.cart_number = idx + 1;
372
+ });
373
+ return { carts, warnings };
374
+ }