@flowselections/floriday-verkoop-module 1.0.60 → 1.0.61

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 (140) hide show
  1. package/dist-lib/components/floriday/FlorydaySyncHealthBanner.d.ts +9 -0
  2. package/dist-lib/components/floriday/FlorydaySyncHealthBanner.d.ts.map +1 -0
  3. package/dist-lib/components/floriday/FlorydaySyncHealthBanner.js +26 -0
  4. package/dist-lib/components/karindeling/AfmetingenTab.d.ts +6 -0
  5. package/dist-lib/components/karindeling/AfmetingenTab.d.ts.map +1 -0
  6. package/dist-lib/components/karindeling/AfmetingenTab.js +128 -0
  7. package/dist-lib/components/karindeling/KarindelingPage.d.ts +2 -0
  8. package/dist-lib/components/karindeling/KarindelingPage.d.ts.map +1 -0
  9. package/dist-lib/components/karindeling/KarindelingPage.js +102 -0
  10. package/dist-lib/components/ui/dropdown-menu.d.ts +9 -0
  11. package/dist-lib/components/ui/dropdown-menu.d.ts.map +1 -0
  12. package/dist-lib/components/ui/dropdown-menu.js +14 -0
  13. package/dist-lib/components/verkoop/AnalyticsCards.d.ts +1 -1
  14. package/dist-lib/components/verkoop/AnalyticsCards.d.ts.map +1 -1
  15. package/dist-lib/components/verkoop/AutoFulfillmentStatusCard.d.ts +1 -1
  16. package/dist-lib/components/verkoop/AutoFulfillmentStatusCard.d.ts.map +1 -1
  17. package/dist-lib/components/verkoop/CartAssignmentWizard.d.ts +1 -1
  18. package/dist-lib/components/verkoop/CartAssignmentWizard.d.ts.map +1 -1
  19. package/dist-lib/components/verkoop/CartLayersDialog.d.ts +1 -1
  20. package/dist-lib/components/verkoop/CartLayersDialog.d.ts.map +1 -1
  21. package/dist-lib/components/verkoop/CreateOrderDialog.d.ts +5 -0
  22. package/dist-lib/components/verkoop/CreateOrderDialog.d.ts.map +1 -0
  23. package/dist-lib/components/verkoop/CreateOrderDialog.js +33 -0
  24. package/dist-lib/components/verkoop/CreateOrderPage.d.ts +1 -1
  25. package/dist-lib/components/verkoop/CreateOrderPage.d.ts.map +1 -1
  26. package/dist-lib/components/verkoop/DiagnoseConnectionDialog.d.ts +1 -1
  27. package/dist-lib/components/verkoop/DiagnoseConnectionDialog.d.ts.map +1 -1
  28. package/dist-lib/components/verkoop/EditLocalOrderDialog.d.ts +1 -1
  29. package/dist-lib/components/verkoop/EditLocalOrderDialog.d.ts.map +1 -1
  30. package/dist-lib/components/verkoop/EngineBufferCard.d.ts +1 -1
  31. package/dist-lib/components/verkoop/EngineBufferCard.d.ts.map +1 -1
  32. package/dist-lib/components/verkoop/InvoiceTimingSettings.d.ts +1 -1
  33. package/dist-lib/components/verkoop/InvoiceTimingSettings.d.ts.map +1 -1
  34. package/dist-lib/components/verkoop/OrderAfleverbrievenCard.d.ts +1 -1
  35. package/dist-lib/components/verkoop/OrderAfleverbrievenCard.d.ts.map +1 -1
  36. package/dist-lib/components/verkoop/OrderCarrierPicker.d.ts +1 -1
  37. package/dist-lib/components/verkoop/OrderCarrierPicker.d.ts.map +1 -1
  38. package/dist-lib/components/verkoop/OrderControlStatusCard.d.ts +1 -1
  39. package/dist-lib/components/verkoop/OrderControlStatusCard.d.ts.map +1 -1
  40. package/dist-lib/components/verkoop/OrderKarindelingCard.d.ts +1 -1
  41. package/dist-lib/components/verkoop/OrderKarindelingCard.d.ts.map +1 -1
  42. package/dist-lib/components/verkoop/OrderPickupMomentPicker.d.ts +1 -1
  43. package/dist-lib/components/verkoop/OrderPickupMomentPicker.d.ts.map +1 -1
  44. package/dist-lib/components/verkoop/OrderShipmentsCard.d.ts +1 -1
  45. package/dist-lib/components/verkoop/OrderShipmentsCard.d.ts.map +1 -1
  46. package/dist-lib/components/verkoop/ProductDimensionsSettings.d.ts +1 -1
  47. package/dist-lib/components/verkoop/ProductDimensionsSettings.d.ts.map +1 -1
  48. package/dist-lib/components/verkoop/SalesOrderDetailPage.d.ts +1 -1
  49. package/dist-lib/components/verkoop/SalesOrderDetailPage.d.ts.map +1 -1
  50. package/dist-lib/components/verkoop/SalesOrderDetailView.d.ts +1 -1
  51. package/dist-lib/components/verkoop/SalesOrderDetailView.d.ts.map +1 -1
  52. package/dist-lib/components/verkoop/SalesOrdersFilterPanel.d.ts +1 -1
  53. package/dist-lib/components/verkoop/SalesOrdersFilterPanel.d.ts.map +1 -1
  54. package/dist-lib/components/verkoop/SalesOrdersFilters.d.ts +1 -1
  55. package/dist-lib/components/verkoop/SalesOrdersFilters.d.ts.map +1 -1
  56. package/dist-lib/components/verkoop/SalesOrdersTable.d.ts +1 -2
  57. package/dist-lib/components/verkoop/SalesOrdersTable.d.ts.map +1 -1
  58. package/dist-lib/components/verkoop/SearchableSelect.d.ts +1 -1
  59. package/dist-lib/components/verkoop/SearchableSelect.d.ts.map +1 -1
  60. package/dist-lib/components/verkoop/StockConsumptionAlertsBanner.d.ts +1 -1
  61. package/dist-lib/components/verkoop/StockConsumptionAlertsBanner.d.ts.map +1 -1
  62. package/dist-lib/components/verkoop/VerkoopOrdersPage.d.ts +1 -1
  63. package/dist-lib/components/verkoop/VerkoopOrdersPage.d.ts.map +1 -1
  64. package/dist-lib/components/verkoop/WeekCalendar.d.ts +1 -1
  65. package/dist-lib/components/verkoop/WeekCalendar.d.ts.map +1 -1
  66. package/dist-lib/integrations/supabase/auth-middleware.d.ts +280 -0
  67. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  68. package/dist-lib/integrations/supabase/client.server.d.ts +280 -0
  69. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  70. package/dist-lib/integrations/supabase/types.d.ts +280 -0
  71. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  72. package/dist-lib/lib/buffer-events.functions.d.ts +280 -0
  73. package/dist-lib/lib/buffer-events.functions.d.ts.map +1 -1
  74. package/dist-lib/lib/cart-layers.functions.d.ts +1124 -36
  75. package/dist-lib/lib/cart-layers.functions.d.ts.map +1 -1
  76. package/dist-lib/lib/cart-lock.functions.d.ts +280 -0
  77. package/dist-lib/lib/cart-lock.functions.d.ts.map +1 -1
  78. package/dist-lib/lib/cart-planning-settings.functions.d.ts +3080 -0
  79. package/dist-lib/lib/cart-planning-settings.functions.d.ts.map +1 -1
  80. package/dist-lib/lib/cart-replan.functions.d.ts +840 -0
  81. package/dist-lib/lib/cart-replan.functions.d.ts.map +1 -1
  82. package/dist-lib/lib/create-order.functions.d.ts +4777 -102
  83. package/dist-lib/lib/create-order.functions.d.ts.map +1 -1
  84. package/dist-lib/lib/cron/push-order-statuses.d.ts +4 -0
  85. package/dist-lib/lib/cron/push-order-statuses.d.ts.map +1 -0
  86. package/dist-lib/lib/cron/push-order-statuses.js +23 -0
  87. package/dist-lib/lib/floriday/floriday.server.d.ts +14 -0
  88. package/dist-lib/lib/floriday/floriday.server.d.ts.map +1 -0
  89. package/dist-lib/lib/floriday/floriday.server.js +92 -0
  90. package/dist-lib/lib/floriday/sales-orders.functions.d.ts +4736 -0
  91. package/dist-lib/lib/floriday/sales-orders.functions.d.ts.map +1 -0
  92. package/dist-lib/lib/floriday/sales-orders.functions.js +193 -0
  93. package/dist-lib/lib/floriday-auto-fulfillment.functions.d.ts +13288 -2362
  94. package/dist-lib/lib/floriday-auto-fulfillment.functions.d.ts.map +1 -1
  95. package/dist-lib/lib/floriday-logistic-labels.functions.d.ts +560 -0
  96. package/dist-lib/lib/floriday-logistic-labels.functions.d.ts.map +1 -1
  97. package/dist-lib/lib/floriday-mirror.functions.d.ts +280 -0
  98. package/dist-lib/lib/floriday-mirror.functions.d.ts.map +1 -1
  99. package/dist-lib/lib/floriday-mirror.functions.js +282 -117
  100. package/dist-lib/lib/floriday-public.functions.d.ts +1960 -0
  101. package/dist-lib/lib/floriday-public.functions.d.ts.map +1 -1
  102. package/dist-lib/lib/floriday-status-sync.server.d.ts +14 -0
  103. package/dist-lib/lib/floriday-status-sync.server.d.ts.map +1 -0
  104. package/dist-lib/lib/floriday-status-sync.server.js +130 -0
  105. package/dist-lib/lib/floriday-writes.functions.d.ts +560 -0
  106. package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
  107. package/dist-lib/lib/invoice-timing.functions.d.ts +1120 -0
  108. package/dist-lib/lib/invoice-timing.functions.d.ts.map +1 -1
  109. package/dist-lib/lib/load-carriers.functions.d.ts +280 -0
  110. package/dist-lib/lib/load-carriers.functions.d.ts.map +1 -1
  111. package/dist-lib/lib/logistics-engine.functions.d.ts +1405 -15
  112. package/dist-lib/lib/logistics-engine.functions.d.ts.map +1 -1
  113. package/dist-lib/lib/logistics-stock.functions.d.ts +560 -0
  114. package/dist-lib/lib/logistics-stock.functions.d.ts.map +1 -1
  115. package/dist-lib/lib/order-addresses.functions.d.ts +280 -0
  116. package/dist-lib/lib/order-addresses.functions.d.ts.map +1 -1
  117. package/dist-lib/lib/order-buffers.functions.d.ts +840 -0
  118. package/dist-lib/lib/order-buffers.functions.d.ts.map +1 -1
  119. package/dist-lib/lib/order-carrier.functions.d.ts +560 -0
  120. package/dist-lib/lib/order-carrier.functions.d.ts.map +1 -1
  121. package/dist-lib/lib/order-fulfill.functions.d.ts +280 -0
  122. package/dist-lib/lib/order-fulfill.functions.d.ts.map +1 -1
  123. package/dist-lib/lib/order-pickup.functions.d.ts +560 -0
  124. package/dist-lib/lib/order-pickup.functions.d.ts.map +1 -1
  125. package/dist-lib/lib/order-shipments.functions.d.ts +1120 -0
  126. package/dist-lib/lib/order-shipments.functions.d.ts.map +1 -1
  127. package/dist-lib/lib/product-dimensions.functions.d.ts +560 -0
  128. package/dist-lib/lib/product-dimensions.functions.d.ts.map +1 -1
  129. package/dist-lib/lib/salesorders-list.functions.d.ts +560 -0
  130. package/dist-lib/lib/salesorders-list.functions.d.ts.map +1 -1
  131. package/dist-lib/lib/salesorders-list.server.d.ts.map +1 -1
  132. package/dist-lib/lib/salesorders-list.server.js +62 -3
  133. package/dist-lib/lib/salesorders.functions.d.ts +1680 -0
  134. package/dist-lib/lib/salesorders.functions.d.ts.map +1 -1
  135. package/dist-lib/lib/salesorders.functions.js +6 -1
  136. package/dist-lib/lib/stock-consumption-alerts.functions.d.ts +560 -0
  137. package/dist-lib/lib/stock-consumption-alerts.functions.d.ts.map +1 -1
  138. package/dist-lib/lib/tradeitems.functions.d.ts +280 -0
  139. package/dist-lib/lib/tradeitems.functions.d.ts.map +1 -1
  140. package/package.json +1 -1
