@flowselections/floriday-verkoop-module 1.0.38 → 1.0.40

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 (58) hide show
  1. package/dist-lib/components/verkoop/VerkoopOrdersPage.d.ts.map +1 -1
  2. package/dist-lib/components/verkoop/VerkoopOrdersPage.js +50 -87
  3. package/dist-lib/integrations/supabase/auth-middleware.d.ts +849 -60
  4. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  5. package/dist-lib/integrations/supabase/client.d.ts +849 -60
  6. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  7. package/dist-lib/integrations/supabase/client.server.d.ts +849 -60
  8. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  9. package/dist-lib/integrations/supabase/types.d.ts +830 -39
  10. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  11. package/dist-lib/lib/cart-lock.functions.d.ts +849 -60
  12. package/dist-lib/lib/cart-lock.functions.d.ts.map +1 -1
  13. package/dist-lib/lib/cart-planning-settings.functions.d.ts +8679 -0
  14. package/dist-lib/lib/cart-planning-settings.functions.d.ts.map +1 -1
  15. package/dist-lib/lib/cart-replan.functions.d.ts +2565 -198
  16. package/dist-lib/lib/cart-replan.functions.d.ts.map +1 -1
  17. package/dist-lib/lib/create-order.functions.d.ts +11835 -0
  18. package/dist-lib/lib/create-order.functions.d.ts.map +1 -1
  19. package/dist-lib/lib/floriday-auto-fulfillment.functions.d.ts +2565 -198
  20. package/dist-lib/lib/floriday-auto-fulfillment.functions.d.ts.map +1 -1
  21. package/dist-lib/lib/floriday-logistic-labels.functions.d.ts +849 -60
  22. package/dist-lib/lib/floriday-logistic-labels.functions.d.ts.map +1 -1
  23. package/dist-lib/lib/floriday-mirror.functions.d.ts +849 -60
  24. package/dist-lib/lib/floriday-mirror.functions.d.ts.map +1 -1
  25. package/dist-lib/lib/floriday-public.functions.d.ts +5523 -0
  26. package/dist-lib/lib/floriday-public.functions.d.ts.map +1 -1
  27. package/dist-lib/lib/floriday-writes.functions.d.ts +4505 -560
  28. package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
  29. package/dist-lib/lib/load-carriers.functions.d.ts +849 -60
  30. package/dist-lib/lib/load-carriers.functions.d.ts.map +1 -1
  31. package/dist-lib/lib/logistics-engine.functions.d.ts +4511 -562
  32. package/dist-lib/lib/logistics-engine.functions.d.ts.map +1 -1
  33. package/dist-lib/lib/logistics-stock.functions.d.ts +1578 -0
  34. package/dist-lib/lib/logistics-stock.functions.d.ts.map +1 -1
  35. package/dist-lib/lib/order-addresses.functions.d.ts +849 -60
  36. package/dist-lib/lib/order-addresses.functions.d.ts.map +1 -1
  37. package/dist-lib/lib/order-buffers.functions.d.ts +2565 -198
  38. package/dist-lib/lib/order-buffers.functions.d.ts.map +1 -1
  39. package/dist-lib/lib/order-carrier.functions.d.ts +1578 -0
  40. package/dist-lib/lib/order-carrier.functions.d.ts.map +1 -1
  41. package/dist-lib/lib/order-pickup.functions.d.ts +1578 -0
  42. package/dist-lib/lib/order-pickup.functions.d.ts.map +1 -1
  43. package/dist-lib/lib/order-shipments.functions.d.ts +3535 -379
  44. package/dist-lib/lib/order-shipments.functions.d.ts.map +1 -1
  45. package/dist-lib/lib/salesorders-list.functions.d.ts +20393 -0
  46. package/dist-lib/lib/salesorders-list.functions.d.ts.map +1 -0
  47. package/dist-lib/lib/salesorders-list.functions.js +21 -0
  48. package/dist-lib/lib/salesorders-list.server.d.ts +28 -0
  49. package/dist-lib/lib/salesorders-list.server.d.ts.map +1 -0
  50. package/dist-lib/lib/salesorders-list.server.js +713 -0
  51. package/dist-lib/lib/salesorders.functions.d.ts +4505 -560
  52. package/dist-lib/lib/salesorders.functions.d.ts.map +1 -1
  53. package/dist-lib/lib/tradeitems.functions.d.ts +849 -60
  54. package/dist-lib/lib/tradeitems.functions.d.ts.map +1 -1
  55. package/dist-lib/lib/useSalesOrdersQuery.d.ts +24 -3
  56. package/dist-lib/lib/useSalesOrdersQuery.d.ts.map +1 -1
  57. package/dist-lib/lib/useSalesOrdersQuery.js +49 -594
  58. package/package.json +2 -2
