@flowselections/floriday-afhandeling-module 1.0.44 → 1.0.45

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 (63) hide show
  1. package/dist-lib/components/afhandeling/ControlePage.d.ts.map +1 -1
  2. package/dist-lib/components/afhandeling/ControlePage.js +92 -54
  3. package/dist-lib/hooks/useCartIndex.d.ts.map +1 -1
  4. package/dist-lib/hooks/useCartIndex.js +16 -7
  5. package/dist-lib/hooks/useEngineBufferEvents.d.ts +5 -0
  6. package/dist-lib/hooks/useEngineBufferEvents.d.ts.map +1 -1
  7. package/dist-lib/hooks/useEngineBufferEvents.js +18 -13
  8. package/dist-lib/hooks/useEngineTasks.d.ts.map +1 -1
  9. package/dist-lib/hooks/useEngineTasks.js +8 -18
  10. package/dist-lib/hooks/useKnownOrders.d.ts.map +1 -1
  11. package/dist-lib/hooks/useKnownOrders.js +2 -1
  12. package/dist-lib/hooks/useOrderPickDates.d.ts +1 -1
  13. package/dist-lib/hooks/useOrderPickDates.d.ts.map +1 -1
  14. package/dist-lib/hooks/useOrderPickDates.js +5 -3
  15. package/dist-lib/hooks/usePickAgenda.d.ts.map +1 -1
  16. package/dist-lib/hooks/usePickAgenda.js +17 -13
  17. package/dist-lib/hooks/usePickMode.d.ts +8 -2
  18. package/dist-lib/hooks/usePickMode.d.ts.map +1 -1
  19. package/dist-lib/hooks/usePickMode.js +17 -9
  20. package/dist-lib/index.d.ts +1 -2
  21. package/dist-lib/index.d.ts.map +1 -1
  22. package/dist-lib/index.js +1 -33
  23. package/dist-lib/integrations/supabase/auth-middleware.d.ts +3053 -161
  24. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  25. package/dist-lib/integrations/supabase/client.server.d.ts +3053 -161
  26. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  27. package/dist-lib/integrations/supabase/moduleClient.d.ts +2982 -88
  28. package/dist-lib/integrations/supabase/moduleClient.d.ts.map +1 -1
  29. package/dist-lib/integrations/supabase/types.d.ts +3063 -143
  30. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  31. package/dist-lib/lib/afhandelingSettings.functions.d.ts +5798 -14
  32. package/dist-lib/lib/afhandelingSettings.functions.d.ts.map +1 -1
  33. package/dist-lib/lib/carriers.functions.d.ts +14510 -40
  34. package/dist-lib/lib/carriers.functions.d.ts.map +1 -1
  35. package/dist-lib/lib/cartAssignments.functions.d.ts +14495 -35
  36. package/dist-lib/lib/cartAssignments.functions.d.ts.map +1 -1
  37. package/dist-lib/lib/cartControl.functions.d.ts +31999 -104
  38. package/dist-lib/lib/cartControl.functions.d.ts.map +1 -1
  39. package/dist-lib/lib/cartControl.functions.js +389 -259
  40. package/dist-lib/lib/deliveryNote.functions.d.ts +3053 -161
  41. package/dist-lib/lib/deliveryNote.functions.d.ts.map +1 -1
  42. package/dist-lib/lib/engineLayouts.functions.d.ts +3053 -161
  43. package/dist-lib/lib/engineLayouts.functions.d.ts.map +1 -1
  44. package/dist-lib/lib/floriday.functions.d.ts +5798 -14
  45. package/dist-lib/lib/floriday.functions.d.ts.map +1 -1
  46. package/dist-lib/lib/orderFulfilment.functions.d.ts +3053 -161
  47. package/dist-lib/lib/orderFulfilment.functions.d.ts.map +1 -1
  48. package/dist-lib/lib/pickDates.functions.d.ts +2985 -88
  49. package/dist-lib/lib/pickDates.functions.d.ts.map +1 -1
  50. package/dist-lib/lib/pickDates.functions.js +30 -3
  51. package/dist-lib/lib/pickMode.d.ts +20 -2
  52. package/dist-lib/lib/pickMode.d.ts.map +1 -1
  53. package/dist-lib/lib/pickMode.js +65 -16
  54. package/dist-lib/lib/realtimeBus.d.ts +20 -0
  55. package/dist-lib/lib/realtimeBus.d.ts.map +1 -0
  56. package/dist-lib/lib/realtimeBus.js +56 -0
  57. package/dist-lib/lib/serverFnError.d.ts +12 -0
  58. package/dist-lib/lib/serverFnError.d.ts.map +1 -1
  59. package/dist-lib/lib/serverFnError.js +48 -1
  60. package/dist-lib/moduleDefinition.d.ts +3 -0
  61. package/dist-lib/moduleDefinition.d.ts.map +1 -0
  62. package/dist-lib/moduleDefinition.js +36 -0
  63. package/package.json +2 -2
@@ -1,6 +1,11 @@
1
1
  import { createServerFn } from "@tanstack/react-start";
2
2
  import { requireSupabaseAuth } from "../integrations/supabase/auth-middleware";
3
3
  import { tryReschedule } from "./cartRescheduling";
