@flowselections/floriday-afhandeling-module 1.0.10 → 1.0.11
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.
- package/dist-lib/components/afhandeling/ControleLijstPrint.d.ts +7 -0
- package/dist-lib/components/afhandeling/ControleLijstPrint.d.ts.map +1 -0
- package/dist-lib/components/afhandeling/ControleLijstPrint.js +67 -0
- package/dist-lib/components/afhandeling/ControlePage.d.ts +2 -0
- package/dist-lib/components/afhandeling/ControlePage.d.ts.map +1 -0
- package/dist-lib/components/afhandeling/ControlePage.js +456 -0
- package/dist-lib/components/afhandeling/DagDetailDialog.d.ts.map +1 -1
- package/dist-lib/components/afhandeling/DagDetailDialog.js +9 -5
- package/dist-lib/components/afhandeling/VervoerderForm.d.ts.map +1 -1
- package/dist-lib/components/afhandeling/VervoerderForm.js +10 -1
- package/dist-lib/components/afhandeling/VervoerdersPage.d.ts.map +1 -1
- package/dist-lib/components/afhandeling/VervoerdersPage.js +3 -2
- package/dist-lib/components/afhandeling/WeekAgendaView.d.ts.map +1 -1
- package/dist-lib/components/afhandeling/WeekAgendaView.js +15 -5
- package/dist-lib/components/afhandeling/WeekdayPicker.d.ts +12 -0
- package/dist-lib/components/afhandeling/WeekdayPicker.d.ts.map +1 -0
- package/dist-lib/components/afhandeling/WeekdayPicker.js +30 -0
- package/dist-lib/components/pickstation/PakbonTemplate.d.ts +1 -0
- package/dist-lib/components/pickstation/PakbonTemplate.d.ts.map +1 -1
- package/dist-lib/components/pickstation/PakbonTemplate.js +31 -8
- package/dist-lib/components/settings/AfhandelingSettingsCard.d.ts.map +1 -1
- package/dist-lib/components/settings/AfhandelingSettingsCard.js +9 -2
- package/dist-lib/components/ui/responsive-dialog.d.ts +20 -0
- package/dist-lib/components/ui/responsive-dialog.d.ts.map +1 -0
- package/dist-lib/components/ui/responsive-dialog.js +44 -0
- package/dist-lib/hooks/useOrderPlanning.d.ts.map +1 -1
- package/dist-lib/hooks/useOrderPlanning.js +17 -6
- package/dist-lib/index.d.ts +1 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +4 -2
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +629 -3
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +629 -3
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +629 -3
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +639 -3
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/afhandelingSettings.functions.d.ts +1318 -62
- package/dist-lib/lib/afhandelingSettings.functions.d.ts.map +1 -1
- package/dist-lib/lib/afhandelingSettings.functions.js +16 -1
- package/dist-lib/lib/carrierMatch.server.d.ts +27 -0
- package/dist-lib/lib/carrierMatch.server.d.ts.map +1 -0
- package/dist-lib/lib/carrierMatch.server.js +75 -0
- package/dist-lib/lib/carriers.functions.d.ts +3151 -15
- package/dist-lib/lib/carriers.functions.d.ts.map +1 -1
- package/dist-lib/lib/carriers.functions.js +17 -2
- package/dist-lib/lib/cartAssignments.functions.d.ts +3145 -15
- package/dist-lib/lib/cartAssignments.functions.d.ts.map +1 -1
- package/dist-lib/lib/cartControl.functions.d.ts +99259 -0
- package/dist-lib/lib/cartControl.functions.d.ts.map +1 -0
- package/dist-lib/lib/cartControl.functions.js +489 -0
- package/dist-lib/lib/cartRescheduling.d.ts +30 -0
- package/dist-lib/lib/cartRescheduling.d.ts.map +1 -0
- package/dist-lib/lib/cartRescheduling.js +71 -0
- package/dist-lib/lib/floriday.functions.d.ts +629 -3
- package/dist-lib/lib/floriday.functions.d.ts.map +1 -1
- package/dist-lib/lib/pickstationI18n.d.ts +8 -0
- package/dist-lib/lib/pickstationI18n.d.ts.map +1 -1
- package/dist-lib/lib/pickstationI18n.js +48 -0
- package/dist-lib/lib/pickstationOrders.server.d.ts +1 -0
- package/dist-lib/lib/pickstationOrders.server.d.ts.map +1 -1
- package/dist-lib/lib/pickstationOrders.server.js +94 -7
- package/dist-lib/lib/pickstationTypes.d.ts +12 -0
- package/dist-lib/lib/pickstationTypes.d.ts.map +1 -1
- package/dist-lib/lib/planning.d.ts +23 -0
- package/dist-lib/lib/planning.d.ts.map +1 -1
- package/dist-lib/lib/planning.js +66 -6
- package/package.json +1 -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
|
+
}
|