@flowselections/floriday-afhandeling-module 1.0.10 → 1.0.12

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 (69) hide show
  1. package/dist-lib/components/afhandeling/ControleLijstPrint.d.ts +7 -0
  2. package/dist-lib/components/afhandeling/ControleLijstPrint.d.ts.map +1 -0
  3. package/dist-lib/components/afhandeling/ControleLijstPrint.js +67 -0
  4. package/dist-lib/components/afhandeling/ControlePage.d.ts +4 -0
  5. package/dist-lib/components/afhandeling/ControlePage.d.ts.map +1 -0
  6. package/dist-lib/components/afhandeling/ControlePage.js +460 -0
  7. package/dist-lib/components/afhandeling/DagDetailDialog.d.ts.map +1 -1
  8. package/dist-lib/components/afhandeling/DagDetailDialog.js +9 -5
  9. package/dist-lib/components/afhandeling/VervoerderForm.d.ts.map +1 -1
  10. package/dist-lib/components/afhandeling/VervoerderForm.js +10 -1
  11. package/dist-lib/components/afhandeling/VervoerdersPage.d.ts.map +1 -1
  12. package/dist-lib/components/afhandeling/VervoerdersPage.js +3 -2
  13. package/dist-lib/components/afhandeling/WeekAgendaView.d.ts.map +1 -1
  14. package/dist-lib/components/afhandeling/WeekAgendaView.js +15 -5
  15. package/dist-lib/components/afhandeling/WeekdayPicker.d.ts +12 -0
  16. package/dist-lib/components/afhandeling/WeekdayPicker.d.ts.map +1 -0
  17. package/dist-lib/components/afhandeling/WeekdayPicker.js +30 -0
  18. package/dist-lib/components/pickstation/PakbonTemplate.d.ts +1 -0
  19. package/dist-lib/components/pickstation/PakbonTemplate.d.ts.map +1 -1
  20. package/dist-lib/components/pickstation/PakbonTemplate.js +31 -8
  21. package/dist-lib/components/settings/AfhandelingSettingsCard.d.ts.map +1 -1
  22. package/dist-lib/components/settings/AfhandelingSettingsCard.js +9 -2
  23. package/dist-lib/components/ui/responsive-dialog.d.ts +20 -0
  24. package/dist-lib/components/ui/responsive-dialog.d.ts.map +1 -0
  25. package/dist-lib/components/ui/responsive-dialog.js +44 -0
  26. package/dist-lib/hooks/useOrderPlanning.d.ts.map +1 -1
  27. package/dist-lib/hooks/useOrderPlanning.js +17 -6
  28. package/dist-lib/index.d.ts +1 -0
  29. package/dist-lib/index.d.ts.map +1 -1
  30. package/dist-lib/index.js +4 -2
  31. package/dist-lib/integrations/supabase/auth-middleware.d.ts +755 -3
  32. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  33. package/dist-lib/integrations/supabase/client.d.ts +755 -3
  34. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  35. package/dist-lib/integrations/supabase/client.server.d.ts +755 -3
  36. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  37. package/dist-lib/integrations/supabase/types.d.ts +765 -3
  38. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  39. package/dist-lib/lib/afhandelingSettings.functions.d.ts +1565 -57
  40. package/dist-lib/lib/afhandelingSettings.functions.d.ts.map +1 -1
  41. package/dist-lib/lib/afhandelingSettings.functions.js +16 -1
  42. package/dist-lib/lib/carrierMatch.server.d.ts +27 -0
  43. package/dist-lib/lib/carrierMatch.server.d.ts.map +1 -0
  44. package/dist-lib/lib/carrierMatch.server.js +75 -0
  45. package/dist-lib/lib/carriers.functions.d.ts +3781 -15
  46. package/dist-lib/lib/carriers.functions.d.ts.map +1 -1
  47. package/dist-lib/lib/carriers.functions.js +17 -2
  48. package/dist-lib/lib/cartAssignments.functions.d.ts +3775 -15
  49. package/dist-lib/lib/cartAssignments.functions.d.ts.map +1 -1
  50. package/dist-lib/lib/cartControl.functions.d.ts +100519 -0
  51. package/dist-lib/lib/cartControl.functions.d.ts.map +1 -0
  52. package/dist-lib/lib/cartControl.functions.js +489 -0
  53. package/dist-lib/lib/cartRescheduling.d.ts +30 -0
  54. package/dist-lib/lib/cartRescheduling.d.ts.map +1 -0
  55. package/dist-lib/lib/cartRescheduling.js +71 -0
  56. package/dist-lib/lib/floriday.functions.d.ts +755 -3
  57. package/dist-lib/lib/floriday.functions.d.ts.map +1 -1
  58. package/dist-lib/lib/pickstationI18n.d.ts +8 -0
  59. package/dist-lib/lib/pickstationI18n.d.ts.map +1 -1
  60. package/dist-lib/lib/pickstationI18n.js +48 -0
  61. package/dist-lib/lib/pickstationOrders.server.d.ts +1 -0
  62. package/dist-lib/lib/pickstationOrders.server.d.ts.map +1 -1
  63. package/dist-lib/lib/pickstationOrders.server.js +94 -7
  64. package/dist-lib/lib/pickstationTypes.d.ts +12 -0
  65. package/dist-lib/lib/pickstationTypes.d.ts.map +1 -1
  66. package/dist-lib/lib/planning.d.ts +23 -0
  67. package/dist-lib/lib/planning.d.ts.map +1 -1
  68. package/dist-lib/lib/planning.js +66 -6
  69. package/package.json +6 -1