@@ -81,8 +81,9 @@ async function mirrorInternal(limit) {
81
81
  }
82
82
  const { data: existing } = await supabaseAdmin
83
83
  .from("orders")
84
- .select("id, floriday_sales_order_id, updated_at, custom_fields, status")
85
- .in("floriday_sales_order_id", foIds);
84
+ .select("id, order_number, floriday_sales_order_id, floriday_order_group_key, updated_at, custom_fields, status, plan_locked_at, buffer_id")
85
+ .in("floriday_sales_order_id", foIds)
86
+ .is("deleted_at", null);
86
87
  const existingByFoId = new Map();
87
88
  for (const o of existing ?? []) {
88
89
  existingByFoId.set(String(o.floriday_sales_order_id), o);
@@ -143,23 +144,38 @@ async function mirrorInternal(limit) {
143
144
  let inserted = 0, updated = 0, skipped = 0, failed = 0;
144
145
  const errors = [];
145
146
  const affectedOrderIds = [];
146
- for (const row of rows) {
147
+ // Statusmapping Floriday -> onze pickstatus. "gemeten" = feitelijk
148
+ // aangetroffen in floriday_salesorders_cache; "uit code" = staat wel in de
149
+ // Floriday-types/onze code, maar is nooit in de data gezien.
150
+ const FLORIDAY_STATUS_MAP = {
151
+ open: "Nog te picken", // uit code
152
+ new: "Nog te picken", // uit code
153
+ nieuw: "Nog te picken", // uit code
154
+ committed: "Nog te picken", // GEMETEN
155
+ confirmed: "Nog te picken", // uit code
156
+ accepted: "Nog te picken", // uit code
157
+ processing: "Nog te picken", // uit code
158
+ shipped: "Nog te picken", // uit code — Floriday-shipped is voor ons geen
159
+ delivered: "Nog te picken", // uit code — bewijs van vertrek; de karren-/
160
+ // shipmentroute bepaalt onze "Verzonden".
161
+ pending: "Nog te picken", // uit code
162
+ cancelled: "geannuleerd", // uit code
163
+ canceled: "geannuleerd", // uit code
164
+ rejected: "geannuleerd", // uit code
165
+ expired: "geannuleerd", // uit code
166
+ unfulfillable: "geannuleerd", // uit code
167
+ };
168
+ /** Eén Floriday-verkooporder = één toekomstige orderregel. */
169
+ function buildLine(row) {
147
170
  const foId = String(row.floriday_id);
148
171
  const d = row.data ?? {};
149
172
  const orgId = String(d?.customerOrganizationId ?? "");
150
173
  const cust = customerByOrgId.get(orgId);
151
174
  const floCust = floCustByOrgId.get(orgId) ?? {};
152
- const customerName = cust?.name ??
153
- floCust?.commercialName ??
154
- floCust?.name ??
155
- "Onbekende klant";
175
+ const customerName = cust?.name ?? floCust?.commercialName ?? floCust?.name ?? "Onbekende klant";
156
176
  const delivery = d?.delivery ?? {};
157
177
  const deliveryIso = delivery?.latestDeliveryDateTime ?? "";
158
178
  const orderDateIso = d?.orderDateTime ?? d?.creationDateTime ?? new Date().toISOString();
159
- // Uitsluitend uit de map: dat nummer is het ERP-nummer dat overal (lijst,
160
- // detail, pickstation) getoond wordt. Ontbreekt het, dan slaan we de order
161
- // over — nooit zelf een nummer verzinnen.
162
- const orderNumber = orderNumberByFoId.get(foId) ?? null;
163
179
  const pickupLocation = pickString(d?.despatchWarehouseId ?? delivery?.pickupLocation);
164
180
  const deliveryLocation = pickString(delivery?.location?.gln ??
165
181
  delivery?.locationGln ??
@@ -196,126 +212,280 @@ async function mirrorInternal(limit) {
196
212
  ? Boolean(ff.received) || isFullyFulfilled(ff, expectedPackagesFromSalesOrder(d))
197
213
  : false;
198
214
  const rawStatus = String(d?.status ?? "").toLowerCase() || "open";
199
- // Statusmapping Floriday -> onze pickstatus.
200
- // "gemeten" = feitelijk aangetroffen in floriday_salesorders_cache (meting
201
- // 2026-08-25: 1143 van 1143 rijen COMMITTED). "uit code" = staat wel in de
202
- // Floriday-types/onze code, maar is nooit in de data gezien; daar houdt de
203
- // zekerheid op.
204
- const FLORIDAY_STATUS_MAP = {
205
- open: "Nog te picken", // uit code
206
- new: "Nog te picken", // uit code
207
- nieuw: "Nog te picken", // uit code
208
- committed: "Nog te picken", // GEMETEN (1143/1143)
209
- confirmed: "Nog te picken", // uit code
210
- accepted: "Nog te picken", // uit code
211
- processing: "Nog te picken", // uit code
212
- shipped: "Nog te picken", // uit code — Floriday-shipped is voor ons geen
213
- delivered: "Nog te picken", // uit code — bewijs van vertrek; de karren-/
214
- // shipmentroute bepaalt onze "Verzonden".
215
- pending: "Nog te picken", // uit code
216
- cancelled: "geannuleerd", // uit code
217
- canceled: "geannuleerd", // uit code
218
- rejected: "geannuleerd", // uit code
219
- expired: "geannuleerd", // uit code
220
- unfulfillable: "geannuleerd", // uit code
221
- };
222
215
  const mapped = FLORIDAY_STATUS_MAP[rawStatus];
223
216
  if (!mapped) {
224
- // Nooit stilzwijgend doorzetten: een onbekende waarde kan anders de
225
- // consumptietrigger raken. Loggen met ruwe waarde zodat we hem kunnen
226
- // toevoegen aan de mapping.
227
217
  console.warn(`[floriday-mirror] onbekende Floriday-status "${rawStatus}" voor order ${foId} — val terug op "Nog te picken"`);
228
218
  }
229
- // fullyFulfilled betekent in Floriday alleen dat de beladingindeling is
230
- // doorgegeven; niet dat de order de kwekerij heeft verlaten. Daarom raakt
231
- // het de orderstatus niet meer, alleen custom_fields.
232
219
  const status = mapped ?? "Nog te picken";
233
- const custom_fields = {
234
- source: "floriday",
235
- pickup_location: pickupLocation,
236
- delivery_location: deliveryLocation,
237
- delivery_location_gln: delivery?.location?.gln ?? null,
238
- region_gln: delivery?.regionGln ?? null,
239
- floriday_sales_order_id: foId,
240
- floriday_status: d?.status ?? null,
241
- floriday_trade_item_id: tradeItemId || null,
242
- floriday_fulfilled: fullyFulfilled,
243
- // Betekenis ongewijzigd: fulfilled zonder dat wíj de fulfillment-order
244
- // aanmaakten. Nu wij zelf pushen klopt de naam alleen nog met die
245
- // createdByErp-check; daarom expliciet ook de tegenhanger vastleggen.
246
- floriday_fulfilled_externally: fullyFulfilled && !ff?.createdByErp,
247
- floriday_fulfilled_by_erp: fullyFulfilled && Boolean(ff?.createdByErp),
248
- floriday_delivery_notes: ff?.deliveryNoteCodes ?? [],
249
- floriday_received_at: ff?.receivedAt ?? null,
250
- floriday_packing: {
251
- pieces_per_package: piecesPerPackage,
252
- packages_per_layer: packagesPerLayer,
253
- layers_per_load_carrier: layersPerLoadCarrier,
254
- // Fustcode en ladingdrager volgen dezelfde bronvolgorde: de order
255
- // wint, het trade item is enkel terugval. Ontbreekt de code, dan
256
- // blijft hij leeg — nooit een verzonnen waarde (800/999 zijn
257
- // beweringen over de aanvoer, geen "onbekend").
258
- vbn_package_code: d?.packingConfiguration?.package?.vbnPackageCode ??
259
- (d?.packingConfiguration ? null : (packingCache?.package?.vbnPackageCode ?? null)),
260
- custom_package_id: d?.packingConfiguration?.package?.customPackageId ??
261
- (d?.packingConfiguration ? null : (packingCache?.package?.customPackageId ?? null)),
262
- load_carrier: d?.packingConfiguration?.loadCarrier ??
263
- (d?.packingConfiguration ? null : (packingCache?.loadCarrier ?? null)),
264
- source: d?.packingConfiguration ? "order" : packingCache ? "trade-item" : null,
265
- },
220
+ const packing = {
221
+ pieces_per_package: piecesPerPackage,
222
+ packages_per_layer: packagesPerLayer,
223
+ layers_per_load_carrier: layersPerLoadCarrier,
224
+ // Fustcode en ladingdrager volgen dezelfde bronvolgorde: de order wint,
225
+ // het trade item is enkel terugval. Ontbreekt de code, dan blijft hij
226
+ // leeg — nooit een verzonnen waarde.
227
+ vbn_package_code: d?.packingConfiguration?.package?.vbnPackageCode ??
228
+ (d?.packingConfiguration ? null : (packingCache?.package?.vbnPackageCode ?? null)),
229
+ custom_package_id: d?.packingConfiguration?.package?.customPackageId ??
230
+ (d?.packingConfiguration ? null : (packingCache?.package?.customPackageId ?? null)),
231
+ load_carrier: d?.packingConfiguration?.loadCarrier ??
232
+ (d?.packingConfiguration ? null : (packingCache?.loadCarrier ?? null)),
233
+ source: d?.packingConfiguration ? "order" : packingCache ? "trade-item" : null,
266
234
  };
267
- const existingOrder = existingByFoId.get(foId);
268
- if (!orderNumber) {
269
- skipped++;
270
- errors.push(`${foId}: geen ordernummer in floriday_order_number_map`);
235
+ return {
236
+ foId,
237
+ d,
238
+ orgId,
239
+ custId: cust?.id ?? null,
240
+ customerName,
241
+ delivery,
242
+ deliveryIso,
243
+ orderDateIso,
244
+ orderDateMs: Date.parse(orderDateIso) || 0,
245
+ sequenceNumber: Number(d?.sequenceNumber ?? 0),
246
+ pickupLocation,
247
+ deliveryLocation,
248
+ numberOfPieces,
249
+ tradeItemId,
250
+ productId,
251
+ productName,
252
+ plants_per_crate,
253
+ crates_per_layer,
254
+ layers_per_cart,
255
+ unitPrice: Number(d?.pricePerPiece?.value ?? 0),
256
+ totalPrice,
257
+ status,
258
+ ff,
259
+ fullyFulfilled,
260
+ packing,
261
+ };
262
+ }
263
+ const allLines = rows.map(buildLine);
264
+ // Floriday levert per artikel een aparte verkooporder; er is geen groeps-id
265
+ // in de payload (salesOrderGroupId/customerOrderId zijn leeg). Regels van
266
+ // dezelfde bestelling herkennen we daarom aan: zelfde klant, zelfde
267
+ // aflevermoment, zelfde afleverlocatie, zelfde uitleverlocatie én vlak na
268
+ // elkaar geplaatst.
269
+ const GROUP_WINDOW_MS = 5 * 60 * 1000;
270
+ const byKey = new Map();
271
+ for (const line of allLines) {
272
+ const key = [
273
+ line.orgId,
274
+ line.deliveryIso,
275
+ line.deliveryLocation ?? "",
276
+ line.pickupLocation ?? "",
277
+ line.status,
278
+ ].join("|");
279
+ const arr = byKey.get(key);
280
+ if (arr)
281
+ arr.push(line);
282
+ else
283
+ byKey.set(key, [line]);
284
+ }
285
+ const groups = [];
286
+ for (const arr of byKey.values()) {
287
+ arr.sort((a, b) => a.orderDateMs - b.orderDateMs || a.sequenceNumber - b.sequenceNumber);
288
+ let current = [];
289
+ for (const line of arr) {
290
+ if (current.length === 0 ||
291
+ line.orderDateMs - current[current.length - 1].orderDateMs <= GROUP_WINDOW_MS) {
292
+ current.push(line);
293
+ }
294
+ else {
295
+ groups.push(current);
296
+ current = [line];
297
+ }
298
+ }
299
+ if (current.length > 0)
300
+ groups.push(current);
301
+ }
302
+ /** Een bestaande order mag niet opgeruimd worden als hij al onderweg is. */
303
+ const isFrozen = (o) => {
304
+ const st = String(o?.status ?? "").toLowerCase();
305
+ return Boolean(o?.plan_locked_at) || st === "verzonden" || st === "shipped";
306
+ };
307
+ // Regels waarvan de bestaande order bevroren is, blijven losse orders.
308
+ const finalGroups = [];
309
+ for (const group of groups) {
310
+ if (group.length === 1) {
311
+ finalGroups.push(group);
271
312
  continue;
272
313
  }
314
+ const frozen = [];
315
+ const free = [];
316
+ for (const line of group) {
317
+ const ex = existingByFoId.get(line.foId);
318
+ if (ex && isFrozen(ex))
319
+ frozen.push(line);
320
+ else
321
+ free.push(line);
322
+ }
323
+ for (const line of frozen)
324
+ finalGroups.push([line]);
325
+ if (free.length > 0)
326
+ finalGroups.push(free);
327
+ }
328
+ // Vaste bestelsleutel per groep. Hij is deterministisch: dezelfde klant,
329
+ // hetzelfde aflevermoment, dezelfde aflever-/uitleverlocatie en het vroegste
330
+ // besteltijdstip (op de minuut) leveren altijd dezelfde sleutel op. Daardoor
331
+ // vindt een volgende spiegelronde exact dezelfde lokale order terug en valt
332
+ // een samengevoegde bestelling nooit opnieuw uiteen.
333
+ const groupKeyFor = (group) => {
334
+ const first = group.reduce((a, b) => (a.orderDateMs <= b.orderDateMs ? a : b));
335
+ const minuteIso = new Date(Math.floor((first.orderDateMs || 0) / 60000) * 60000).toISOString();
336
+ return [
337
+ "fo",
338
+ first.orgId,
339
+ first.deliveryIso,
340
+ first.deliveryLocation ?? "",
341
+ first.pickupLocation ?? "",
342
+ minuteIso,
343
+ ].join("|");
344
+ };
345
+ const keyedGroups = finalGroups.map((group) => ({ group, key: groupKeyFor(group) }));
346
+ // Bestaande orders die al aan een van deze bestelsleutels hangen. Zo vinden
347
+ // we ook de order terug waarvan de primaire Floriday-regel is gewijzigd.
348
+ const existingByGroupKey = new Map();
349
+ const allKeys = Array.from(new Set(keyedGroups.map((g) => g.key)));
350
+ if (allKeys.length > 0) {
351
+ const { data: keyRows } = await supabaseAdmin
352
+ .from("orders")
353
+ .select("id, order_number, floriday_sales_order_id, floriday_order_group_key, updated_at, custom_fields, status, plan_locked_at, buffer_id")
354
+ .in("floriday_order_group_key", allKeys)
355
+ .is("deleted_at", null);
356
+ for (const o of keyRows ?? []) {
357
+ const k = String(o.floriday_order_group_key ?? "");
358
+ if (!k)
359
+ continue;
360
+ const prev = existingByGroupKey.get(k);
361
+ // Laagste ordernummer wint; die order blijft bestaan bij samenvoegen.
362
+ if (!prev ||
363
+ String(o.order_number ?? "") < String(prev.order_number ?? "")) {
364
+ existingByGroupKey.set(k, o);
365
+ }
366
+ }
367
+ }
368
+ for (const { group, key: groupKey } of keyedGroups) {
369
+ const primary = group[0];
370
+ const memberIds = group.map((l) => l.foId);
371
+ const anyFulfilled = group.some((l) => l.fullyFulfilled);
372
+ const ffPrimary = primary.ff;
373
+ const totalPrice = group.reduce((s, l) => s + (Number(l.totalPrice) || 0), 0);
374
+ const custom_fields = {
375
+ source: "floriday",
376
+ pickup_location: primary.pickupLocation,
377
+ delivery_location: primary.deliveryLocation,
378
+ delivery_location_gln: primary.delivery?.location?.gln ?? null,
379
+ region_gln: primary.delivery?.regionGln ?? null,
380
+ floriday_sales_order_id: primary.foId,
381
+ // Alle Floriday-verkooporders die samen deze ene order vormen.
382
+ floriday_sales_order_ids: memberIds,
383
+ floriday_line_count: group.length,
384
+ floriday_status: primary.d?.status ?? null,
385
+ floriday_trade_item_id: primary.tradeItemId || null,
386
+ floriday_fulfilled: anyFulfilled,
387
+ floriday_fulfilled_externally: anyFulfilled && !ffPrimary?.createdByErp,
388
+ floriday_fulfilled_by_erp: anyFulfilled && Boolean(ffPrimary?.createdByErp),
389
+ floriday_delivery_notes: ffPrimary?.deliveryNoteCodes ?? [],
390
+ floriday_received_at: ffPrimary?.receivedAt ?? null,
391
+ floriday_packing: primary.packing,
392
+ floriday_lines: group.map((l) => ({
393
+ sales_order_id: l.foId,
394
+ trade_item_id: l.tradeItemId || null,
395
+ pieces: l.numberOfPieces,
396
+ packing: l.packing,
397
+ })),
398
+ };
273
399
  try {
400
+ // Bestaande order kiezen. Volgorde: de order die al aan deze
401
+ // bestelsleutel hangt (dat is de bestelling zelf), dan die van de
402
+ // primaire regel, dan de eerste vrije order van een andere regel.
403
+ let target = existingByGroupKey.get(groupKey) ?? null;
404
+ if (target && isFrozen(target))
405
+ target = null;
406
+ if (!target) {
407
+ const byPrimary = existingByFoId.get(primary.foId) ?? null;
408
+ if (byPrimary && !isFrozen(byPrimary))
409
+ target = byPrimary;
410
+ }
411
+ if (!target) {
412
+ for (const l of group) {
413
+ const ex = existingByFoId.get(l.foId);
414
+ if (ex && !isFrozen(ex)) {
415
+ target = ex;
416
+ break;
417
+ }
418
+ }
419
+ }
420
+ // Ordernummer: een bestaande order houdt zijn nummer, anders het nummer
421
+ // uit floriday_order_number_map van de primaire regel.
422
+ const orderNumber = (target ? String(target.order_number ?? "").trim() : "") ||
423
+ orderNumberByFoId.get(primary.foId) ||
424
+ null;
425
+ if (!orderNumber) {
426
+ skipped += group.length;
427
+ errors.push(`${primary.foId}: geen ordernummer in floriday_order_number_map`);
428
+ continue;
429
+ }
430
+ // Dubbele orders van dezelfde bestelling opruimen: alles wat via een
431
+ // regel of via dezelfde bestelsleutel bestaat en niet het doel is.
432
+ const obsolete = [
433
+ ...group.map((l) => existingByFoId.get(l.foId)),
434
+ existingByGroupKey.get(groupKey),
435
+ ].filter((o) => o && (!target || o.id !== target.id) && !isFrozen(o));
436
+ const obsoleteIds = Array.from(new Set(obsolete.map((o) => String(o.id))));
437
+ if (obsoleteIds.length > 0) {
438
+ await supabaseAdmin.from("order_items").delete().in("order_id", obsoleteIds);
439
+ await supabaseAdmin.from("orders")
440
+ .update({ deleted_at: new Date().toISOString(), buffer_id: null })
441
+ .in("id", obsoleteIds);
442
+ }
274
443
  let orderId;
275
- if (existingOrder) {
444
+ if (target) {
276
445
  // Tweerichtingssynchronisatie: de Floriday-status overschrijft onze
277
446
  // status alleen als hij aan de Floriday-kant echt is veranderd sinds de
278
- // vorige spiegelronde. Anders zou een wijziging uit ons ERP (bv.
279
- // "Verzonden") bij elke ronde teruggedraaid worden.
280
- const prevFloridayStatus = (existingOrder?.custom_fields ?? {})?.floriday_status ?? null;
281
- const floridayStatusChanged = String(d?.status ?? "") !== String(prevFloridayStatus ?? "");
282
- const localStatus = String(existingOrder?.status ?? "");
283
- const statusPatch = floridayStatusChanged && status !== localStatus
284
- ? { status, status_sync_token: globalThis.crypto.randomUUID() }
447
+ // vorige spiegelronde.
448
+ const prevFloridayStatus = (target?.custom_fields ?? {})?.floriday_status ?? null;
449
+ const floridayStatusChanged = String(primary.d?.status ?? "") !== String(prevFloridayStatus ?? "");
450
+ const localStatus = String(target?.status ?? "");
451
+ const statusPatch = floridayStatusChanged && primary.status !== localStatus
452
+ ? { status: primary.status, status_sync_token: globalThis.crypto.randomUUID() }
285
453
  : {};
286
- // Update basiskolommen; laat bestaande karren staan tot replan-queue draait.
287
454
  const { error: uErr } = await supabaseAdmin.from("orders")
288
455
  .update({
289
456
  order_number: orderNumber,
290
- customer_id: cust?.id ?? null,
291
- customer_name: customerName,
292
- delivery_date: deliveryIso ? deliveryIso.slice(0, 10) : null,
293
- order_date: orderDateIso,
457
+ customer_id: primary.custId,
458
+ customer_name: primary.customerName,
459
+ delivery_date: primary.deliveryIso ? primary.deliveryIso.slice(0, 10) : null,
460
+ order_date: primary.orderDateIso,
294
461
  ...statusPatch,
295
462
  total: String(totalPrice),
296
463
  custom_fields,
297
464
  order_source: "floriday",
465
+ floriday_sales_order_id: primary.foId,
466
+ floriday_order_group_key: groupKey,
467
+ deleted_at: null,
298
468
  })
299
- .eq("id", existingOrder.id);
469
+ .eq("id", target.id);
300
470
  if (uErr)
301
471
  throw new Error(uErr.message);
302
- orderId = existingOrder.id;
472
+ orderId = target.id;
303
473
  updated++;
304
474
  }
305
475
  else {
306
476
  const { data: ins, error: iErr } = await supabaseAdmin.from("orders")
307
477
  .insert({
308
478
  order_number: orderNumber,
309
- customer_name: customerName,
310
- customer_id: cust?.id ?? null,
311
- order_date: orderDateIso,
312
- delivery_date: deliveryIso ? deliveryIso.slice(0, 10) : null,
313
- status,
479
+ customer_name: primary.customerName,
480
+ customer_id: primary.custId,
481
+ order_date: primary.orderDateIso,
482
+ delivery_date: primary.deliveryIso ? primary.deliveryIso.slice(0, 10) : null,
483
+ status: primary.status,
314
484
  total: String(totalPrice),
315
485
  custom_fields,
316
486
  order_source: "floriday",
317
- floriday_sales_order_id: foId,
318
- // Nieuwe spiegelorder: de status komt uit Floriday.
487
+ floriday_sales_order_id: primary.foId,
488
+ floriday_order_group_key: groupKey,
319
489
  status_sync_token: globalThis.crypto.randomUUID(),
320
490
  })
321
491
  .select("id")
@@ -325,30 +495,25 @@ async function mirrorInternal(limit) {
325
495
  orderId = ins.id;
326
496
  inserted++;
327
497
  }
328
- // Upsert de enkele orderregel (FO's zijn 1-artikel).
498
+ // Eén orderregel per Floriday-verkooporder in deze groep.
329
499
  await supabaseAdmin.from("order_items").delete().eq("order_id", orderId);
330
- const { error: liErr } = await supabaseAdmin.from("order_items").insert({
500
+ const { error: liErr } = await supabaseAdmin.from("order_items").insert(group.map((l) => ({
331
501
  order_id: orderId,
332
- product_id: productId,
333
- product_name: productName,
334
- quantity: Math.max(0, numberOfPieces),
502
+ product_id: l.productId,
503
+ product_name: l.productName,
504
+ quantity: Math.max(0, l.numberOfPieces),
335
505
  unit: "stuks",
336
- unit_price: Number(d?.pricePerPiece?.value ?? 0),
337
- });
506
+ unit_price: l.unitPrice,
507
+ plants_per_crate: l.plants_per_crate,
508
+ crates_per_layer: l.crates_per_layer,
509
+ layers_per_cart: l.layers_per_cart,
510
+ })));
338
511
  if (liErr)
339
512
  throw new Error(liErr.message);
340
- // Zorg dat product-packing gebruikt wordt door de planner. Als er nog
341
- // geen product-mapping is, valt de planner terug op 1×1×1 en dat wil
342
- // je niet — sla dan tijdelijke packing op via een "shadow" product
343
- // niet toe; in plaats daarvan berust de planner op de defaults.
344
- // (Toekomstige verbetering: shadow product per trade item.)
345
- void plants_per_crate;
346
- void crates_per_layer;
347
- void layers_per_cart;
348
513
  // Buffer resolven en in de replan-queue zetten. Alleen orders die buiten
349
514
  // ons ERP om in Floriday zijn afgehandeld hoeven niet meer gepland te
350
- // worden; een fulfillment die wíj zelf pushten moet gewoon gepickt.
351
- const externallyHandled = fullyFulfilled && !ff?.createdByErp;
515
+ // worden.
516
+ const externallyHandled = anyFulfilled && !ffPrimary?.createdByErp;
352
517
  if (!externallyHandled) {
353
518
  await resolveBufferForOrderInternal(orderId);
354
519
  affectedOrderIds.push(orderId);
@@ -357,7 +522,7 @@ async function mirrorInternal(limit) {
357
522
  catch (err) {
358
523
  failed++;
359
524
  const msg = err.message ?? "unknown";
360
- errors.push(`${foId}: ${msg.slice(0, 200)}`);
525
+ errors.push(`${primary.foId}: ${msg.slice(0, 200)}`);
361
526
  }
362
527
  }
363
528
  // Batch enqueue voor karherplanning. processed_at resetten, anders blijft een