4
+ /**
5
+ * Voorzichtige bovengrens wanneer de klantinstelling ontbreekt of ongeldig is.
6
+ * Klantonafhankelijk; de werkelijke grens komt uit `afhandeling_settings`.
7
+ */
8
+ const CONSERVATIVE_BULK_MAX_CARTS = 10;
4
9
  function toIso(d) {
5
10
  const y = d.getFullYear();
6
11
  const m = String(d.getMonth() + 1).padStart(2, "0");
@@ -10,129 +15,130 @@ function toIso(d) {
10
15
  async function loadControlPlanningSettings(supabase, userId) {
11
16
  const { data: settings } = await supabase
12
17
  .from("afhandeling_settings")
13
- .select("pick_weekdays, pick_days_before_transport")
18
+ .select("pick_weekdays, pick_days_before_transport, control_bulk_max_carts")
14
19
  .eq("user_id", userId)
15
20
  .maybeSingle();
16
21
  const pickWeekdays = Array.isArray(settings?.pick_weekdays) && settings.pick_weekdays.length > 0
17
22
  ? settings.pick_weekdays
18
23
  : [1, 2, 3, 4, 5];
19
24
  const minWorkdays = Math.max(0, Number(settings?.pick_days_before_transport ?? 1));
20
- return { pickWeekdays, minWorkdays };
25
+ // Bovengrens per database-aanroep. Zonder geldige instelling geldt de
26
+ // voorzichtige ondergrens: nooit stil een grote portie proberen.
27
+ const configured = Number(settings?.control_bulk_max_carts);
28
+ const maxBulkCarts = Number.isFinite(configured) && configured >= 1 && configured <= 200
29
+ ? Math.floor(configured)
30
+ : CONSERVATIVE_BULK_MAX_CARTS;
31
+ return { pickWeekdays, minWorkdays, maxBulkCarts };
21
32
  }
22
33
  export function controlCartKey(cart) {
23
34
  return cart.cartNumberId ?? `${cart.orderSource}#${cart.orderId}#${cart.cartIndex}`;
24
35
  }
25
- async function loadControlCarts(supabase) {
36
+ async function loadControlCartsDetailed(supabase) {
37
+ const warnings = [];
26
38
  const { data, error } = await supabase.rpc("control_get_todays_carts");
27
39
  if (error)
28
40
  throw new Error(error.message);
29
41
  const allRows = (data ?? []);
30
42
  if (allRows.length === 0)
31
- return [];
32
- // Alleen orders die de Verkoop-module kent mogen zichtbaar zijn.
43
+ return { carts: [], rpcCount: 0, filteredOut: 0, warnings };
44
+ // Alleen orders die de Verkoop-module kent mogen zichtbaar zijn. Beide
45
+ // onafhankelijke zoekpaden lopen tegelijk; dezelfde rijen leveren meteen de
46
+ // lichte orderverrijking voor de controlelijst.
33
47
  const candidateIds = Array.from(new Set(allRows.map((r) => r.order_id).filter(Boolean)));
34
48
  const candidateNumbers = Array.from(new Set(allRows.map((r) => r.order_number).filter((v) => !!v)));
35
49
  const knownIds = new Set();
36
50
  const knownNumbers = new Set();
37
- if (candidateIds.length > 0) {
38
- const { data: byId } = await supabase
39
- .from("orders")
40
- .select("id, order_number")
41
- .is("deleted_at", null)
42
- .in("id", candidateIds);
43
- for (const o of (byId ?? [])) {
44
- knownIds.add(o.id);
45
- if (o.order_number)
46
- knownNumbers.add(o.order_number);
47
- }
48
- }
49
- if (candidateNumbers.length > 0) {
50
- const { data: byNumber } = await supabase
51
- .from("orders")
52
- .select("id, order_number")
53
- .is("deleted_at", null)
54
- .in("order_number", candidateNumbers);
55
- for (const o of (byNumber ?? [])) {
56
- knownIds.add(o.id);
57
- if (o.order_number)
58
- knownNumbers.add(o.order_number);
59
- }
51
+ const ORDER_SELECT = "id, order_number, customer_name, delivery_date, custom_fields";
52
+ const [byIdResult, byNumberResult] = await Promise.all([
53
+ candidateIds.length > 0
54
+ ? supabase
55
+ .from("orders")
56
+ .select(ORDER_SELECT)
57
+ .is("deleted_at", null)
58
+ .in("id", candidateIds)
59
+ : Promise.resolve({ data: [], error: null }),
60
+ candidateNumbers.length > 0
61
+ ? supabase
62
+ .from("orders")
63
+ .select(ORDER_SELECT)
64
+ .is("deleted_at", null)
65
+ .in("order_number", candidateNumbers)
66
+ : Promise.resolve({ data: [], error: null }),
67
+ ]);
68
+ if (byIdResult.error)
69
+ warnings.push(`Bestellingen opzoeken op id: ${byIdResult.error.message}`);
70
+ if (byNumberResult.error)
71
+ warnings.push(`Bestellingen opzoeken op nummer: ${byNumberResult.error.message}`);
72
+ const matchedOrders = [
73
+ ...(byIdResult.data ?? []),
74
+ ...(byNumberResult.data ?? []),
75
+ ];
76
+ for (const o of matchedOrders) {
77
+ knownIds.add(o.id);
78
+ if (o.order_number)
79
+ knownNumbers.add(o.order_number);
60
80
  }
61
81
  const rows = allRows.filter((r) => knownIds.has(r.order_id) || (r.order_number ? knownNumbers.has(r.order_number) : false));
62
- if (rows.length === 0)
63
- return [];
64
- // --- Verrijking 1: ordergegevens ---
65
- const orderIds = Array.from(new Set(rows.map((r) => r.order_id).filter(Boolean)));
66
- const orderMap = new Map();
67
- if (orderIds.length > 0) {
68
- const { data: orderRows } = await supabase
69
- .from("orders")
70
- .select("id, order_number, customer_name, delivery_date")
71
- .in("id", orderIds);
72
- for (const o of (orderRows ?? [])) {
73
- orderMap.set(o.id, {
74
- orderNumber: o.order_number ?? "",
75
- customerName: o.customer_name ?? "",
76
- deliveryDate: o.delivery_date ?? null,
77
- });
78
- }
82
+ if (rows.length === 0) {
83
+ return { carts: [], rpcCount: allRows.length, filteredOut: allRows.length, warnings };
79
84
  }
80
- const extras = new Map();
81
- try {
82
- const { loadTodaysPickstationOrders } = await import("./pickstationOrders.server");
83
- const psOrders = await loadTodaysPickstationOrders();
84
- for (const o of psOrders) {
85
- extras.set(`${o.orderId}#${o.cartIndex - 1}`, {
86
- cartNumber: o.cartNumber,
87
- carrierName: o.carrierName,
88
- pickupDate: o.pickupDate,
89
- pickupTime: o.pickupTime,
90
- country: o.country,
91
- cratesOnCart: o.cratesOnCart,
92
- printedCount: o.printedCount,
93
- orderNumbers: o.orderNumber ? [o.orderNumber] : [],
94
- });
95
- }
96
- }
97
- catch {
98
- // Verrijking is optioneel: zonder deze gegevens blijft de kar zichtbaar.
99
- }
100
- // --- Verrijking 3: totaal aantal karren per order uit de gedeelde karindex ---
101
- // De karindex van de Engine is de enige bron voor "hoeveel karren heeft deze
102
- // order". Lukt dat niet, dan valt het totaal terug op de karren die we hier
103
- // zien (nooit een verzonnen getal).
104
- const totalsByOrder = new Map();
105
- try {
106
- const pickDates = rows.map((r) => r.pick_date).filter(Boolean).sort();
107
- if (pickDates.length > 0) {
108
- const { data: idx } = await supabase.rpc("logistics_cart_index", {
109
- p_from: pickDates[0],
110
- p_to: pickDates[pickDates.length - 1],
111
- });
112
- const perRef = new Map();
113
- for (const row of (Array.isArray(idx) ? idx : [])) {
114
- if (!row.cart_id)
115
- continue;
116
- for (const ref of [row.order_uuid, row.order_number]) {
117
- if (!ref)
118
- continue;
119
- const set = perRef.get(ref) ?? new Set();
120
- set.add(row.cart_id);
121
- perRef.set(ref, set);
122
- }
123
- }
124
- for (const [ref, set] of perRef)
125
- totalsByOrder.set(ref, set.size);
126
- }
127
- }
128
- catch {
129
- // Zonder karindex vallen we terug op de karren in deze lijst.
85
+ // Lichte ordergegevens uit dezelfde twee batchqueries.
86
+ // De Engine bewaart het gekozen ophaalmoment per order als
87
+ // "datum|van|tot" in custom_fields.pickup_moment (zelfde bron als het
88
+ // verzet-dialoog). Alleen overnemen; nooit zelf een tijd verzinnen.
89
+ const parsePickupMoment = (raw) => {
90
+ const parts = String(raw ?? "").split("|");
91
+ const date = (parts[0] ?? "").trim();
92
+ const time = (parts[1] ?? "").trim().slice(0, 5);
93
+ return { date: date || null, time: time || null };
94
+ };
95
+ const orderMap = new Map();
96
+ for (const o of matchedOrders) {
97
+ const moment = parsePickupMoment(o.custom_fields?.["pickup_moment"]);
98
+ const value = {
99
+ orderNumber: o.order_number ?? "",
100
+ customerName: o.customer_name ?? "",
101
+ deliveryDate: o.delivery_date ?? null,
102
+ pickupDate: moment.date,
103
+ pickupTime: moment.time,
104
+ };
105
+ orderMap.set(o.id, value);
106
+ if (o.order_number)
107
+ orderMap.set(o.order_number, value);
130
108
  }
109
+ // De RPC is de Engine-bron voor de zichtbare karren. Het totaal wordt alleen
110
+ // uit die teruggegeven, stabiele karregels afgeleid; er volgt geen tweede
111
+ // zware planning- of Pickstation-opbouw voordat de lijst zichtbaar wordt.
131
112
  const fallbackTotals = new Map();
132
113
  for (const r of rows) {
133
114
  const cur = fallbackTotals.get(r.order_id) ?? 0;
134
115
  fallbackTotals.set(r.order_id, Math.max(cur, r.cart_index));
135
116
  }
117
+ // Oorspronkelijke plandag: de Engine schuift niet-afgeronde taken elke dag
118
+ // mee naar vandaag. De vroegst geplande dag per order maakt achterstand
119
+ // zichtbaar. Mislukt dit, dan blijft de kar gewoon zichtbaar.
120
+ const originalByRef = new Map();
121
+ const refs = Array.from(new Set(rows.map((r) => r.order_number).filter((v) => !!v)));
122
+ if (refs.length > 0) {
123
+ const { data: taskRows, error: taskError } = await supabase
124
+ .from("logistics_tasks")
125
+ .select("order_ref, planned_start")
126
+ .in("order_ref", refs)
127
+ .limit(6000);
128
+ if (taskError) {
129
+ warnings.push(`Oorspronkelijke plandatum opzoeken: ${taskError.message}`);
130
+ }
131
+ else {
132
+ for (const t of (taskRows ?? [])) {
133
+ if (!t.order_ref || !t.planned_start)
134
+ continue;
135
+ const day = String(t.planned_start).slice(0, 10);
136
+ const cur = originalByRef.get(t.order_ref);
137
+ if (!cur || day < cur)
138
+ originalByRef.set(t.order_ref, day);
139
+ }
140
+ }
141
+ }
136
142
  const carts = [];
137
143
  const seen = new Set();
138
144
  for (const r of rows) {
@@ -147,48 +153,56 @@ async function loadControlCarts(supabase) {
147
153
  if (seen.has(key))
148
154
  continue;
149
155
  seen.add(key);
150
- const order = orderMap.get(r.order_id);
151
- const extra = extras.get(`${r.order_id}#${r.cart_index}`) ??
152
- extras.get(`${r.order_id}#${r.cart_index - 1}`);
156
+ const order = orderMap.get(r.order_id) ?? (r.order_number ? orderMap.get(r.order_number) : undefined);
153
157
  const orderNumber = r.order_number ?? order?.orderNumber ?? "";
154
- const orderNumbers = Array.from(new Set([orderNumber, ...(extra?.orderNumbers ?? [])].filter(Boolean)));
155
- const cartTotal = totalsByOrder.get(r.order_id) ??
156
- (orderNumber ? totalsByOrder.get(orderNumber) : undefined) ??
157
- fallbackTotals.get(r.order_id) ??
158
- null;
158
+ const orderNumbers = orderNumber ? [orderNumber] : [];
159
+ // De logistiek-engine levert de karnummering mee zodra die kolommen
160
+ // bestaan; anders blijft de index de bron.
161
+ const cartSeq = r.cart_seq ?? r.cart_index;
162
+ const cartTotal = r.cart_total ?? fallbackTotals.get(r.order_id) ?? null;
163
+ const originalPickDate = orderNumber ? originalByRef.get(orderNumber) ?? null : null;
159
164
  carts.push({
160
165
  cartNumberId,
161
- cartNumber: r.cart_number ?? extra?.cartNumber ?? null,
166
+ cartNumber: r.cart_number ?? null,
162
167
  orderId: r.order_id,
163
168
  orderSource,
164
169
  cartIndex: r.cart_index,
165
- cartSeq: r.cart_index,
170
+ cartSeq,
166
171
  cartTotal,
172
+ originalPickDate,
167
173
  orderNumber,
168
174
  orderNumbers,
169
175
  customerName: order?.customerName ?? "",
170
- carrierName: extra?.carrierName ?? null,
176
+ carrierName: null,
171
177
  deliveryDate: order?.deliveryDate ?? null,
172
- pickupDate: extra?.pickupDate ?? null,
173
- pickupTime: extra?.pickupTime ?? null,
174
- country: extra?.country ?? null,
175
- cratesOnCart: extra?.cratesOnCart ?? null,
178
+ pickupDate: order?.pickupDate ?? null,
179
+ pickupTime: order?.pickupTime ?? null,
180
+ country: null,
181
+ cratesOnCart: null,
176
182
  pickDate: r.pick_date,
177
- printedCount: r.printed_today ? Math.max(1, extra?.printedCount ?? 1) : 0,
183
+ printedCount: r.printed_today ? 1 : 0,
178
184
  printedToday: Boolean(r.printed_today),
179
185
  printedAt: r.printed_at ?? null,
180
186
  status: r.status ?? "pending",
181
187
  });
182
188
  }
183
- return carts;
189
+ return {
190
+ carts,
191
+ rpcCount: allRows.length,
192
+ filteredOut: allRows.length - carts.length,
193
+ warnings,
194
+ };
195
+ }
196
+ /** Alleen de karren, voor paden die geen diagnose nodig hebben. */
197
+ async function loadControlCarts(supabase) {
198
+ return (await loadControlCartsDetailed(supabase)).carts;
184
199
  }
185
200
  /** Haal alle karren op die vandaag gecontroleerd moeten worden (RPC is leidend). */
186
201
  export const getTodaysControlCarts = createServerFn({ method: "GET" })
187
202
  .middleware([requireSupabaseAuth])
188
203
  .handler(async ({ context }) => {
189
204
  const supabase = context.supabase;
190
- const carts = await loadControlCarts(supabase);
191
- return { carts };
205
+ return await loadControlCartsDetailed(supabase);
192
206
  });
193
207
  export const getControlRescheduleProposals = createServerFn({ method: "POST" })
194
208
  .middleware([requireSupabaseAuth])
@@ -252,8 +266,29 @@ export const submitControlSession = createServerFn({ method: "POST" })
252
266
  const supabase = context.supabase;
253
267
  // 1) Laad exact dezelfde karren als het scherm toont (RPC is leidend)
254
268
  const pendingOrders = await loadControlCarts(supabase);
255
- const { pickWeekdays, minWorkdays } = await loadControlPlanningSettings(supabase, context.userId);
256
- // 2) Maak sessie aan
269
+ const { pickWeekdays, minWorkdays, maxBulkCarts } = await loadControlPlanningSettings(supabase, context.userId);
270
+ // 2) Maak sessie aan. Het aanvraag-kenmerk maakt opnieuw indienen veilig:
271
+ // dezelfde controle levert nooit twee sessies of dubbele regels op.
272
+ const requestKey = data.requestKey?.trim() || null;
273
+ if (requestKey) {
274
+ const { data: existing } = await supabase
275
+ .from("cart_control_sessions")
276
+ .select("id, checked_count, rescheduled_count, blocked_count")
277
+ .eq("user_id", context.userId)
278
+ .eq("request_key", requestKey)
279
+ .maybeSingle();
280
+ if (existing) {
281
+ return {
282
+ sessionId: existing.id,
283
+ checkedCount: existing.checked_count ?? 0,
284
+ rescheduledCount: existing.rescheduled_count ?? 0,
285
+ blockedCount: existing.blocked_count ?? 0,
286
+ rescheduled: [],
287
+ blocked: [],
288
+ stillOpen: [],
289
+ };
290
+ }
291
+ }
257
292
  const { data: session, error: sessErr } = await supabase
258
293
  .from("cart_control_sessions")
259
294
  .insert({
@@ -261,6 +296,7 @@ export const submitControlSession = createServerFn({ method: "POST" })
261
296
  control_date: toIso(new Date()),
262
297
  total_carts: pendingOrders.length,
263
298
  notes: data.notes ?? null,
299
+ request_key: requestKey,
264
300
  })
265
301
  .select()
266
302
  .single();
@@ -270,165 +306,259 @@ export const submitControlSession = createServerFn({ method: "POST" })
270
306
  const confirmedRescheduleSet = new Set(data.confirmedRescheduleCartKeys ?? []);
271
307
  const items = [];
272
308
  const deliveredPayload = [];
309
+ const overrides = [];
310
+ const checkedByKey = new Map();
273
311
  const rescheduled = [];
274
312
  const blocked = [];
313
+ const stillOpen = [];
275
314
  let checkedCount = 0;
276
- for (const o of pendingOrders) {
277
- const key = controlCartKey(o);
278
- const cartNumberId = o.cartNumberId;
279
- const baseFields = {
280
- session_id: session.id,
281
- cart_number_id: cartNumberId,
282
- cart_number_label: o.cartNumber,
283
- order_id: o.orderId,
284
- order_source: o.orderSource,
285
- cart_index: o.cartIndex,
286
- order_number: o.orderNumber,
287
- customer_name: o.customerName,
288
- carrier_name: o.carrierName,
289
- delivery_date: o.deliveryDate,
290
- pickup_date: o.pickupDate,
291
- };
292
- if (checkedSet.has(key)) {
293
- // De controleregel is leidend voor Engine-karren. Werk daarnaast de
294
- // verouderde cart_numbers-status bij wanneer die bron dit ondersteunt.
295
- deliveredPayload.push({
315
+ try {
316
+ for (const o of pendingOrders) {
317
+ const key = controlCartKey(o);
318
+ const cartNumberId = o.cartNumberId;
319
+ const baseFields = {
320
+ session_id: session.id,
321
+ cart_number_id: cartNumberId,
322
+ cart_number_label: o.cartNumber,
296
323
  order_id: o.orderId,
297
324
  order_source: o.orderSource,
298
325
  cart_index: o.cartIndex,
299
- cart_number: o.cartNumber,
300
- pick_date: o.pickDate || toIso(new Date()),
326
+ order_number: o.orderNumber,
327
+ customer_name: o.customerName,
328
+ carrier_name: o.carrierName,
329
+ delivery_date: o.deliveryDate,
330
+ pickup_date: o.pickupDate,
331
+ };
332
+ if (checkedSet.has(key)) {
333
+ // De controleregel is leidend voor Engine-karren. Werk daarnaast de
334
+ // verouderde cart_numbers-status bij wanneer die bron dit ondersteunt.
335
+ deliveredPayload.push({
336
+ order_id: o.orderId,
337
+ order_source: o.orderSource,
338
+ cart_index: o.cartIndex,
339
+ // Het label uit de Engine is géén karnummer: dat kent
340
+ // assign_cart_numbers toe (enige nummeringspad, onder advisory lock).
341
+ engine_cart_label: o.cartNumber,
342
+ pick_date: o.pickDate || toIso(new Date()),
343
+ });
344
+ checkedByKey.set(`${o.orderSource}#${o.orderId}#${o.cartIndex}`, o);
345
+ items.push({ ...baseFields, action: "checked" });
346
+ checkedCount++;
347
+ continue;
348
+ }
349
+ // Niet aangevinkt → alleen verplaatsen wanneer de gebruiker dit expliciet
350
+ // in de bevestigings-pop-up heeft gekozen. Zonder bevestiging blijft de
351
+ // kar op de huidige pickdatum staan en registreren we uitsluitend een
352
+ // blokkade/waarschuwing.
353
+ const result = tryReschedule({
354
+ currentPickDate: new Date(o.pickDate || toIso(new Date())),
355
+ deliveryDate: o.deliveryDate ? new Date(o.deliveryDate) : null,
356
+ pickWeekdays,
357
+ minWorkdaysBeforeDelivery: minWorkdays,
301
358
  });
302
- items.push({ ...baseFields, action: "checked" });
303
- checkedCount++;
304
- continue;
359
+ if (result.ok && confirmedRescheduleSet.has(key)) {
360
+ const newIso = toIso(result.newPickDate);
361
+ overrides.push({
362
+ order_id: o.orderId,
363
+ order_source: o.orderSource,
364
+ cart_index: o.cartIndex,
365
+ pick_date: newIso,
366
+ moved_from_date: toIso(new Date()),
367
+ });
368
+ items.push({
369
+ ...baseFields,
370
+ action: "rescheduled",
371
+ new_pick_date: newIso,
372
+ });
373
+ rescheduled.push({
374
+ key,
375
+ cartNumber: o.cartNumber,
376
+ orderNumber: o.orderNumber,
377
+ customerName: o.customerName,
378
+ newPickDate: newIso,
379
+ });
380
+ }
381
+ else {
382
+ const blockReason = result.ok
383
+ ? `Verplaatsing naar ${toIso(result.newPickDate)} is voorgesteld, maar niet bevestigd. De kar blijft op de huidige pickdatum staan.`
384
+ : result.reason;
385
+ items.push({
386
+ ...baseFields,
387
+ action: "blocked",
388
+ block_reason: blockReason,
389
+ });
390
+ blocked.push({
391
+ cartNumberId,
392
+ cartNumber: o.cartNumber,
393
+ orderId: o.orderId,
394
+ orderSource: o.orderSource,
395
+ cartIndex: o.cartIndex,
396
+ orderNumber: o.orderNumber,
397
+ customerName: o.customerName,
398
+ carrierName: o.carrierName,
399
+ deliveryDate: o.deliveryDate,
400
+ pickupDate: o.pickupDate,
401
+ reason: blockReason,
402
+ });
403
+ }
305
404
  }
306
- // Niet aangevinkt → alleen verplaatsen wanneer de gebruiker dit expliciet
307
- // in de bevestigings-pop-up heeft gekozen. Zonder bevestiging blijft de
308
- // kar op de huidige pickdatum staan en registreren we uitsluitend een
309
- // blokkade/waarschuwing.
310
- const result = tryReschedule({
311
- currentPickDate: new Date(o.pickDate || toIso(new Date())),
312
- deliveryDate: o.deliveryDate ? new Date(o.deliveryDate) : null,
313
- pickWeekdays,
314
- minWorkdaysBeforeDelivery: minWorkdays,
315
- });
316
- if (result.ok && confirmedRescheduleSet.has(key)) {
317
- const newIso = toIso(result.newPickDate);
318
- await supabase
405
+ // Verplaatsingen van niet-afgevinkte karren in één keer wegschrijven.
406
+ if (overrides.length > 0) {
407
+ const { error: overrideError } = await supabase
319
408
  .from("cart_pick_overrides")
409
+ .upsert(overrides, { onConflict: "order_id,order_source,cart_index" });
410
+ if (overrideError)
411
+ throw new Error(overrideError.message);
412
+ }
413
+ // Afgevinkte karren krijgen de status 'afgeleverd'. Eén begrensde
414
+ // bulk-aanroep, daarna één gezamenlijke verificatie en één gerichte naloop
415
+ // over uitsluitend de karren waarvan geverifieerd is dat ze nog openstaan.
416
+ // De naloop draait maximaal één keer; wat daarna nog openstaat wordt per kar
417
+ // gemeld en verdwijnt dus nooit stil.
418
+ const chunk = (rows, size) => {
419
+ const out = [];
420
+ for (let i = 0; i < rows.length; i += size)
421
+ out.push(rows.slice(i, i + size));
422
+ return out;
423
+ };
424
+ const payloadKey = (cart) => `${String(cart.order_source ?? "")}#${String(cart.order_id ?? "")}#${Number(cart.cart_index ?? 0)}`;
425
+ const verifyOpen = async (carts) => {
426
+ const orderIds = Array.from(new Set(carts.map((cart) => String(cart.order_id ?? ""))));
427
+ const rows = [];
428
+ for (const idBatch of chunk(orderIds, 200)) {
429
+ const { data: deliveredRows, error: verifyError } = await supabase
430
+ .from("cart_numbers")
431
+ .select("order_id, order_source, cart_index, status, delivered_at")
432
+ .in("order_id", idBatch);
433
+ if (verifyError)
434
+ throw new Error(`Afleverstatus controleren: ${verifyError.message}`);
435
+ rows.push(...(deliveredRows ?? []));
436
+ }
437
+ const deliveredKeys = new Set(rows
438
+ .filter((row) => row.status === "delivered" && row.delivered_at)
439
+ .map((row) => payloadKey(row)));
440
+ return carts.filter((cart) => !deliveredKeys.has(payloadKey(cart)));
441
+ };
442
+ let deliverError = null;
443
+ const runBulk = async (carts) => {
444
+ for (const batch of chunk(carts, maxBulkCarts)) {
445
+ const { error } = await supabase.rpc("control_mark_carts_delivered", { p_carts: batch });
446
+ if (error && !deliverError)
447
+ deliverError = error.message;
448
+ }
449
+ };
450
+ let openCarts = [];
451
+ if (deliveredPayload.length > 0) {
452
+ await runBulk(deliveredPayload);
453
+ openCarts = await verifyOpen(deliveredPayload);
454
+ if (openCarts.length > 0) {
455
+ await runBulk(openCarts);
456
+ openCarts = await verifyOpen(openCarts);
457
+ }
458
+ }
459
+ // Wat nog openstaat blijft zichtbaar: de kar wordt in deze sessie als
460
+ // 'nog niet afgemeld' vastgelegd en in het scherm benoemd.
461
+ const openKeys = new Set(openCarts.map(payloadKey));
462
+ if (openKeys.size > 0) {
463
+ const openReason = `Deze kar is nog niet afgemeld door de logistiek-engine${deliverError ? ` (${deliverError})` : ""}. De kar blijft in de lijst staan en wordt bij een volgende controle opnieuw aangeboden.`;
464
+ for (const item of items) {
465
+ const key = `${String(item.order_source ?? "")}#${String(item.order_id ?? "")}#${Number(item.cart_index ?? 0)}`;
466
+ if (item.action !== "checked" || !openKeys.has(key))
467
+ continue;
468
+ item.action = "blocked";
469
+ item.block_reason = openReason;
470
+ checkedCount--;
471
+ const cart = checkedByKey.get(key);
472
+ if (cart) {
473
+ const entry = {
474
+ cartNumberId: cart.cartNumberId,
475
+ cartNumber: cart.cartNumber,
476
+ orderId: cart.orderId,
477
+ orderSource: cart.orderSource,
478
+ cartIndex: cart.cartIndex,
479
+ orderNumber: cart.orderNumber,
480
+ customerName: cart.customerName,
481
+ carrierName: cart.carrierName,
482
+ deliveryDate: cart.deliveryDate,
483
+ pickupDate: cart.pickupDate,
484
+ reason: openReason,
485
+ };
486
+ blocked.push(entry);
487
+ stillOpen.push(entry);
488
+ }
489
+ }
490
+ }
491
+ const ITEM_BATCH = 200;
492
+ for (let i = 0; i < items.length; i += ITEM_BATCH) {
493
+ const { error: itemsError } = await supabase
494
+ .from("cart_control_items")
495
+ .insert(items.slice(i, i + ITEM_BATCH));
496
+ if (itemsError)
497
+ throw new Error(itemsError.message);
498
+ }
499
+ // Update sessie-aantallen
500
+ const { error: sessionUpdateError } = await supabase
501
+ .from("cart_control_sessions")
502
+ .update({
503
+ checked_count: checkedCount,
504
+ rescheduled_count: rescheduled.length,
505
+ blocked_count: blocked.length,
506
+ })
507
+ .eq("id", session.id);
508
+ if (sessionUpdateError)
509
+ throw new Error(sessionUpdateError.message);
510
+ // Kritieke melding aanmaken of oplossen
511
+ const alertKey = `cart_not_rescheduled:${toIso(new Date())}`;
512
+ if (blocked.length > 0) {
513
+ const relatedIds = blocked
514
+ .map((b) => b.cartNumberId)
515
+ .filter((v) => !!v);
516
+ await supabase
517
+ .from("dashboard_critical_alerts")
320
518
  .upsert({
321
- order_id: o.orderId,
322
- order_source: o.orderSource,
323
- cart_index: o.cartIndex,
324
- pick_date: newIso,
325
- moved_from_date: toIso(new Date()),
326
- }, { onConflict: "order_id,order_source,cart_index" });
327
- items.push({
328
- ...baseFields,
329
- action: "rescheduled",
330
- new_pick_date: newIso,
331
- });
332
- rescheduled.push({
333
- key,
334
- cartNumber: o.cartNumber,
335
- orderNumber: o.orderNumber,
336
- customerName: o.customerName,
337
- newPickDate: newIso,
338
- });
519
+ user_id: context.userId,
520
+ alert_key: alertKey,
521
+ alert_type: "cart_not_rescheduled",
522
+ priority: "critical",
523
+ title: "Karren kunnen niet doorgeschoven worden",
524
+ description: `Er zijn ${blocked.length} karren die vandaag niet zijn klaargezet en niet automatisch naar een volgende pickdag kunnen worden verplaatst. Directe actie is vereist.`,
525
+ related_cart_ids: relatedIds,
526
+ resolved_at: null,
527
+ metadata: { blocked: blocked },
528
+ }, { onConflict: "user_id,alert_key" });
339
529
  }
340
530
  else {
341
- const blockReason = result.ok
342
- ? `Verplaatsing naar ${toIso(result.newPickDate)} is voorgesteld, maar niet bevestigd. De kar blijft op de huidige pickdatum staan.`
343
- : result.reason;
344
- items.push({
345
- ...baseFields,
346
- action: "blocked",
347
- block_reason: blockReason,
348
- });
349
- blocked.push({
350
- cartNumberId,
351
- cartNumber: o.cartNumber,
352
- orderId: o.orderId,
353
- orderSource: o.orderSource,
354
- cartIndex: o.cartIndex,
355
- orderNumber: o.orderNumber,
356
- customerName: o.customerName,
357
- carrierName: o.carrierName,
358
- deliveryDate: o.deliveryDate,
359
- pickupDate: o.pickupDate,
360
- reason: blockReason,
361
- });
531
+ // Auto-resolve als er geen blokkades zijn
532
+ await supabase
533
+ .from("dashboard_critical_alerts")
534
+ .update({ resolved_at: new Date().toISOString() })
535
+ .eq("user_id", context.userId)
536
+ .eq("alert_key", alertKey)
537
+ .is("resolved_at", null);
362
538
  }
363
- }
364
- // Afgevinkte karren krijgen de status 'afgeleverd' met het moment van controle,
365
- // zodat andere modules alleen de karren van vandaag als afgeleverd tonen.
366
- if (deliveredPayload.length > 0) {
367
- const { error: deliveredError } = await supabase.rpc("control_mark_carts_delivered", {
368
- p_carts: deliveredPayload,
369
- });
370
- if (deliveredError)
371
- throw new Error(deliveredError.message);
372
- }
373
- if (items.length > 0) {
374
- const { error: itemsError } = await supabase.from("cart_control_items").insert(items);
375
- if (itemsError)
376
- throw new Error(itemsError.message);
377
- }
378
- // Update sessie-aantallen
379
- const { error: sessionUpdateError } = await supabase
380
- .from("cart_control_sessions")
381
- .update({
382
- checked_count: checkedCount,
383
- rescheduled_count: rescheduled.length,
384
- blocked_count: blocked.length,
385
- })
386
- .eq("id", session.id);
387
- if (sessionUpdateError)
388
- throw new Error(sessionUpdateError.message);
389
- // Kritieke melding aanmaken of oplossen
390
- const alertKey = `cart_not_rescheduled:${toIso(new Date())}`;
391
- if (blocked.length > 0) {
392
- const relatedIds = blocked
393
- .map((b) => b.cartNumberId)
394
- .filter((v) => !!v);
539
+ // Definitieve controle: wis alleen het autosave-concept van deze gebruiker voor vandaag.
395
540
  await supabase
396
- .from("dashboard_critical_alerts")
397
- .upsert({
398
- user_id: context.userId,
399
- alert_key: alertKey,
400
- alert_type: "cart_not_rescheduled",
401
- priority: "critical",
402
- title: "Karren kunnen niet doorgeschoven worden",
403
- description: `Er zijn ${blocked.length} karren die vandaag niet zijn klaargezet en niet automatisch naar een volgende pickdag kunnen worden verplaatst. Directe actie is vereist.`,
404
- related_cart_ids: relatedIds,
405
- resolved_at: null,
406
- metadata: { blocked: blocked },
407
- }, { onConflict: "user_id,alert_key" });
408
- }
409
- else {
410
- // Auto-resolve als er geen blokkades zijn
411
- await supabase
412
- .from("dashboard_critical_alerts")
413
- .update({ resolved_at: new Date().toISOString() })
541
+ .from("cart_control_draft")
542
+ .delete()
414
543
  .eq("user_id", context.userId)
415
- .eq("alert_key", alertKey)
416
- .is("resolved_at", null);
544
+ .eq("draft_date", toIso(new Date()));
545
+ return {
546
+ sessionId: session.id,
547
+ checkedCount,
548
+ rescheduledCount: rescheduled.length,
549
+ blockedCount: blocked.length,
550
+ rescheduled,
551
+ blocked,
552
+ stillOpen,
553
+ };
554
+ }
555
+ catch (error) {
556
+ // Een niet-afgeronde poging hoort niet als lege controlesessie in de
557
+ // historie te blijven staan. De al afgeleverde karren blijven veilig
558
+ // afgeleverd; een volgende poging behandelt alleen wat nog openstaat.
559
+ await supabase.from("cart_control_sessions").delete().eq("id", session.id);
560
+ throw error;
417
561
  }
418
- // Definitieve controle: wis alleen het autosave-concept van deze gebruiker voor vandaag.
419
- await supabase
420
- .from("cart_control_draft")
421
- .delete()
422
- .eq("user_id", context.userId)
423
- .eq("draft_date", toIso(new Date()));
424
- return {
425
- sessionId: session.id,
426
- checkedCount,
427
- rescheduledCount: rescheduled.length,
428
- blockedCount: blocked.length,
429
- rescheduled,
430
- blocked,
431
- };
432
562
  });
433
563
  export const listControlHistory = createServerFn({ method: "GET" })
434
564
  .middleware([requireSupabaseAuth])