@@ -0,0 +1,489 @@
1
+ import { createServerFn } from "@tanstack/react-start";
2
+ import { requireSupabaseAuth } from "../integrations/supabase/auth-middleware";
3
+ import { tryReschedule } from "./cartRescheduling";
4
+ function toIso(d) {
5
+ const y = d.getFullYear();
6
+ const m = String(d.getMonth() + 1).padStart(2, "0");
7
+ const day = String(d.getDate()).padStart(2, "0");
8
+ return `${y}-${m}-${day}`;
9
+ }
10
+ const APPROVED_STATUSES = new Set(["ready", "shipped", "delivered"]);
11
+ async function loadControlPlanningSettings(supabase, userId) {
12
+ const { data: settings } = await supabase
13
+ .from("afhandeling_settings")
14
+ .select("pick_weekdays, pick_days_before_transport")
15
+ .eq("user_id", userId)
16
+ .maybeSingle();
17
+ const pickWeekdays = Array.isArray(settings?.pick_weekdays) && settings.pick_weekdays.length > 0
18
+ ? settings.pick_weekdays
19
+ : [1, 2, 3, 4, 5];
20
+ const minWorkdays = Math.max(1, Number(settings?.pick_days_before_transport ?? 1));
21
+ return { pickWeekdays, minWorkdays };
22
+ }
23
+ /** Haal alle karren op die vandaag gepickt / klaargezet hadden moeten worden. */
24
+ export const getTodaysControlCarts = createServerFn({ method: "GET" })
25
+ .middleware([requireSupabaseAuth])
26
+ .handler(async () => {
27
+ const { loadTodaysPickstationOrders } = await import("./pickstationOrders.server");
28
+ const orders = await loadTodaysPickstationOrders();
29
+ const carts = orders
30
+ .filter((o) => !APPROVED_STATUSES.has(o.status))
31
+ .map((o) => ({
32
+ cartNumberId: null, // will be resolved below via cart_numbers lookup
33
+ cartNumber: o.cartNumber,
34
+ orderId: o.orderId,
35
+ orderSource: o.orderSource,
36
+ cartIndex: o.cartIndex - 1, // pickstation uses 1-based, DB is 0-based
37
+ orderNumber: o.orderNumber,
38
+ customerName: o.customerName,
39
+ carrierName: o.carrierName,
40
+ deliveryDate: o.deliveryDate,
41
+ pickupDate: o.pickupDate,
42
+ pickDate: o.pickDate,
43
+ printedCount: o.printedCount,
44
+ status: o.status,
45
+ }));
46
+ return { carts };
47
+ });
48
+ export const getControlRescheduleProposals = createServerFn({ method: "POST" })
49
+ .middleware([requireSupabaseAuth])
50
+ .inputValidator((d) => d)
51
+ .handler(async ({ data, context }) => {
52
+ const supabase = context.supabase;
53
+ const { loadTodaysPickstationOrders } = await import("./pickstationOrders.server");
54
+ const orders = await loadTodaysPickstationOrders();
55
+ const pendingOrders = orders.filter((o) => !APPROVED_STATUSES.has(o.status));
56
+ const { pickWeekdays, minWorkdays } = await loadControlPlanningSettings(supabase, context.userId);
57
+ const checkedSet = new Set(data.checkedCartKeys);
58
+ const proposals = [];
59
+ const blocked = [];
60
+ for (const o of pendingOrders) {
61
+ const key = `${o.orderSource}#${o.orderId}#${o.cartIndex - 1}`;
62
+ if (checkedSet.has(key))
63
+ continue;
64
+ const currentIso = o.pickDate || toIso(new Date());
65
+ const result = tryReschedule({
66
+ currentPickDate: new Date(currentIso),
67
+ deliveryDate: o.deliveryDate ? new Date(o.deliveryDate) : null,
68
+ pickWeekdays,
69
+ minWorkdaysBeforeDelivery: minWorkdays,
70
+ });
71
+ if (result.ok) {
72
+ const proposedPickDate = toIso(result.newPickDate);
73
+ proposals.push({
74
+ key,
75
+ cartNumber: o.cartNumber,
76
+ orderNumber: o.orderNumber,
77
+ customerName: o.customerName,
78
+ carrierName: o.carrierName,
79
+ deliveryDate: o.deliveryDate,
80
+ pickupDate: o.pickupDate,
81
+ currentPickDate: currentIso,
82
+ proposedPickDate,
83
+ reason: "Deze kar is niet afgevinkt tijdens de controle en staat nog op de picklijst van vandaag.",
84
+ impact: `De kar verdwijnt uit de pickagenda van ${currentIso} en komt op ${proposedPickDate}. Controleer of ophalen en levering op ${o.deliveryDate ?? "de leverdatum"} hierdoor haalbaar blijven.`,
85
+ });
86
+ }
87
+ else {
88
+ blocked.push({
89
+ cartNumberId: null,
90
+ cartNumber: o.cartNumber,
91
+ orderId: o.orderId,
92
+ orderSource: o.orderSource,
93
+ cartIndex: o.cartIndex - 1,
94
+ orderNumber: o.orderNumber,
95
+ customerName: o.customerName,
96
+ carrierName: o.carrierName,
97
+ deliveryDate: o.deliveryDate,
98
+ pickupDate: o.pickupDate,
99
+ reason: result.reason,
100
+ });
101
+ }
102
+ }
103
+ return { proposals, blocked };
104
+ });
105
+ export const submitControlSession = createServerFn({ method: "POST" })
106
+ .middleware([requireSupabaseAuth])
107
+ .inputValidator((d) => d)
108
+ .handler(async ({ data, context }) => {
109
+ const supabase = context.supabase;
110
+ // 1) Laad alle karren van vandaag + instellingen
111
+ const { loadTodaysPickstationOrders } = await import("./pickstationOrders.server");
112
+ const orders = await loadTodaysPickstationOrders();
113
+ const pendingOrders = orders.filter((o) => !APPROVED_STATUSES.has(o.status));
114
+ const { pickWeekdays, minWorkdays } = await loadControlPlanningSettings(supabase, context.userId);
115
+ // 2) Maak sessie aan
116
+ const { data: session, error: sessErr } = await supabase
117
+ .from("cart_control_sessions")
118
+ .insert({
119
+ user_id: context.userId,
120
+ control_date: toIso(new Date()),
121
+ total_carts: pendingOrders.length,
122
+ notes: data.notes ?? null,
123
+ })
124
+ .select()
125
+ .single();
126
+ if (sessErr)
127
+ throw new Error(sessErr.message);
128
+ const checkedSet = new Set(data.checkedCartKeys);
129
+ const confirmedRescheduleSet = new Set(data.confirmedRescheduleCartKeys ?? []);
130
+ const items = [];
131
+ const rescheduled = [];
132
+ const blocked = [];
133
+ let checkedCount = 0;
134
+ // Lookup cart_numbers row ids
135
+ const { data: cnRows } = await supabase
136
+ .from("cart_numbers")
137
+ .select("id, order_source, order_id, cart_index")
138
+ .in("order_id", Array.from(new Set(pendingOrders.map((o) => o.orderId))));
139
+ const cnMap = new Map();
140
+ for (const r of (cnRows ?? [])) {
141
+ cnMap.set(`${r.order_source}#${r.order_id}#${r.cart_index}`, r.id);
142
+ }
143
+ for (const o of pendingOrders) {
144
+ const key = `${o.orderSource}#${o.orderId}#${o.cartIndex - 1}`;
145
+ const cartNumberId = cnMap.get(key) ?? null;
146
+ const baseFields = {
147
+ session_id: session.id,
148
+ cart_number_id: cartNumberId,
149
+ cart_number_label: o.cartNumber,
150
+ order_id: o.orderId,
151
+ order_source: o.orderSource,
152
+ cart_index: o.cartIndex - 1,
153
+ order_number: o.orderNumber,
154
+ customer_name: o.customerName,
155
+ carrier_name: o.carrierName,
156
+ delivery_date: o.deliveryDate,
157
+ pickup_date: o.pickupDate,
158
+ };
159
+ if (checkedSet.has(key)) {
160
+ // Aangevinkt → status naar 'ready' (klaar voor transport)
161
+ await supabase.rpc("pickstation_set_cart_status", {
162
+ p_order_source: o.orderSource,
163
+ p_order_id: o.orderId,
164
+ p_cart_index: o.cartIndex - 1,
165
+ p_status: "ready",
166
+ });
167
+ items.push({ ...baseFields, action: "checked" });
168
+ checkedCount++;
169
+ continue;
170
+ }
171
+ // Niet aangevinkt → alleen verplaatsen wanneer de gebruiker dit expliciet
172
+ // in de bevestigings-pop-up heeft gekozen. Zonder bevestiging blijft de
173
+ // kar op de huidige pickdatum staan en registreren we uitsluitend een
174
+ // blokkade/waarschuwing.
175
+ const result = tryReschedule({
176
+ currentPickDate: new Date(o.pickDate || toIso(new Date())),
177
+ deliveryDate: o.deliveryDate ? new Date(o.deliveryDate) : null,
178
+ pickWeekdays,
179
+ minWorkdaysBeforeDelivery: minWorkdays,
180
+ });
181
+ if (result.ok && confirmedRescheduleSet.has(key)) {
182
+ const newIso = toIso(result.newPickDate);
183
+ await supabase
184
+ .from("cart_pick_overrides")
185
+ .upsert({
186
+ order_id: o.orderId,
187
+ order_source: o.orderSource,
188
+ cart_index: o.cartIndex - 1,
189
+ pick_date: newIso,
190
+ moved_from_date: toIso(new Date()),
191
+ }, { onConflict: "order_id,order_source,cart_index" });
192
+ items.push({
193
+ ...baseFields,
194
+ action: "rescheduled",
195
+ new_pick_date: newIso,
196
+ });
197
+ rescheduled.push({
198
+ key,
199
+ cartNumber: o.cartNumber,
200
+ orderNumber: o.orderNumber,
201
+ customerName: o.customerName,
202
+ newPickDate: newIso,
203
+ });
204
+ }
205
+ else {
206
+ const blockReason = result.ok
207
+ ? `Verplaatsing naar ${toIso(result.newPickDate)} is voorgesteld, maar niet bevestigd. De kar blijft op de huidige pickdatum staan.`
208
+ : result.reason;
209
+ items.push({
210
+ ...baseFields,
211
+ action: "blocked",
212
+ block_reason: blockReason,
213
+ });
214
+ blocked.push({
215
+ cartNumberId,
216
+ cartNumber: o.cartNumber,
217
+ orderId: o.orderId,
218
+ orderSource: o.orderSource,
219
+ cartIndex: o.cartIndex - 1,
220
+ orderNumber: o.orderNumber,
221
+ customerName: o.customerName,
222
+ carrierName: o.carrierName,
223
+ deliveryDate: o.deliveryDate,
224
+ pickupDate: o.pickupDate,
225
+ reason: blockReason,
226
+ });
227
+ }
228
+ }
229
+ if (items.length > 0) {
230
+ await supabase.from("cart_control_items").insert(items);
231
+ }
232
+ // Update sessie-aantallen
233
+ await supabase
234
+ .from("cart_control_sessions")
235
+ .update({
236
+ checked_count: checkedCount,
237
+ rescheduled_count: rescheduled.length,
238
+ blocked_count: blocked.length,
239
+ })
240
+ .eq("id", session.id);
241
+ // Kritieke melding aanmaken of oplossen
242
+ const alertKey = `cart_not_rescheduled:${toIso(new Date())}`;
243
+ if (blocked.length > 0) {
244
+ const relatedIds = blocked
245
+ .map((b) => b.cartNumberId)
246
+ .filter((v) => !!v);
247
+ await supabase
248
+ .from("dashboard_critical_alerts")
249
+ .upsert({
250
+ user_id: context.userId,
251
+ alert_key: alertKey,
252
+ alert_type: "cart_not_rescheduled",
253
+ priority: "critical",
254
+ title: "Karren kunnen niet doorgeschoven worden",
255
+ 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.`,
256
+ related_cart_ids: relatedIds,
257
+ resolved_at: null,
258
+ metadata: { blocked: blocked },
259
+ }, { onConflict: "user_id,alert_key" });
260
+ }
261
+ else {
262
+ // Auto-resolve als er geen blokkades zijn
263
+ await supabase
264
+ .from("dashboard_critical_alerts")
265
+ .update({ resolved_at: new Date().toISOString() })
266
+ .eq("user_id", context.userId)
267
+ .eq("alert_key", alertKey)
268
+ .is("resolved_at", null);
269
+ }
270
+ // Definitieve controle: wis alleen het autosave-concept van deze gebruiker voor vandaag.
271
+ await supabase
272
+ .from("cart_control_draft")
273
+ .delete()
274
+ .eq("user_id", context.userId)
275
+ .eq("draft_date", toIso(new Date()));
276
+ return {
277
+ sessionId: session.id,
278
+ checkedCount,
279
+ rescheduledCount: rescheduled.length,
280
+ blockedCount: blocked.length,
281
+ rescheduled,
282
+ blocked,
283
+ };
284
+ });
285
+ export const listControlHistory = createServerFn({ method: "GET" })
286
+ .middleware([requireSupabaseAuth])
287
+ .handler(async ({ context }) => {
288
+ const supabase = context.supabase;
289
+ const { data, error } = await supabase
290
+ .from("cart_control_sessions")
291
+ .select("id, control_date, performed_at, total_carts, checked_count, rescheduled_count, blocked_count, notes")
292
+ .eq("user_id", context.userId)
293
+ .order("performed_at", { ascending: false })
294
+ .limit(200);
295
+ if (error)
296
+ throw new Error(error.message);
297
+ const sessions = (data ?? []).map((r) => ({
298
+ id: r.id,
299
+ controlDate: r.control_date,
300
+ performedAt: r.performed_at,
301
+ totalCarts: r.total_carts,
302
+ checkedCount: r.checked_count,
303
+ rescheduledCount: r.rescheduled_count,
304
+ blockedCount: r.blocked_count,
305
+ notes: r.notes ?? null,
306
+ }));
307
+ return { sessions };
308
+ });
309
+ export const getControlSessionDetail = createServerFn({ method: "GET" })
310
+ .middleware([requireSupabaseAuth])
311
+ .inputValidator((d) => d)
312
+ .handler(async ({ data, context }) => {
313
+ const supabase = context.supabase;
314
+ const { data: rows, error } = await supabase
315
+ .from("cart_control_items")
316
+ .select("cart_number_label, order_number, customer_name, carrier_name, delivery_date, pickup_date, action, new_pick_date, block_reason")
317
+ .eq("session_id", data.sessionId)
318
+ .order("cart_number_label", { ascending: true });
319
+ if (error)
320
+ throw new Error(error.message);
321
+ const items = (rows ?? []).map((r) => ({
322
+ cartNumberLabel: r.cart_number_label ?? null,
323
+ orderNumber: r.order_number ?? null,
324
+ customerName: r.customer_name ?? null,
325
+ carrierName: r.carrier_name ?? null,
326
+ deliveryDate: r.delivery_date ?? null,
327
+ pickupDate: r.pickup_date ?? null,
328
+ action: r.action,
329
+ newPickDate: r.new_pick_date ?? null,
330
+ blockReason: r.block_reason ?? null,
331
+ }));
332
+ return { items };
333
+ });
334
+ export const listActiveCriticalAlerts = createServerFn({ method: "GET" })
335
+ .middleware([requireSupabaseAuth])
336
+ .handler(async ({ context }) => {
337
+ const supabase = context.supabase;
338
+ const { data, error } = await supabase
339
+ .from("dashboard_critical_alerts")
340
+ .select("id, alert_key, alert_type, priority, title, description, raised_at, metadata")
341
+ .eq("user_id", context.userId)
342
+ .is("resolved_at", null)
343
+ .order("raised_at", { ascending: false });
344
+ if (error)
345
+ throw new Error(error.message);
346
+ const alerts = (data ?? []).map((r) => {
347
+ const meta = r.metadata ?? {};
348
+ const blockedCount = Array.isArray(meta.blocked) ? meta.blocked.length : 0;
349
+ return {
350
+ id: r.id,
351
+ alertKey: r.alert_key,
352
+ alertType: r.alert_type,
353
+ priority: r.priority,
354
+ title: r.title,
355
+ description: r.description ?? null,
356
+ raisedAt: r.raised_at,
357
+ blockedCount,
358
+ };
359
+ });
360
+ return { alerts };
361
+ });
362
+ export const resolveCriticalAlert = createServerFn({ method: "POST" })
363
+ .middleware([requireSupabaseAuth])
364
+ .inputValidator((d) => d)
365
+ .handler(async ({ data, context }) => {
366
+ const supabase = context.supabase;
367
+ const { error } = await supabase
368
+ .from("dashboard_critical_alerts")
369
+ .update({ resolved_at: new Date().toISOString() })
370
+ .eq("id", data.id)
371
+ .eq("user_id", context.userId);
372
+ if (error)
373
+ throw new Error(error.message);
374
+ return { ok: true };
375
+ });
376
+ function normalizeStringArray(value) {
377
+ return Array.isArray(value) ? value.filter((k) => typeof k === "string") : [];
378
+ }
379
+ function normalizeCartStates(value) {
380
+ if (!value || typeof value !== "object" || Array.isArray(value))
381
+ return {};
382
+ const result = {};
383
+ for (const [key, raw] of Object.entries(value)) {
384
+ if (!key || !raw || typeof raw !== "object" || Array.isArray(raw))
385
+ continue;
386
+ const row = raw;
387
+ const status = row.status === "ready" ? "ready" : "pending";
388
+ const state = { status };
389
+ if (typeof row.note === "string")
390
+ state.note = row.note;
391
+ if (typeof row.updatedAt === "string")
392
+ state.updatedAt = row.updatedAt;
393
+ result[key] = state;
394
+ }
395
+ return result;
396
+ }
397
+ function isJsonValue(value) {
398
+ if (value === null)
399
+ return true;
400
+ if (["string", "number", "boolean"].includes(typeof value))
401
+ return true;
402
+ if (Array.isArray(value))
403
+ return value.every(isJsonValue);
404
+ if (typeof value === "object") {
405
+ return Object.values(value).every((v) => v === undefined || isJsonValue(v));
406
+ }
407
+ return false;
408
+ }
409
+ function normalizeFormState(value) {
410
+ if (!value || typeof value !== "object" || Array.isArray(value))
411
+ return {};
412
+ const result = {};
413
+ for (const [key, raw] of Object.entries(value)) {
414
+ if (!key)
415
+ continue;
416
+ if (raw !== undefined && isJsonValue(raw))
417
+ result[key] = raw;
418
+ }
419
+ return result;
420
+ }
421
+ export const getControlDraft = createServerFn({ method: "GET" })
422
+ .middleware([requireSupabaseAuth])
423
+ .handler(async ({ context }) => {
424
+ const supabase = context.supabase;
425
+ const today = toIso(new Date());
426
+ const { data, error } = await supabase
427
+ .from("cart_control_draft")
428
+ .select("notes, checked_keys, cart_states, form_state, updated_at")
429
+ .eq("user_id", context.userId)
430
+ .eq("draft_date", today)
431
+ .maybeSingle();
432
+ if (error)
433
+ throw new Error(error.message);
434
+ const cartStates = normalizeCartStates(data?.cart_states);
435
+ const checkedKeys = normalizeStringArray(data?.checked_keys);
436
+ const derivedCheckedKeys = checkedKeys.length > 0
437
+ ? checkedKeys
438
+ : Object.entries(cartStates)
439
+ .filter(([, state]) => state.status === "ready")
440
+ .map(([key]) => key);
441
+ return {
442
+ notes: data?.notes ?? "",
443
+ checkedKeys: derivedCheckedKeys,
444
+ cartStates,
445
+ formState: normalizeFormState(data?.form_state),
446
+ updatedAt: data?.updated_at ?? null,
447
+ };
448
+ });
449
+ export const saveControlDraft = createServerFn({ method: "POST" })
450
+ .middleware([requireSupabaseAuth])
451
+ .inputValidator((d) => d)
452
+ .handler(async ({ data, context }) => {
453
+ const supabase = context.supabase;
454
+ const today = toIso(new Date());
455
+ const nowIso = new Date().toISOString();
456
+ const patch = {
457
+ user_id: context.userId,
458
+ draft_date: today,
459
+ updated_at: nowIso,
460
+ };
461
+ if (data.notes !== undefined)
462
+ patch.notes = data.notes;
463
+ if (data.checkedKeys !== undefined)
464
+ patch.checked_keys = normalizeStringArray(data.checkedKeys);
465
+ if (data.cartStates !== undefined)
466
+ patch.cart_states = normalizeCartStates(data.cartStates);
467
+ if (data.formState !== undefined)
468
+ patch.form_state = normalizeFormState(data.formState);
469
+ const { error } = await supabase
470
+ .from("cart_control_draft")
471
+ .upsert(patch, { onConflict: "user_id,draft_date" });
472
+ if (error)
473
+ throw new Error(error.message);
474
+ return { ok: true, updatedAt: nowIso };
475
+ });
476
+ export const clearControlDraft = createServerFn({ method: "POST" })
477
+ .middleware([requireSupabaseAuth])
478
+ .handler(async ({ context }) => {
479
+ const supabase = context.supabase;
480
+ const today = toIso(new Date());
481
+ const { error } = await supabase
482
+ .from("cart_control_draft")
483
+ .delete()
484
+ .eq("user_id", context.userId)
485
+ .eq("draft_date", today);
486
+ if (error)
487
+ throw new Error(error.message);
488
+ return { ok: true };
489
+ });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Bepaalt of een niet-gecontroleerde kar als voorstel één (of meer) pickdagen
3
+ * kan opschuiven zonder de leverdatum in gevaar te brengen.
4
+ *
5
+ * We gebruiken uitsluitend globale instellingen (pickWeekdays) en de
6
+ * originele leverdatum als harde deadline. De heuristiek is bewust
7
+ * conservatief: een kar mag alleen als voorstel opschuiven wanneer er nog
8
+ * minstens één werkdag zit tussen de nieuwe pickdatum en de leverdatum. De
9
+ * daadwerkelijke verplaatsing gebeurt alleen na expliciete gebruikerskeuze.
10
+ */
11
+ export interface RescheduleInput {
12
+ /** Huidige (originele) pickdatum. */
13
+ currentPickDate: Date;
14
+ /** Bezorgdatum van de order. */
15
+ deliveryDate: Date | null;
16
+ /** Toegestane ISO-weekdagen (1=ma … 7=zo). */
17
+ pickWeekdays: number[];
18
+ /** Aantal werkdagen dat een order minimaal vóór levering nog gepickt moet zijn. */
19
+ minWorkdaysBeforeDelivery: number;
20
+ holidays?: Date[];
21
+ }
22
+ export type RescheduleResult = {
23
+ ok: true;
24
+ newPickDate: Date;
25
+ } | {
26
+ ok: false;
27
+ reason: string;
28
+ };
29
+ export declare function tryReschedule(input: RescheduleInput): RescheduleResult;
30
+ //# sourceMappingURL=cartRescheduling.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cartRescheduling.d.ts","sourceRoot":"","sources":["../../src/lib/cartRescheduling.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAUH,MAAM,WAAW,eAAe;IAC9B,qCAAqC;IACrC,eAAe,EAAE,IAAI,CAAC;IACtB,gCAAgC;IAChC,YAAY,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,8CAA8C;IAC9C,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mFAAmF;IACnF,yBAAyB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC;CACnB;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,WAAW,EAAE,IAAI,CAAA;CAAE,GAC/B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAgClC,wBAAgB,aAAa,CAAC,KAAK,EAAE,eAAe,GAAG,gBAAgB,CAsDtE"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Bepaalt of een niet-gecontroleerde kar als voorstel één (of meer) pickdagen
3
+ * kan opschuiven zonder de leverdatum in gevaar te brengen.
4
+ *
5
+ * We gebruiken uitsluitend globale instellingen (pickWeekdays) en de
6
+ * originele leverdatum als harde deadline. De heuristiek is bewust
7
+ * conservatief: een kar mag alleen als voorstel opschuiven wanneer er nog
8
+ * minstens één werkdag zit tussen de nieuwe pickdatum en de leverdatum. De
9
+ * daadwerkelijke verplaatsing gebeurt alleen na expliciete gebruikerskeuze.
10
+ */
11
+ import { addWorkdays, isoWeekday, isWorkday, normalizeWeekdays, subtractWorkdays, } from "./planning";
12
+ function normalize(d) {
13
+ const out = new Date(d);
14
+ out.setHours(0, 0, 0, 0);
15
+ return out;
16
+ }
17
+ function fmt(d) {
18
+ return d.toLocaleDateString("nl-NL", { day: "2-digit", month: "2-digit" });
19
+ }
20
+ /** Zoek de eerstvolgende toegestane pickdag na `from`. */
21
+ function nextAllowedPickDay(from, allowedWeekdays, holidays) {
22
+ const cursor = new Date(from);
23
+ cursor.setDate(cursor.getDate() + 1);
24
+ for (let i = 0; i < 30; i++) {
25
+ if (allowedWeekdays.has(isoWeekday(cursor)) &&
26
+ isWorkday(cursor, holidays)) {
27
+ return normalize(cursor);
28
+ }
29
+ cursor.setDate(cursor.getDate() + 1);
30
+ }
31
+ return normalize(cursor);
32
+ }
33
+ export function tryReschedule(input) {
34
+ if (!input.deliveryDate) {
35
+ return { ok: false, reason: "Geen leverdatum bekend." };
36
+ }
37
+ const allowed = new Set(normalizeWeekdays(input.pickWeekdays));
38
+ const delivery = normalize(input.deliveryDate);
39
+ const today = normalize(new Date());
40
+ if (delivery.getTime() <= today.getTime()) {
41
+ return {
42
+ ok: false,
43
+ reason: `Leverdatum ${fmt(delivery)} is vandaag of al verstreken.`,
44
+ };
45
+ }
46
+ const newDate = nextAllowedPickDay(normalize(input.currentPickDate), allowed, input.holidays);
47
+ // Uiterste pickdatum: minWorkdaysBeforeDelivery werkdagen vóór levering.
48
+ const latest = subtractWorkdays(delivery, Math.max(1, input.minWorkdaysBeforeDelivery), input.holidays);
49
+ if (newDate.getTime() > latest.getTime()) {
50
+ return {
51
+ ok: false,
52
+ reason: `Volgende pickdag ${fmt(newDate)} valt na de uiterste pickdatum ${fmt(latest)} voor levering op ${fmt(delivery)}.`,
53
+ };
54
+ }
55
+ // Extra check: nieuwe pickdatum moet vóór leverdatum liggen.
56
+ if (newDate.getTime() >= delivery.getTime()) {
57
+ return {
58
+ ok: false,
59
+ reason: `Nieuwe pickdatum ${fmt(newDate)} ligt niet vóór de leverdatum ${fmt(delivery)}.`,
60
+ };
61
+ }
62
+ // Sanity: hebben we wél minimaal 1 werkdag tussen pick en levering?
63
+ const minRequired = addWorkdays(newDate, 1, input.holidays);
64
+ if (minRequired.getTime() > delivery.getTime()) {
65
+ return {
66
+ ok: false,
67
+ reason: `Onvoldoende werkdagen tussen pickdatum en leverdatum ${fmt(delivery)}.`,
68
+ };
69
+ }
70
+ return { ok: true, newPickDate: newDate };
71
+ }