@@ -0,0 +1,713 @@
1
+ import { centralOrderNumber, computeTotals, mapJsonbToSalesOrderDTO, } from "./salesorders-mapping";
2
+ import { cratePackageName, loadCarrierName, mergeTypeNames } from "./load-carrier-labels";
3
+ import { normalizeLoadCarrierCode } from "./load-carriers";
4
+ import { normalizeTime, resolveDeliveryTimeForDate } from "./carrier-availability";
5
+ import { expandOrderItemsForPlanner } from "./expand-group-order-items";
6
+ import { planCarts } from "./cart-planner";
7
+ const FO_CAP = 20000;
8
+ const LOCAL_CAP = 5000;
9
+ const PROPOSAL_CAP = 2000;
10
+ const EXPORT_CAP = 10000;
11
+ const PAGE_SIZE_MAX = 200;
12
+ const mailboxSources = new Set([
13
+ "mailbox",
14
+ "mailbox_order",
15
+ "mailbox-bestellingen",
16
+ "mailbox_bestellingen",
17
+ "mail",
18
+ "email",
19
+ "email_order",
20
+ ]);
21
+ // ── kleine helpers ────────────────────────────────────────────────────────
22
+ function pickString(v) {
23
+ if (v == null)
24
+ return null;
25
+ if (typeof v === "string")
26
+ return v;
27
+ if (typeof v === "number")
28
+ return String(v);
29
+ if (Array.isArray(v)) {
30
+ for (const item of v) {
31
+ const s = pickString(item?.text ?? item?.value ?? item);
32
+ if (s)
33
+ return s;
34
+ }
35
+ return null;
36
+ }
37
+ if (typeof v === "object") {
38
+ return (v.nl ?? v.NL ?? v.en ?? v.EN ?? v.text ?? v.value ??
39
+ Object.values(v).find((x) => typeof x === "string") ??
40
+ null);
41
+ }
42
+ return null;
43
+ }
44
+ function toNumber(value) {
45
+ if (typeof value === "number")
46
+ return Number.isFinite(value) ? value : 0;
47
+ if (typeof value !== "string")
48
+ return 0;
49
+ const cleaned = value.replace(/[^0-9,.-]/g, "").trim();
50
+ if (!cleaned)
51
+ return 0;
52
+ const lastComma = cleaned.lastIndexOf(",");
53
+ const lastDot = cleaned.lastIndexOf(".");
54
+ const lastSeparator = Math.max(lastComma, lastDot);
55
+ const hasRepeated = (cleaned.match(/[,.]/g) ?? []).length > 1;
56
+ const normalized = hasRepeated && lastSeparator >= 0
57
+ ? `${cleaned.slice(0, lastSeparator).replace(/[,.]/g, "")}.${cleaned.slice(lastSeparator + 1)}`
58
+ : lastComma > lastDot
59
+ ? cleaned.replace(/\./g, "").replace(",", ".")
60
+ : cleaned.replace(/,/g, "");
61
+ const n = Number.parseFloat(normalized);
62
+ return Number.isFinite(n) ? n : 0;
63
+ }
64
+ function statusFlags(statusValue) {
65
+ const status = String(statusValue ?? "").toLowerCase();
66
+ return {
67
+ isCancelled: status === "cancelled" || status === "geannuleerd" || status === "rejected",
68
+ isShipped: status === "shipped" || status === "verzonden" || status === "delivered" || status === "geleverd",
69
+ isInvoiced: status === "invoiced" || status === "gefactureerd",
70
+ isConfirmed: status === "confirmed" ||
71
+ status === "bevestigd" ||
72
+ status === "nieuw" ||
73
+ status === "approved" ||
74
+ status === "open" ||
75
+ status === "concept",
76
+ isControlled: false,
77
+ controlledAt: null,
78
+ };
79
+ }
80
+ function pickPackingConfigForLine(configs, packing) {
81
+ if (!Array.isArray(configs) || configs.length === 0)
82
+ return null;
83
+ const ppc = Number(packing?.plants_per_crate ?? 0);
84
+ const cpl = Number(packing?.crates_per_layer ?? 0);
85
+ const lpc = Number(packing?.layers_per_cart ?? 0);
86
+ const match = configs.find((c) => {
87
+ const a = Number(c?.piecesPerPackage ?? c?.unitsPerPackage ?? 0);
88
+ const b = Number(c?.packagesPerLayer ?? 0);
89
+ const d = Number(c?.layersPerLoadCarrier ?? 0);
90
+ return ppc > 0 && a === ppc && (!cpl || b === cpl) && (!lpc || d === lpc);
91
+ });
92
+ return match ?? configs.find((c) => c?.primary === true || c?.isPrimary === true) ?? configs[0] ?? null;
93
+ }
94
+ async function chunked(ids, size, fn) {
95
+ const out = [];
96
+ for (let i = 0; i < ids.length; i += size) {
97
+ out.push(...(await fn(ids.slice(i, i + size))));
98
+ }
99
+ return out;
100
+ }
101
+ const TTL_MS = 5 * 60 * 1000;
102
+ let crateDescCache = null;
103
+ async function loadCrateDescriptions(supabase, warnings) {
104
+ if (crateDescCache && Date.now() - crateDescCache.at < TTL_MS)
105
+ return crateDescCache.value;
106
+ const map = new Map();
107
+ try {
108
+ const { data, error } = await supabase
109
+ .from("floricode_records")
110
+ .select("data")
111
+ .eq("resource", "LoadCarrier")
112
+ .limit(5000);
113
+ if (error)
114
+ throw new Error(error.message);
115
+ for (const row of data ?? []) {
116
+ const d = row.data ?? {};
117
+ if (d?.code == null)
118
+ continue;
119
+ const desc = typeof d.description === "string" ? d.description.trim() : "";
120
+ if (desc)
121
+ map.set(String(d.code), desc);
122
+ }
123
+ crateDescCache = { at: Date.now(), value: map };
124
+ }
125
+ catch (e) {
126
+ warnings.push(`Fustomschrijvingen niet geladen: ${e.message}`);
127
+ if (crateDescCache)
128
+ return crateDescCache.value;
129
+ }
130
+ return map;
131
+ }
132
+ // ── hoofdfunctie ──────────────────────────────────────────────────────────
133
+ export async function buildSalesOrdersList(supabase, params) {
134
+ const warnings = [];
135
+ const pageSize = Math.min(PAGE_SIZE_MAX, Math.max(1, Math.floor(params.pageSize || 20)));
136
+ const page = Math.max(1, Math.floor(params.page || 1));
137
+ // 1) Actieve omgeving + connectie.
138
+ const envRes = await supabase
139
+ .from("floriday_settings")
140
+ .select("active_environment")
141
+ .eq("id", 1)
142
+ .maybeSingle();
143
+ const rawEnv = String(envRes.data?.active_environment ?? "staging");
144
+ const environment = rawEnv === "production" || rawEnv === "prod" || rawEnv === "live" ? "live" : "staging";
145
+ const connRes = await supabase
146
+ .from("floriday_connections")
147
+ .select("id, organization_name, label")
148
+ .eq("is_active", true)
149
+ .eq("environment", environment)
150
+ .limit(1);
151
+ const conn = (connRes.data ?? [])[0];
152
+ const sellerFallback = conn?.organization_name ?? conn?.label ?? null;
153
+ let truncated = false;
154
+ // 2) Floriday-orders binnen het venster (alleen benodigde kolommen).
155
+ const dateColumn = params.dateField === "delivery"
156
+ ? "data->delivery->>latestDeliveryDateTime"
157
+ : "data->>orderDateTime";
158
+ const foRows = [];
159
+ if (conn?.id) {
160
+ const pageStep = 1000;
161
+ for (let from = 0; from < FO_CAP; from += pageStep) {
162
+ let q = supabase
163
+ .from("floriday_salesorders_cache")
164
+ .select("floriday_id, data, controlled_at")
165
+ .eq("connection_id", conn.id)
166
+ .eq("is_deleted", false)
167
+ .order("sequence_number", { ascending: false })
168
+ .range(from, from + pageStep - 1);
169
+ if (params.since)
170
+ q = q.gte(dateColumn, params.since);
171
+ if (params.until)
172
+ q = q.lt(dateColumn, params.until);
173
+ const { data, error } = await q;
174
+ if (error)
175
+ throw new Error(`Floriday-orders lezen faalde: ${error.message}`);
176
+ const batch = data ?? [];
177
+ foRows.push(...batch);
178
+ if (batch.length < pageStep)
179
+ break;
180
+ if (foRows.length >= FO_CAP)
181
+ truncated = true;
182
+ }
183
+ }
184
+ // 3) Lokale orders binnen hetzelfde venster.
185
+ const localOrderSelect = "id, order_number, customer_name, customer_id, order_date, delivery_date, status, total, custom_fields, order_source, created_at, is_reservation, buffer_id, floriday_sales_order_id, order_items(id, product_id, product_name, quantity, unit, unit_price, products(plants_per_crate, crates_per_layer, layers_per_cart, floriday_trade_item_id, custom_fields))";
186
+ let localQ = supabase
187
+ .from("orders")
188
+ .select(localOrderSelect)
189
+ .is("deleted_at", null)
190
+ .order("created_at", { ascending: false })
191
+ .limit(LOCAL_CAP);
192
+ if (params.since && params.until) {
193
+ if (params.dateField === "delivery") {
194
+ localQ = localQ
195
+ .gte("delivery_date", params.since.slice(0, 10))
196
+ .lt("delivery_date", params.until.slice(0, 10));
197
+ }
198
+ else {
199
+ localQ = localQ.gte("order_date", params.since).lt("order_date", params.until);
200
+ }
201
+ }
202
+ const localRes = await localQ;
203
+ if (localRes.error)
204
+ throw new Error(`Lokale orders lezen faalde: ${localRes.error.message}`);
205
+ const localOrderRows = (localRes.data ?? []);
206
+ // 4) Mailbox-voorstellen (best-effort).
207
+ let proposalRows = [];
208
+ try {
209
+ let pq = supabase
210
+ .from("mailbox_order_proposals")
211
+ .select("id, order_number, status, delivery_date, created_order_id, created_at, matched_customer_id, mailbox_messages(from_name, from_email, subject, received_at), mailbox_proposal_lines(raw_product_text, quantity, unit)")
212
+ .neq("status", "rejected")
213
+ .order("created_at", { ascending: false })
214
+ .limit(PROPOSAL_CAP);
215
+ if (params.since && params.until && params.dateField === "ordered") {
216
+ pq = pq.gte("created_at", params.since).lt("created_at", params.until);
217
+ }
218
+ const pr = await pq;
219
+ if (pr.error)
220
+ throw new Error(pr.error.message);
221
+ proposalRows = (pr.data ?? []);
222
+ }
223
+ catch (e) {
224
+ warnings.push(`Mailbox-voorstellen niet geladen: ${e.message}`);
225
+ }
226
+ // 5) Referentiedata — alleen wat nodig is voor deze rijen.
227
+ const neededOrgIds = Array.from(new Set(foRows.map((r) => String(r?.data?.customerOrganizationId ?? "")).filter((s) => s.length > 0)));
228
+ const neededCustomerIds = Array.from(new Set(localOrderRows.map((r) => String(r.customer_id ?? "")).filter((s) => s.length > 0)));
229
+ const neededTradeItemIds = Array.from(new Set([
230
+ ...foRows.map((r) => String(r?.data?.tradeItemId ?? "")),
231
+ ...localOrderRows.flatMap((r) => (r.order_items ?? []).map((it) => String(it?.products?.floriday_trade_item_id ?? ""))),
232
+ ].filter((s) => s.length > 0)));
233
+ const neededCarrierIds = Array.from(new Set(localOrderRows
234
+ .map((r) => String((r.custom_fields ?? {}).carrier ?? ""))
235
+ .filter((s) => s.length > 0)));
236
+ const foIdsForNumbers = Array.from(new Set(foRows.map((r) => String(r.floriday_id ?? "")).filter((s) => s.length > 0)));
237
+ const safe = async (label, fn, fallback) => {
238
+ try {
239
+ return await fn();
240
+ }
241
+ catch (e) {
242
+ warnings.push(`${label}: ${e.message}`);
243
+ return fallback;
244
+ }
245
+ };
246
+ const [orderNumberRows, customerRows, floridayCustomerRows, tradeItemRows, carrierRows, crateDescriptions, groupPacking,] = await Promise.all([
247
+ safe("Ordernummers", () => chunked(foIdsForNumbers, 400, async (chunk) => {
248
+ const { data, error } = await supabase
249
+ .from("floriday_order_number_map")
250
+ .select("floriday_id, order_number")
251
+ .in("floriday_id", chunk);
252
+ if (error)
253
+ throw new Error(error.message);
254
+ return (data ?? []);
255
+ }), []),
256
+ safe("Klanten", async () => {
257
+ const orFilter = [
258
+ neededOrgIds.length ? `floriday_organization_id.in.(${neededOrgIds.join(",")})` : null,
259
+ neededCustomerIds.length ? `id.in.(${neededCustomerIds.join(",")})` : null,
260
+ ]
261
+ .filter(Boolean)
262
+ .join(",") || "id.is.null";
263
+ const { data, error } = await supabase
264
+ .from("customers")
265
+ .select("id, company_name, commercial_name, city, postal_code, logo_url, floriday_organization_id")
266
+ .or(orFilter)
267
+ .limit(5000);
268
+ if (error)
269
+ throw new Error(error.message);
270
+ return (data ?? []);
271
+ }, []),
272
+ safe("Floriday-klantcache", () => chunked(neededOrgIds, 300, async (chunk) => {
273
+ const { data, error } = await supabase
274
+ .from("floriday_customers_cache")
275
+ .select("organization_id, data")
276
+ .in("organization_id", chunk);
277
+ if (error)
278
+ throw new Error(error.message);
279
+ return (data ?? []);
280
+ }), []),
281
+ safe("Artikelcache", () => chunked(neededTradeItemIds, 300, async (chunk) => {
282
+ const { data, error } = await supabase
283
+ .from("floriday_trade_items_cache")
284
+ .select("floriday_id, data")
285
+ .in("floriday_id", chunk);
286
+ if (error)
287
+ throw new Error(error.message);
288
+ return (data ?? []);
289
+ }), []),
290
+ safe("Vervoerders", async () => {
291
+ if (!neededCarrierIds.length)
292
+ return [];
293
+ const { data, error } = await supabase
294
+ .from("carriers")
295
+ .select("id, delivery_days, signup_time_1, signup_time_2, signup_days_before_delivery, pickup_weekdays, delivery_weekdays, pickup_moments")
296
+ .in("id", neededCarrierIds);
297
+ if (error)
298
+ throw new Error(error.message);
299
+ return (data ?? []);
300
+ }, []),
301
+ loadCrateDescriptions(supabase, warnings),
302
+ safe("Productgroepen", () => loadGroupPacking(supabase), new Map()),
303
+ ]);
304
+ const orderNumberByFoId = new Map();
305
+ for (const m of orderNumberRows) {
306
+ orderNumberByFoId.set(String(m.floriday_id), String(m.order_number ?? ""));
307
+ }
308
+ const carrierById = new Map();
309
+ for (const c of carrierRows)
310
+ carrierById.set(String(c.id), c);
311
+ // 6) Klant-lookups.
312
+ const lookup = new Map();
313
+ const localCustomerLookup = new Map();
314
+ for (const c of customerRows) {
315
+ localCustomerLookup.set(String(c.id), {
316
+ name: c.company_name ?? "Onbekende klant",
317
+ city: c.city ?? "",
318
+ });
319
+ const orgId = c.floriday_organization_id;
320
+ if (!orgId)
321
+ continue;
322
+ lookup.set(String(orgId), {
323
+ name: c.commercial_name ?? c.company_name ?? "Onbekende klant",
324
+ postalCode: c.postal_code ?? "",
325
+ city: c.city ?? "",
326
+ logoUrl: c.logo_url ?? null,
327
+ raw: null,
328
+ });
329
+ }
330
+ for (const row of floridayCustomerRows) {
331
+ const d = row.data ?? {};
332
+ const orgId = row.organization_id ?? d.organizationId;
333
+ if (!orgId || lookup.has(String(orgId)))
334
+ continue;
335
+ const addr = d.physicalAddress ?? d.mailingAddress ?? {};
336
+ lookup.set(String(orgId), {
337
+ name: d.commercialName ?? d.name ?? "Onbekende klant",
338
+ postalCode: addr.postalCode ?? "",
339
+ city: addr.city ?? "",
340
+ logoUrl: d.logoUrl ?? null,
341
+ raw: null,
342
+ });
343
+ }
344
+ // 7) Artikel-lookup.
345
+ const tradeItemLookup = new Map();
346
+ for (const row of tradeItemRows) {
347
+ const d = row.data ?? {};
348
+ const id = String(row.floriday_id ?? d.tradeItemId ?? "");
349
+ if (!id)
350
+ continue;
351
+ const photos = d.photos ?? d.images ?? d.tradeItemPhotos ?? [];
352
+ const firstPhotoRaw = Array.isArray(photos) && photos.length > 0
353
+ ? (photos[0]?.url ?? photos[0]?.imageUrl ?? photos[0]?.href ?? photos[0])
354
+ : null;
355
+ tradeItemLookup.set(id, {
356
+ name: pickString(d.name) ??
357
+ pickString(d.productName) ??
358
+ pickString(d.tradeItemName) ??
359
+ pickString(d.tradeItemTexts) ??
360
+ "Onbekend artikel",
361
+ imageUrl: pickString(firstPhotoRaw),
362
+ sku: pickString(d.vbnProductCode) ?? pickString(d.gtin) ?? pickString(d.articleCode),
363
+ raw: d,
364
+ });
365
+ }
366
+ // 8) Buffers.
367
+ const bufferByFoId = new Map();
368
+ const bufferByLocalId = new Map();
369
+ for (const r of localOrderRows) {
370
+ const bid = r.buffer_id;
371
+ if (!bid)
372
+ continue;
373
+ if (r.floriday_sales_order_id)
374
+ bufferByFoId.set(String(r.floriday_sales_order_id), bid);
375
+ else
376
+ bufferByLocalId.set(String(r.id), bid);
377
+ }
378
+ // 9) Floriday-orders → DTO (zonder zware raw-payload richting browser).
379
+ const floridayOrders = foRows.map((r) => {
380
+ const enriched = {
381
+ ...r.data,
382
+ ...(orderNumberByFoId.get(String(r.floriday_id))
383
+ ? { orderNumber: orderNumberByFoId.get(String(r.floriday_id)) }
384
+ : {}),
385
+ __controlledAt: r.controlled_at ?? null,
386
+ };
387
+ const dto = mapJsonbToSalesOrderDTO(enriched, lookup, tradeItemLookup, crateDescriptions);
388
+ dto.raw = { order: { status: r.data?.status ?? null }, customer: null, tradeItem: null };
389
+ const bid = bufferByFoId.get(dto.id) ?? null;
390
+ if (bid)
391
+ dto.bufferId = bid;
392
+ return dto;
393
+ });
394
+ // 10) Lokale orders → DTO.
395
+ const resolveLinePacking = (item) => {
396
+ const name = String(item?.product_name ?? "");
397
+ if (name.startsWith("[Groep] ")) {
398
+ const gname = name.slice("[Groep] ".length).trim().toLowerCase();
399
+ const gp = groupPacking.get(gname);
400
+ if (gp)
401
+ return gp;
402
+ }
403
+ return item?.products ?? null;
404
+ };
405
+ const localOrders = [];
406
+ const includedOrderIds = new Set();
407
+ for (const r of localOrderRows) {
408
+ if (r.floriday_sales_order_id)
409
+ continue;
410
+ const cf = r.custom_fields ?? {};
411
+ const sourceRaw = String(cf?.source ?? r.order_source ?? "manual").toLowerCase();
412
+ const source = mailboxSources.has(sourceRaw) ? "mailbox" : "local";
413
+ includedOrderIds.add(String(r.id));
414
+ const lineItems = Array.isArray(r.order_items) ? r.order_items : [];
415
+ const firstLine = lineItems[0] ?? null;
416
+ const lineTotal = lineItems.reduce((s, it) => s + (toNumber(it?.quantity) || 0) * (toNumber(it?.unit_price) || 0), 0);
417
+ const total = lineTotal > 0 ? lineTotal : toNumber(r.total);
418
+ const totalQuantity = lineItems.reduce((s, it) => s + (toNumber(it?.quantity) || 0), 0);
419
+ let fustTotal = 0;
420
+ let karTotalRaw = 0;
421
+ const crateNames = [];
422
+ const cartNames = [];
423
+ for (const item of lineItems) {
424
+ const pieces = toNumber(item?.quantity) || 0;
425
+ if (pieces <= 0)
426
+ continue;
427
+ const packing = resolveLinePacking(item);
428
+ const plantsPerCrate = Math.max(1, Number(packing?.plants_per_crate ?? 1));
429
+ const cratesPerLayer = Math.max(0, Number(packing?.crates_per_layer ?? 0));
430
+ const layersPerCart = Math.max(0, Number(packing?.layers_per_cart ?? 0));
431
+ const fust = Math.ceil(pieces / plantsPerCrate);
432
+ fustTotal += fust;
433
+ const packagesPerCart = cratesPerLayer * layersPerCart;
434
+ if (packagesPerCart > 0)
435
+ karTotalRaw += fust / packagesPerCart;
436
+ const prod = item?.products ?? null;
437
+ const fid = prod?.floriday_trade_item_id ? String(prod.floriday_trade_item_id) : null;
438
+ const configs = (() => {
439
+ const fromCache = fid ? tradeItemLookup.get(fid)?.raw?.packingConfigurations : null;
440
+ if (Array.isArray(fromCache) && fromCache.length > 0)
441
+ return fromCache;
442
+ const payload = prod?.custom_fields?.floriday_payload?.packingConfigurations;
443
+ return Array.isArray(payload) ? payload : [];
444
+ })();
445
+ const cfg = pickPackingConfigForLine(configs, packing);
446
+ crateNames.push(cratePackageName(cfg?.package?.vbnPackageCode ?? cfg?.package?.customPackageId ?? null, crateDescriptions));
447
+ cartNames.push(loadCarrierName(cfg?.loadCarrier ?? null));
448
+ }
449
+ const karCount = karTotalRaw > 0 ? Math.max(1, Math.ceil(karTotalRaw)) : totalQuantity > 0 ? 1 : 0;
450
+ const fustCount = fustTotal > 0 ? fustTotal : totalQuantity;
451
+ const unitPrice = firstLine ? toNumber(firstLine.unit_price) : total;
452
+ const rawOrderDate = r.order_date ?? "";
453
+ const orderedAt = typeof rawOrderDate === "string" && rawOrderDate.includes("T")
454
+ ? rawOrderDate
455
+ : (r.created_at ?? rawOrderDate ?? "");
456
+ const rawDeliveryDate = String(r.delivery_date ?? "");
457
+ let deliveryTime = normalizeTime(cf.delivery_time);
458
+ if (!deliveryTime && rawDeliveryDate && cf.carrier) {
459
+ const createdAt = r.created_at ? new Date(r.created_at) : new Date();
460
+ deliveryTime = resolveDeliveryTimeForDate(carrierById.get(String(cf.carrier)) ?? null, rawDeliveryDate.slice(0, 10), createdAt);
461
+ }
462
+ const deliveryAt = rawDeliveryDate && !rawDeliveryDate.includes("T") && deliveryTime
463
+ ? `${rawDeliveryDate.slice(0, 10)}T${deliveryTime}:00`
464
+ : rawDeliveryDate;
465
+ const customerId = r.customer_id ? String(r.customer_id) : "";
466
+ const localCustomer = customerId ? localCustomerLookup.get(customerId) : undefined;
467
+ const firstProduct = pickString(firstLine?.product_name);
468
+ const firstPacking = firstLine ? resolveLinePacking(firstLine) : null;
469
+ const firstPlantsPerCrate = Math.max(1, Number(firstPacking?.plants_per_crate ?? 1));
470
+ localOrders.push({
471
+ id: `local:${r.id}`,
472
+ orderNumber: centralOrderNumber(r.order_number),
473
+ productName: firstProduct
474
+ ? lineItems.length > 1
475
+ ? `${firstProduct} +${lineItems.length - 1}`
476
+ : firstProduct
477
+ : source === "mailbox"
478
+ ? "Mailbox-bestelling"
479
+ : "Lokale bestelling",
480
+ sku: source === "mailbox" ? "Mailbox-bestelling" : "Lokale bestelling",
481
+ imageUrl: null,
482
+ orderedAt,
483
+ deliveryAt,
484
+ quantity: {
485
+ multiplier: totalQuantity > 0 ? Math.ceil(totalQuantity / firstPlantsPerCrate) : 1,
486
+ count: firstPlantsPerCrate,
487
+ unitPrice,
488
+ },
489
+ revenue: total,
490
+ fustCount,
491
+ karCount,
492
+ crateTypeName: mergeTypeNames(crateNames),
493
+ cartTypeName: mergeTypeNames(cartNames),
494
+ loadCarrierType: normalizeLoadCarrierCode(cf.load_carrier_type),
495
+ totalPieces: totalQuantity,
496
+ isLocal: true,
497
+ isReservation: !!r.is_reservation,
498
+ customer: {
499
+ name: r.customer_name ?? localCustomer?.name ?? "Onbekende klant",
500
+ postalCode: "",
501
+ city: localCustomer?.city ?? "",
502
+ logoUrl: null,
503
+ },
504
+ status: statusFlags(r.status),
505
+ raw: { order: { status: r.status ?? null }, customer: null, tradeItem: null },
506
+ bufferId: bufferByLocalId.get(String(r.id)) ?? null,
507
+ });
508
+ }
509
+ // 11) Mailbox-voorstellen zonder aangemaakte order.
510
+ for (const p of proposalRows) {
511
+ if (p.created_order_id && includedOrderIds.has(String(p.created_order_id)))
512
+ continue;
513
+ const msgRaw = p.mailbox_messages;
514
+ const msg = Array.isArray(msgRaw) ? msgRaw[0] : msgRaw;
515
+ const lines = Array.isArray(p.mailbox_proposal_lines) ? p.mailbox_proposal_lines : [];
516
+ const firstLine = lines[0] ?? null;
517
+ const matchedCustomerId = p.matched_customer_id ? String(p.matched_customer_id) : "";
518
+ const matchedCustomer = matchedCustomerId ? localCustomerLookup.get(matchedCustomerId) : undefined;
519
+ const quantity = Number(firstLine?.quantity ?? 1) || 1;
520
+ localOrders.push({
521
+ id: `mailbox-proposal:${p.id}`,
522
+ orderNumber: centralOrderNumber(p.order_number),
523
+ productName: pickString(firstLine?.raw_product_text) ?? pickString(msg?.subject) ?? "Mailbox-bestelling",
524
+ sku: "Mailbox-bestelling",
525
+ imageUrl: null,
526
+ orderedAt: msg?.received_at ?? p.created_at ?? "",
527
+ deliveryAt: p.delivery_date ?? "",
528
+ quantity: { multiplier: quantity, count: 1, unitPrice: 0 },
529
+ revenue: 0,
530
+ fustCount: 0,
531
+ karCount: quantity > 0 ? 1 : 0,
532
+ totalPieces: quantity,
533
+ isLocal: true,
534
+ customer: {
535
+ name: matchedCustomer?.name ?? msg?.from_name ?? msg?.from_email ?? "Onbekende klant",
536
+ postalCode: "",
537
+ city: matchedCustomer?.city ?? "",
538
+ logoUrl: null,
539
+ },
540
+ status: statusFlags(p.status),
541
+ raw: { order: { status: p.status ?? null }, customer: null, tradeItem: null },
542
+ });
543
+ }
544
+ // 12) Samenvoegen, buffergroottes, sorteren.
545
+ const all = [...localOrders, ...floridayOrders].sort((a, b) => (b.orderedAt ?? "").localeCompare(a.orderedAt ?? ""));
546
+ const sizeByBuffer = new Map();
547
+ for (const o of all) {
548
+ if (o.bufferId)
549
+ sizeByBuffer.set(o.bufferId, (sizeByBuffer.get(o.bufferId) ?? 0) + 1);
550
+ }
551
+ for (const o of all)
552
+ if (o.bufferId)
553
+ o.bufferSize = sizeByBuffer.get(o.bufferId) ?? 1;
554
+ // 13) Filteren (zoek + status), tellen, pagineren.
555
+ const search = (params.search ?? "").trim().toLowerCase();
556
+ const matchesSearch = (o) => !search ||
557
+ `${o.productName} ${o.sku} ${o.orderNumber} ${o.customer.name}`.toLowerCase().includes(search);
558
+ const matchesStatusFilter = (o) => {
559
+ switch (params.status) {
560
+ case "cancelled":
561
+ return o.status.isCancelled;
562
+ case "shipped":
563
+ return o.status.isShipped;
564
+ case "invoiced":
565
+ return o.status.isInvoiced;
566
+ case "open":
567
+ return !o.status.isCancelled && !o.status.isShipped;
568
+ default:
569
+ return true;
570
+ }
571
+ };
572
+ const matchesTab = (o) => {
573
+ switch (params.tab) {
574
+ case "open":
575
+ return !o.status.isCancelled && !o.status.isShipped;
576
+ case "shipped":
577
+ return o.status.isShipped && !o.status.isCancelled;
578
+ case "invoiced":
579
+ return o.status.isInvoiced;
580
+ case "cancelled":
581
+ return o.status.isCancelled;
582
+ default:
583
+ return true;
584
+ }
585
+ };
586
+ const withoutTab = all.filter((o) => matchesSearch(o) && matchesStatusFilter(o));
587
+ const filtered = withoutTab.filter(matchesTab);
588
+ const collapse = (rows) => {
589
+ const seen = new Set();
590
+ const out = [];
591
+ for (const o of rows) {
592
+ const key = o.bufferId && (o.bufferSize ?? 1) > 1 ? `buf:${o.bufferId}` : `one:${o.id}`;
593
+ if (seen.has(key))
594
+ continue;
595
+ seen.add(key);
596
+ out.push(o);
597
+ }
598
+ return out;
599
+ };
600
+ const collapsedAll = collapse(withoutTab);
601
+ const statusCounts = {
602
+ all: collapsedAll.length,
603
+ open: 0,
604
+ shipped: 0,
605
+ invoiced: 0,
606
+ cancelled: 0,
607
+ };
608
+ for (const o of collapsedAll) {
609
+ if (o.status.isCancelled)
610
+ statusCounts.cancelled++;
611
+ else if (o.status.isShipped)
612
+ statusCounts.shipped++;
613
+ else
614
+ statusCounts.open++;
615
+ if (o.status.isInvoiced)
616
+ statusCounts.invoiced++;
617
+ }
618
+ const totals = computeTotals(collapse(filtered));
619
+ const totalCount = filtered.length;
620
+ const rows = params.all
621
+ ? filtered.slice(0, EXPORT_CAP)
622
+ : filtered.slice((page - 1) * pageSize, page * pageSize);
623
+ // 14) Karindeling alleen voor de zichtbare rijen (begrensd werk).
624
+ if (!params.all) {
625
+ const localById = new Map(localOrderRows.map((r) => [String(r.id), r]));
626
+ await Promise.all(rows
627
+ .filter((o) => o.id.startsWith("local:"))
628
+ .map(async (o) => {
629
+ const r = localById.get(o.id.slice("local:".length));
630
+ const lineItems = Array.isArray(r?.order_items) ? r.order_items : [];
631
+ if (!lineItems.length)
632
+ return;
633
+ const raws = lineItems.map((it, idx) => {
634
+ const packing = resolveLinePacking(it);
635
+ return {
636
+ id: String(it?.id ?? `${r.id}#${idx}`),
637
+ product_id: it?.product_id ?? null,
638
+ product_name: String(it?.product_name ?? ""),
639
+ quantity: Number(it?.quantity ?? 0),
640
+ plants_per_crate: Math.max(1, Number(packing?.plants_per_crate ?? 1)),
641
+ crates_per_layer: Math.max(1, Number(packing?.crates_per_layer ?? 1)),
642
+ layers_per_cart: Math.max(1, Number(packing?.layers_per_cart ?? 1)),
643
+ };
644
+ });
645
+ try {
646
+ const expanded = await expandOrderItemsForPlanner(supabase, raws);
647
+ if (!expanded.length)
648
+ return;
649
+ const planned = planCarts(expanded);
650
+ if (planned.carts.length > 0)
651
+ o.karCount = planned.carts.length;
652
+ }
653
+ catch {
654
+ /* fallback: reeds berekende karCount */
655
+ }
656
+ }));
657
+ }
658
+ return {
659
+ rows,
660
+ totalCount,
661
+ totals,
662
+ statusCounts,
663
+ sellerFallback,
664
+ environment,
665
+ warnings,
666
+ truncated,
667
+ };
668
+ }
669
+ // Belading per productgroep (op naam), gebruikt voor "[Groep] X"-regels.
670
+ async function loadGroupPacking(supabase) {
671
+ const [groupsRes, mirrorRes] = await Promise.all([
672
+ supabase.from("product_groups").select("id, name, head_product_id").limit(5000),
673
+ supabase
674
+ .from("products")
675
+ .select("id, plants_per_crate, crates_per_layer, layers_per_cart, trade_item_key")
676
+ .like("trade_item_key", "group:%")
677
+ .limit(5000),
678
+ ]);
679
+ if (groupsRes.error)
680
+ throw new Error(groupsRes.error.message);
681
+ const mirrorByGroupId = new Map();
682
+ for (const p of (mirrorRes.data ?? [])) {
683
+ const key = p.trade_item_key;
684
+ if (typeof key === "string" && key.startsWith("group:"))
685
+ mirrorByGroupId.set(key.slice(6), p);
686
+ }
687
+ const groups = (groupsRes.data ?? []);
688
+ const headIds = Array.from(new Set(groups.map((g) => g.head_product_id).filter((v) => typeof v === "string" && !!v)));
689
+ const headById = new Map();
690
+ if (headIds.length) {
691
+ const { data } = await supabase
692
+ .from("products")
693
+ .select("id, plants_per_crate, crates_per_layer, layers_per_cart")
694
+ .in("id", headIds);
695
+ for (const p of (data ?? []))
696
+ headById.set(p.id, p);
697
+ }
698
+ const packFromRow = (row) => row
699
+ ? {
700
+ plants_per_crate: Math.max(1, Number(row.plants_per_crate ?? 1)),
701
+ crates_per_layer: Math.max(1, Number(row.crates_per_layer ?? 1)),
702
+ layers_per_cart: Math.max(1, Number(row.layers_per_cart ?? 1)),
703
+ }
704
+ : null;
705
+ const byName = new Map();
706
+ for (const g of groups) {
707
+ const packing = packFromRow(headById.get(g.head_product_id)) ?? packFromRow(mirrorByGroupId.get(g.id));
708
+ const gname = g.name ? String(g.name).trim().toLowerCase() : "";
709
+ if (packing && gname)
710
+ byName.set(gname, packing);
711
+ }
712
+ return byName;
713
+ }