@flowselections/floriday-verkoop-module 1.0.38 → 1.0.39

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
@@ -1,19 +1,18 @@
1
- import { useEffect, useMemo } from "react";
2
- import { useQuery, useQueryClient } from "@tanstack/react-query";
1
+ // Lichte client-hook: alle aggregatie gebeurt serverside in
2
+ // `listSalesOrders`. De browser ontvangt alleen de rijen van de zichtbare
3
+ // pagina plus totalen/tellingen, zodat de lijst snel blijft bij tienduizenden
4
+ // orders.
5
+ import { useEffect } from "react";
6
+ import { useQuery, useQueryClient, keepPreviousData } from "@tanstack/react-query";
3
7
  import { useServerFn } from "@tanstack/react-start";
4
8
  import { supabase } from "../integrations/supabase/client";
5
- import { getActiveFloridayEnvironment, getFlorydayCachedList } from "./floriday-public.functions";
6
- import { expandOrderItemsForPlanner } from "./expand-group-order-items";
7
- import { planCarts } from "./cart-planner";
8
- import { normalizeTime, resolveDeliveryTimeForDate } from "./carrier-availability";
9
- import { cratePackageName, loadCarrierName, mergeTypeNames } from "./load-carrier-labels";
10
- import { normalizeLoadCarrierCode } from "./load-carriers";
11
- import { centralOrderNumber, isoWeekRange, mapJsonbToSalesOrderDTO, monthRange, } from "./salesorders-mapping";
12
- // Bepaal het serverside periode-venster op basis van de UI-filters.
13
- // Retourneert `since`/`until` als ISO-strings + het gekozen dateField.
14
- // Bij ontbrekende/onvolledige filters geen venster → server past geen
15
- // periode-filter toe (client-side applyFilters filtert dan alsnog).
16
- function derivePeriodWindow(filters) {
9
+ import { listSalesOrders } from "./salesorders-list.functions";
10
+ import { isoWeekRange, monthRange, } from "./salesorders-mapping";
11
+ /**
12
+ * Bepaal het serverside periode-venster op basis van de UI-filters.
13
+ * Zonder (volledige) periodefilters wordt geen venster toegepast.
14
+ */
15
+ export function derivePeriodWindow(filters) {
17
16
  const dateField = filters?.dateField ?? "ordered";
18
17
  if (!filters)
19
18
  return { dateField };
@@ -54,605 +53,61 @@ function derivePeriodWindow(filters) {
54
53
  start = r.start;
55
54
  end = r.end;
56
55
  }
57
- // Buffer van 1 dag rond het venster: order/delivery-tijden kunnen net buiten
58
- // de weekgrens vallen; de client filtert alsnog exact via applyFilters.
59
56
  if (start && end) {
60
- const s = new Date(start);
61
- s.setUTCDate(s.getUTCDate() - 1);
62
- const e = new Date(end);
63
- e.setUTCDate(e.getUTCDate() + 1);
64
- return { since: s.toISOString(), until: e.toISOString(), dateField };
57
+ return { since: start.toISOString(), until: end.toISOString(), dateField };
65
58
  }
66
59
  return { dateField };
67
60
  }
68
- function pickString(v) {
69
- if (v == null)
70
- return null;
71
- if (typeof v === "string")
72
- return v;
73
- if (typeof v === "number")
74
- return String(v);
75
- if (Array.isArray(v)) {
76
- for (const item of v) {
77
- const s = pickString(item?.text ?? item?.value ?? item);
78
- if (s)
79
- return s;
80
- }
81
- return null;
82
- }
83
- if (typeof v === "object") {
84
- return (v.nl ?? v.NL ?? v.en ?? v.EN ?? v.text ?? v.value ??
85
- Object.values(v).find((x) => typeof x === "string") ??
86
- null);
87
- }
88
- return null;
89
- }
90
- const mailboxSources = new Set([
91
- "mailbox",
92
- "mailbox_order",
93
- "mailbox-bestellingen",
94
- "mailbox_bestellingen",
95
- "mail",
96
- "email",
97
- "email_order",
98
- ]);
99
- function toNumber(value) {
100
- if (typeof value === "number")
101
- return Number.isFinite(value) ? value : 0;
102
- if (typeof value !== "string")
103
- return 0;
104
- const cleaned = value.replace(/[^0-9,.-]/g, "").trim();
105
- if (!cleaned)
106
- return 0;
107
- const lastComma = cleaned.lastIndexOf(",");
108
- const lastDot = cleaned.lastIndexOf(".");
109
- const lastSeparator = Math.max(lastComma, lastDot);
110
- const hasRepeatedSeparators = (cleaned.match(/[,.]/g) ?? []).length > 1;
111
- const normalized = hasRepeatedSeparators && lastSeparator >= 0
112
- ? `${cleaned.slice(0, lastSeparator).replace(/[,.]/g, "")}.${cleaned.slice(lastSeparator + 1)}`
113
- : lastComma > lastDot
114
- ? cleaned.replace(/\./g, "").replace(",", ".")
115
- : cleaned.replace(/,/g, "");
116
- const n = Number.parseFloat(normalized);
117
- return Number.isFinite(n) ? n : 0;
118
- }
119
- function statusFlags(statusValue) {
120
- const status = String(statusValue ?? "").toLowerCase();
121
- return {
122
- isCancelled: status === "cancelled" || status === "geannuleerd" || status === "rejected",
123
- isShipped: status === "shipped" || status === "verzonden" || status === "delivered" || status === "geleverd",
124
- isInvoiced: status === "invoiced" || status === "gefactureerd",
125
- isConfirmed: status === "confirmed" ||
126
- status === "bevestigd" ||
127
- status === "nieuw" ||
128
- status === "approved" ||
129
- status === "open" ||
130
- status === "concept",
131
- isControlled: false,
132
- controlledAt: null,
61
+ export function useSalesOrdersQuery(filters, options) {
62
+ const fetchList = useServerFn(listSalesOrders);
63
+ const queryClient = useQueryClient();
64
+ const window = derivePeriodWindow(filters);
65
+ const tab = options?.tab ?? "all";
66
+ const page = options?.page ?? 1;
67
+ // Zonder expliciete paginering (bv. detailpagina) halen we een ruime,
68
+ // begrensde set op.
69
+ const pageSize = options?.pageSize ?? 200;
70
+ const params = {
71
+ since: window.since,
72
+ until: window.until,
73
+ dateField: window.dateField,
74
+ search: filters?.search,
75
+ status: filters?.status,
76
+ tab,
77
+ page,
78
+ pageSize,
133
79
  };
134
- }
135
- async function fetchAllLocalOrders() {
136
- const pageSize = 1000;
137
- const rows = [];
138
- for (let from = 0; from < 50000; from += pageSize) {
139
- const to = from + pageSize - 1;
140
- const { data, error } = await supabase
141
- .from("orders")
142
- .select("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))")
143
- .is("deleted_at", null)
144
- .order("created_at", { ascending: false })
145
- .range(from, to);
146
- if (error)
147
- throw error;
148
- const batch = data ?? [];
149
- rows.push(...batch);
150
- if (batch.length < pageSize)
151
- break;
152
- }
153
- return rows;
154
- }
155
- // Kies de packing-configuratie van een artikel die het best past bij de
156
- // belading van de orderregel; anders de primaire configuratie.
157
- function pickPackingConfigForLine(configs, packing) {
158
- if (!Array.isArray(configs) || configs.length === 0)
159
- return null;
160
- const ppc = Number(packing?.plants_per_crate ?? 0);
161
- const cpl = Number(packing?.crates_per_layer ?? 0);
162
- const lpc = Number(packing?.layers_per_cart ?? 0);
163
- const match = configs.find((c) => {
164
- const a = Number(c?.piecesPerPackage ?? c?.unitsPerPackage ?? 0);
165
- const b = Number(c?.packagesPerLayer ?? 0);
166
- const d = Number(c?.layersPerLoadCarrier ?? 0);
167
- return ppc > 0 && a === ppc && (!cpl || b === cpl) && (!lpc || d === lpc);
80
+ const query = useQuery({
81
+ queryKey: ["floriday", "salesorders-list", params],
82
+ queryFn: async () => {
83
+ const res = await fetchList({ data: params });
84
+ const { rows, ...rest } = res;
85
+ return { orders: rows, ...rest };
86
+ },
87
+ placeholderData: keepPreviousData,
88
+ staleTime: 30000,
89
+ refetchOnWindowFocus: true,
168
90
  });
169
- return (match ??
170
- configs.find((c) => c?.primary === true || c?.isPrimary === true) ??
171
- configs[0] ??
172
- null);
173
- }
174
- export function useSalesOrdersQuery(filters) {
175
- const fetchCachedList = useServerFn(getFlorydayCachedList);
176
- const fetchActiveEnv = useServerFn(getActiveFloridayEnvironment);
177
- const queryClient = useQueryClient();
178
- const periodWindow = useMemo(() => derivePeriodWindow(filters), [filters]);
179
- const queryKey = useMemo(() => [
180
- "floriday",
181
- "salesorders-cache",
182
- periodWindow.dateField,
183
- periodWindow.since ?? null,
184
- periodWindow.until ?? null,
185
- ], [periodWindow.dateField, periodWindow.since, periodWindow.until]);
91
+ // Realtime: lijst verversen zodra de cache of lokale orders wijzigen.
186
92
  useEffect(() => {
187
93
  let timer = null;
188
94
  const invalidate = () => {
189
95
  if (timer)
190
96
  clearTimeout(timer);
191
97
  timer = setTimeout(() => {
192
- queryClient.invalidateQueries({ queryKey: ["floriday", "salesorders-cache"] });
193
- }, 250);
98
+ queryClient.invalidateQueries({ queryKey: ["floriday", "salesorders-list"] });
99
+ }, 750);
194
100
  };
195
101
  const channel = supabase
196
- .channel("salesorders-live-packing")
197
- .on("postgres_changes", { event: "*", schema: "public", table: "products" }, invalidate)
198
- .on("postgres_changes", { event: "*", schema: "public", table: "order_items" }, invalidate)
199
- .on("postgres_changes", { event: "*", schema: "public", table: "floriday_trade_items_cache" }, invalidate)
102
+ .channel("salesorders-list-refresh")
200
103
  .on("postgres_changes", { event: "*", schema: "public", table: "floriday_salesorders_cache" }, invalidate)
104
+ .on("postgres_changes", { event: "*", schema: "public", table: "orders" }, invalidate)
201
105
  .subscribe();
202
106
  return () => {
203
107
  if (timer)
204
108
  clearTimeout(timer);
205
- void supabase.removeChannel(channel);
109
+ supabase.removeChannel(channel);
206
110
  };
207
111
  }, [queryClient]);
208
- return useQuery({
209
- queryKey,
210
- queryFn: async () => {
211
- // Actieve omgeving (staging/live) volgt de Authenticatie-module.
212
- const { environment } = await fetchActiveEnv();
213
- const dbEnv = environment === "live" ? "live" : "staging";
214
- // Floriday-data via de centrale gateway (geen directe floriday_*_cache reads).
215
- const [ordersList, tradeItemsList, connectionsRes, localOrderRows, localCustomersRes, mailboxProposalsRes,] = await Promise.all([
216
- fetchCachedList({
217
- data: {
218
- environment,
219
- entity: "sales-orders",
220
- limit: 20000,
221
- since: periodWindow.since,
222
- until: periodWindow.until,
223
- dateField: periodWindow.dateField,
224
- },
225
- }),
226
- fetchCachedList({ data: { environment, entity: "trade-items", limit: 5000 } }),
227
- supabase
228
- .from("floriday_connections")
229
- .select("organization_name, label")
230
- .eq("is_active", true)
231
- .eq("environment", dbEnv)
232
- .limit(1),
233
- fetchAllLocalOrders(),
234
- supabase
235
- .from("customers")
236
- .select("id, company_name, commercial_name, city, postal_code, logo_url, floriday_organization_id")
237
- .limit(5000),
238
- supabase
239
- .from("mailbox_order_proposals")
240
- .select("id, order_number, status, delivery_date, created_order_id, created_at, matched_customer_id, parsed_payload, mailbox_messages(from_name, from_email, subject, received_at), mailbox_proposal_lines(raw_product_text, quantity, unit)")
241
- .neq("status", "rejected")
242
- .order("created_at", { ascending: false })
243
- .limit(5000),
244
- ]);
245
- if (localCustomersRes.error)
246
- throw localCustomersRes.error;
247
- if (mailboxProposalsRes.error)
248
- throw mailboxProposalsRes.error;
249
- // Targeted customer-cache fetch: alleen orgIds die daadwerkelijk in de
250
- // orders voorkomen. Voorkomt dat we vastlopen op limit 2000 terwijl er
251
- // tienduizenden klanten in floriday_customers_cache staan.
252
- const neededOrgIds = Array.from(new Set((ordersList.rows ?? [])
253
- .map((r) => String(r?.data?.customerOrganizationId ?? ""))
254
- .filter((id) => id.length > 0)));
255
- const customerCacheRows = [];
256
- const orgChunkSize = 500;
257
- for (let i = 0; i < neededOrgIds.length; i += orgChunkSize) {
258
- const chunk = neededOrgIds.slice(i, i + orgChunkSize);
259
- const { data, error } = await supabase
260
- .from("floriday_customers_cache")
261
- .select("organization_id, data")
262
- .in("organization_id", chunk);
263
- if (error) {
264
- console.warn("[useSalesOrdersQuery] floriday_customers_cache chunk faalde:", error.message);
265
- continue;
266
- }
267
- customerCacheRows.push(...(data ?? []));
268
- }
269
- const customersList = { rows: customerCacheRows };
270
- // Vervoerders (aanmeldmomenten) — nodig om de aanlevertijd te tonen bij
271
- // orders waar die nog niet is vastgelegd in custom_fields.delivery_time.
272
- const carrierById = new Map();
273
- {
274
- const { data: carrierRows } = await supabase
275
- .from("carriers")
276
- .select("id, delivery_days, signup_time_1, signup_time_2, signup_days_before_delivery, pickup_weekdays, delivery_weekdays, pickup_moments")
277
- .limit(500);
278
- for (const c of carrierRows ?? [])
279
- carrierById.set(String(c.id), c);
280
- }
281
- // Fust-omschrijvingen (Floricode/VBN) om het type ladingdrager te tonen.
282
- const cratePackageDescriptions = new Map();
283
- {
284
- const { data: floricodeRows } = await supabase
285
- .from("floricode_records")
286
- .select("data")
287
- .eq("resource", "LoadCarrier")
288
- .limit(5000);
289
- for (const row of floricodeRows ?? []) {
290
- const d = row.data ?? {};
291
- if (d?.code == null)
292
- continue;
293
- const desc = typeof d.description === "string" ? d.description.trim() : "";
294
- if (desc)
295
- cratePackageDescriptions.set(String(d.code), desc);
296
- }
297
- }
298
- const lookup = new Map();
299
- // Primair: gekoppelde klanten uit de Klanten Module (customers-tabel).
300
- for (const c of localCustomersRes.data ?? []) {
301
- const orgId = c.floriday_organization_id;
302
- if (!orgId)
303
- continue;
304
- lookup.set(String(orgId), {
305
- name: c.commercial_name ?? c.company_name ?? "Onbekende klant",
306
- postalCode: c.postal_code ?? "",
307
- city: c.city ?? "",
308
- logoUrl: c.logo_url ?? null,
309
- raw: c,
310
- });
311
- }
312
- // Fallback: floriday_customers_cache (camelCase, incl. nested address).
313
- for (const row of customersList.rows ?? []) {
314
- const d = row.data ?? {};
315
- const orgId = row.organization_id ?? d.organizationId ?? d.organization_id;
316
- if (!orgId)
317
- continue;
318
- if (lookup.has(String(orgId)))
319
- continue;
320
- const addr = d.physicalAddress ?? d.mailingAddress ?? {};
321
- lookup.set(String(orgId), {
322
- name: d.commercialName ?? d.name ?? d.commercial_name ?? d.company_name ?? "Onbekende klant",
323
- postalCode: addr.postalCode ?? addr.postal_code ?? d.postal_code ?? "",
324
- city: addr.city ?? d.city ?? "",
325
- logoUrl: d.logoUrl ?? d.logo_url ?? null,
326
- raw: d,
327
- });
328
- }
329
- const tradeItemLookup = new Map();
330
- for (const row of tradeItemsList.rows ?? []) {
331
- const d = row.data ?? {};
332
- const id = String(row.floriday_id ?? d.tradeItemId ?? "");
333
- if (!id)
334
- continue;
335
- const photos = d.photos ?? d.images ?? d.tradeItemPhotos ?? [];
336
- const firstPhotoRaw = Array.isArray(photos) && photos.length > 0
337
- ? (photos[0]?.url ?? photos[0]?.imageUrl ?? photos[0]?.href ?? photos[0])
338
- : null;
339
- const firstPhoto = pickString(firstPhotoRaw);
340
- const name = pickString(d.name) ??
341
- pickString(d.productName) ??
342
- pickString(d.tradeItemName) ??
343
- pickString(d.tradeItemTexts) ??
344
- "Onbekend artikel";
345
- const sku = pickString(d.vbnProductCode) ??
346
- pickString(d.gtin) ??
347
- pickString(d.articleCode);
348
- tradeItemLookup.set(id, { name, imageUrl: firstPhoto, sku, raw: d });
349
- }
350
- // Buffer-informatie uit de lokale orders-tabel. Voor gemirrorde FO's
351
- // vinden we hem via floriday_sales_order_id; voor lokale orders via id.
352
- const bufferByFoId = new Map();
353
- const bufferByLocalId = new Map();
354
- for (const r of localOrderRows) {
355
- const bid = r.buffer_id;
356
- if (!bid)
357
- continue;
358
- const foId = r.floriday_sales_order_id;
359
- if (foId)
360
- bufferByFoId.set(String(foId), bid);
361
- else
362
- bufferByLocalId.set(String(r.id), bid);
363
- }
364
- const floridayOrders = (ordersList.rows ?? []).map((r) => {
365
- const dto = mapJsonbToSalesOrderDTO({ ...r.data, __controlledAt: r.controlled_at ?? null }, lookup, tradeItemLookup, cratePackageDescriptions);
366
- const bid = bufferByFoId.get(dto.id) ?? null;
367
- if (bid)
368
- dto.bufferId = bid;
369
- return dto;
370
- });
371
- const localCustomerLookup = new Map();
372
- for (const c of localCustomersRes.data ?? []) {
373
- localCustomerLookup.set(String(c.id), {
374
- name: c.company_name ?? "Onbekende klant",
375
- city: c.city ?? "",
376
- });
377
- }
378
- const localOrders = [];
379
- const includedOrderIds = new Set();
380
- // Belading per productgroep: head_product_id (products-rij) heeft
381
- // voorrang, val terug op de group-mirror (products.trade_item_key =
382
- // 'group:<id>'). Bij verkoop-lijnen met product_name "[Groep] X"
383
- // gebruiken we deze belading i.p.v. die van de eerste member.
384
- const [productGroupsRes, groupMirrorRes] = await Promise.all([
385
- supabase
386
- .from("product_groups")
387
- .select("id, name, head_product_id, head_trade_item_floriday_id")
388
- .limit(5000),
389
- supabase
390
- .from("products")
391
- .select("id, plants_per_crate, crates_per_layer, layers_per_cart, trade_item_key")
392
- .like("trade_item_key", "group:%")
393
- .limit(5000),
394
- ]);
395
- const mirrorByGroupId = new Map();
396
- for (const p of groupMirrorRes.data ?? []) {
397
- const key = p.trade_item_key;
398
- if (typeof key === "string" && key.startsWith("group:")) {
399
- mirrorByGroupId.set(key.slice(6), p);
400
- }
401
- }
402
- const headProductIds = Array.from(new Set((productGroupsRes.data ?? [])
403
- .map((g) => g.head_product_id)
404
- .filter((v) => typeof v === "string" && !!v)));
405
- const headProductById = new Map();
406
- if (headProductIds.length > 0) {
407
- const { data: hp } = await supabase
408
- .from("products")
409
- .select("id, plants_per_crate, crates_per_layer, layers_per_cart")
410
- .in("id", headProductIds);
411
- for (const p of hp ?? [])
412
- headProductById.set(p.id, p);
413
- }
414
- const packFromRow = (row) => row
415
- ? {
416
- plants_per_crate: Math.max(1, Number(row.plants_per_crate ?? 1)),
417
- crates_per_layer: Math.max(1, Number(row.crates_per_layer ?? 1)),
418
- layers_per_cart: Math.max(1, Number(row.layers_per_cart ?? 1)),
419
- }
420
- : null;
421
- const groupPackingByNameLower = new Map();
422
- for (const g of productGroupsRes.data ?? []) {
423
- const packing = packFromRow(headProductById.get(g.head_product_id)) ??
424
- packFromRow(mirrorByGroupId.get(g.id));
425
- const gname = g.name ? String(g.name).trim().toLowerCase() : "";
426
- if (packing && gname)
427
- groupPackingByNameLower.set(gname, packing);
428
- }
429
- const resolveLinePacking = (item) => {
430
- const name = String(item?.product_name ?? "");
431
- if (name.startsWith("[Groep] ")) {
432
- const gname = name.slice("[Groep] ".length).trim().toLowerCase();
433
- const gp = groupPackingByNameLower.get(gname);
434
- if (gp)
435
- return gp;
436
- }
437
- return item?.products ?? null;
438
- };
439
- // Bereken karren per lokale order via de karindeling-planner (inclusief
440
- // uitpakken van groep-artikelen naar members met eigen belading). Zo
441
- // matcht de kolom "Karren" in de tabel met de karindeling-kaart.
442
- const karCountByOrderId = new Map();
443
- await Promise.all(localOrderRows.map(async (r) => {
444
- const lineItems = Array.isArray(r?.order_items) ? r.order_items : [];
445
- if (!lineItems.length)
446
- return;
447
- const raws = lineItems.map((it, idx) => {
448
- const packing = resolveLinePacking(it);
449
- return {
450
- id: String(it?.id ?? `${r.id}#${idx}`),
451
- product_id: it?.product_id ?? null,
452
- product_name: String(it?.product_name ?? ""),
453
- quantity: Number(it?.quantity ?? 0),
454
- plants_per_crate: Math.max(1, Number(packing?.plants_per_crate ?? 1)),
455
- crates_per_layer: Math.max(1, Number(packing?.crates_per_layer ?? 1)),
456
- layers_per_cart: Math.max(1, Number(packing?.layers_per_cart ?? 1)),
457
- };
458
- });
459
- try {
460
- const expanded = await expandOrderItemsForPlanner(supabase, raws);
461
- if (!expanded.length)
462
- return;
463
- const planned = planCarts(expanded);
464
- karCountByOrderId.set(String(r.id), planned.carts.length);
465
- }
466
- catch {
467
- // fallback blijft de per-regel berekening hieronder
468
- }
469
- }));
470
- for (const r of localOrderRows) {
471
- // Skip gemirrorde Floriday-orders — die tonen we uit de FO-cache zodat
472
- // status/mapping/klantnaam consistent blijven met de originele FO.
473
- if (r.floriday_sales_order_id)
474
- continue;
475
- const cf = r.custom_fields ?? {};
476
- const sourceRaw = String(cf?.source ?? r.order_source ?? "manual").toLowerCase();
477
- const source = mailboxSources.has(sourceRaw) ? "mailbox" : "local";
478
- // Include alle lokale/ERP-orders uit de gedeelde orders-tabel. Alleen
479
- // records met expliciete mailbox-herkomst tonen we als MO.
480
- includedOrderIds.add(String(r.id));
481
- const lineItems = Array.isArray(r.order_items) ? r.order_items : [];
482
- const firstLine = lineItems[0] ?? null;
483
- const lineTotal = lineItems.reduce((sum, item) => sum + (toNumber(item?.quantity) || 0) * (toNumber(item?.unit_price) || 0), 0);
484
- const total = lineTotal > 0 ? lineTotal : toNumber(r.total);
485
- const totalQuantity = lineItems.reduce((sum, item) => sum + (toNumber(item?.quantity) || 0), 0);
486
- // Bereken fust en karren per regel op basis van de belading van elk artikel.
487
- let fustTotal = 0;
488
- let karTotalRaw = 0;
489
- const crateNames = [];
490
- const cartNames = [];
491
- for (const item of lineItems) {
492
- const pieces = toNumber(item?.quantity) || 0;
493
- if (pieces <= 0)
494
- continue;
495
- const packing = resolveLinePacking(item);
496
- const plantsPerCrate = Math.max(1, Number(packing?.plants_per_crate ?? 1));
497
- const cratesPerLayer = Math.max(0, Number(packing?.crates_per_layer ?? 0));
498
- const layersPerCart = Math.max(0, Number(packing?.layers_per_cart ?? 0));
499
- const fust = Math.ceil(pieces / plantsPerCrate);
500
- fustTotal += fust;
501
- const packagesPerCart = cratesPerLayer * layersPerCart;
502
- if (packagesPerCart > 0)
503
- karTotalRaw += fust / packagesPerCart;
504
- // Type ladingdrager van het gekoppelde artikel (Floriday-belading).
505
- const prod = item?.products ?? null;
506
- const fid = prod?.floriday_trade_item_id ? String(prod.floriday_trade_item_id) : null;
507
- const configs = (() => {
508
- const fromCache = fid ? tradeItemLookup.get(fid)?.raw?.packingConfigurations : null;
509
- if (Array.isArray(fromCache) && fromCache.length > 0)
510
- return fromCache;
511
- const payload = prod?.custom_fields?.floriday_payload?.packingConfigurations;
512
- return Array.isArray(payload) ? payload : [];
513
- })();
514
- const cfg = pickPackingConfigForLine(configs, packing);
515
- crateNames.push(cratePackageName(cfg?.package?.vbnPackageCode ?? cfg?.package?.customPackageId ?? null, cratePackageDescriptions));
516
- cartNames.push(loadCarrierName(cfg?.loadCarrier ?? null));
517
- }
518
- const crateTypeName = mergeTypeNames(crateNames);
519
- const cartTypeName = mergeTypeNames(cartNames);
520
- const plannedKarCount = karCountByOrderId.get(String(r.id));
521
- const karCount = plannedKarCount && plannedKarCount > 0
522
- ? plannedKarCount
523
- : (karTotalRaw > 0 ? Math.max(1, Math.ceil(karTotalRaw)) : (totalQuantity > 0 ? 1 : 0));
524
- const fustCount = fustTotal > 0 ? fustTotal : totalQuantity;
525
- const unitPrice = firstLine ? toNumber(firstLine.unit_price) : total;
526
- const rawOrderDate = r.order_date ?? "";
527
- // Als order_date alleen een datum is (geen tijd), val terug op created_at
528
- // zodat mailbox-orders het echte binnenkomst-tijdstip tonen i.p.v. 02:00.
529
- const orderedAt = (typeof rawOrderDate === "string" && rawOrderDate.length > 0 && rawOrderDate.includes("T"))
530
- ? rawOrderDate
531
- : (r.created_at ?? rawOrderDate ?? "");
532
- // Leverdatum is een kale datum; de aanlevertijd komt uit het
533
- // aanmeldmoment van de vervoerder (bv. 20:00). Zonder tijd zou de UI
534
- // 02:00 tonen (UTC-middernacht omgerekend naar Amsterdam).
535
- const rawDeliveryDate = String(r.delivery_date ?? "");
536
- const orderCf = (r.custom_fields ?? {});
537
- let deliveryTime = normalizeTime(orderCf.delivery_time);
538
- if (!deliveryTime && rawDeliveryDate && orderCf.carrier) {
539
- const createdAt = r.created_at ? new Date(r.created_at) : new Date();
540
- deliveryTime = resolveDeliveryTimeForDate(carrierById.get(String(orderCf.carrier)) ?? null, rawDeliveryDate.slice(0, 10), createdAt);
541
- }
542
- const deliveryAt = rawDeliveryDate && !rawDeliveryDate.includes("T") && deliveryTime
543
- ? `${rawDeliveryDate.slice(0, 10)}T${deliveryTime}:00`
544
- : rawDeliveryDate;
545
- const customerId = r.customer_id ? String(r.customer_id) : "";
546
- const localCustomer = customerId ? localCustomerLookup.get(customerId) : undefined;
547
- const firstProduct = pickString(firstLine?.product_name);
548
- const firstProductPacking = firstLine ? resolveLinePacking(firstLine) : null;
549
- const firstPlantsPerCrate = Math.max(1, Number(firstProductPacking?.plants_per_crate ?? 1));
550
- const firstCratesPerLayer = Math.max(0, Number(firstProductPacking?.crates_per_layer ?? 0));
551
- const firstLayersPerCart = Math.max(0, Number(firstProductPacking?.layers_per_cart ?? 0));
552
- const localRawOrder = {
553
- ...r,
554
- packingConfiguration: firstProductPacking
555
- ? {
556
- ...(r.packingConfiguration ?? {}),
557
- piecesPerPackage: firstPlantsPerCrate,
558
- packagesPerLayer: firstCratesPerLayer,
559
- layersPerLoadCarrier: firstLayersPerCart,
560
- }
561
- : r.packingConfiguration,
562
- numberOfPieces: totalQuantity,
563
- };
564
- const productName = firstProduct
565
- ? lineItems.length > 1
566
- ? `${firstProduct} +${lineItems.length - 1}`
567
- : firstProduct
568
- : source === "mailbox" ? "Mailbox-bestelling" : "Lokale bestelling";
569
- localOrders.push({
570
- id: `local:${r.id}`,
571
- orderNumber: centralOrderNumber(r.order_number),
572
- productName,
573
- sku: source === "mailbox" ? "Mailbox-bestelling" : "Lokale bestelling",
574
- imageUrl: null,
575
- orderedAt,
576
- deliveryAt,
577
- quantity: {
578
- multiplier: totalQuantity > 0 ? Math.ceil(totalQuantity / firstPlantsPerCrate) : 1,
579
- count: firstPlantsPerCrate,
580
- unitPrice,
581
- },
582
- revenue: total,
583
- fustCount,
584
- karCount,
585
- crateTypeName,
586
- cartTypeName,
587
- loadCarrierType: normalizeLoadCarrierCode(orderCf.load_carrier_type),
588
- totalPieces: totalQuantity,
589
- isLocal: true,
590
- isReservation: !!r.is_reservation,
591
- customer: {
592
- name: r.customer_name ?? localCustomer?.name ?? "Onbekende klant",
593
- postalCode: "",
594
- city: localCustomer?.city ?? "",
595
- logoUrl: null,
596
- },
597
- status: statusFlags(r.status),
598
- raw: { order: localRawOrder, customer: null, tradeItem: null },
599
- bufferId: bufferByLocalId.get(String(r.id)) ?? null,
600
- });
601
- }
602
- for (const p of mailboxProposalsRes.data ?? []) {
603
- if (p.created_order_id && includedOrderIds.has(String(p.created_order_id))) {
604
- continue;
605
- }
606
- const msgRaw = p.mailbox_messages;
607
- const msg = Array.isArray(msgRaw) ? msgRaw[0] : msgRaw;
608
- const lines = Array.isArray(p.mailbox_proposal_lines)
609
- ? p.mailbox_proposal_lines
610
- : [];
611
- const firstLine = lines[0] ?? null;
612
- const matchedCustomerId = p.matched_customer_id ? String(p.matched_customer_id) : "";
613
- const matchedCustomer = matchedCustomerId ? localCustomerLookup.get(matchedCustomerId) : undefined;
614
- const quantity = Number(firstLine?.quantity ?? 1) || 1;
615
- localOrders.push({
616
- id: `mailbox-proposal:${p.id}`,
617
- orderNumber: centralOrderNumber(p.order_number),
618
- productName: pickString(firstLine?.raw_product_text) ?? pickString(msg?.subject) ?? "Mailbox-bestelling",
619
- sku: "Mailbox-bestelling",
620
- imageUrl: null,
621
- orderedAt: msg?.received_at ?? p.created_at ?? "",
622
- deliveryAt: p.delivery_date ?? "",
623
- quantity: { multiplier: quantity, count: 1, unitPrice: 0 },
624
- revenue: 0,
625
- fustCount: 0,
626
- karCount: quantity > 0 ? 1 : 0,
627
- totalPieces: quantity,
628
- isLocal: true,
629
- customer: {
630
- name: matchedCustomer?.name ?? msg?.from_name ?? msg?.from_email ?? "Onbekende klant",
631
- postalCode: "",
632
- city: matchedCustomer?.city ?? "",
633
- logoUrl: null,
634
- },
635
- status: statusFlags(p.status),
636
- raw: { order: p, customer: matchedCustomer ?? null, tradeItem: null },
637
- });
638
- }
639
- const orders = [...localOrders, ...floridayOrders].sort((a, b) => (b.orderedAt ?? "").localeCompare(a.orderedAt ?? ""));
640
- // Bereken bufferSize per bufferId over alle zichtbare orders.
641
- const sizeByBuffer = new Map();
642
- for (const o of orders) {
643
- if (o.bufferId)
644
- sizeByBuffer.set(o.bufferId, (sizeByBuffer.get(o.bufferId) ?? 0) + 1);
645
- }
646
- for (const o of orders) {
647
- if (o.bufferId)
648
- o.bufferSize = sizeByBuffer.get(o.bufferId) ?? 1;
649
- }
650
- const conn = connectionsRes.data?.[0];
651
- const sellerFallback = conn?.organization_name ?? conn?.label ?? null;
652
- return { orders, sellerFallback, environment };
653
- },
654
- staleTime: 15000,
655
- refetchInterval: 30000,
656
- refetchOnWindowFocus: true,
657
- });
112
+ return query;
658
113
  }