@flowselections/floriday-afhandeling-module 1.0.43 → 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.
- package/dist-lib/components/afhandeling/ControlePage.d.ts.map +1 -1
- package/dist-lib/components/afhandeling/ControlePage.js +92 -54
- package/dist-lib/components/afhandeling/WeekAgendaView.d.ts.map +1 -1
- package/dist-lib/components/afhandeling/WeekAgendaView.js +24 -1
- package/dist-lib/hooks/useCartIndex.d.ts.map +1 -1
- package/dist-lib/hooks/useCartIndex.js +16 -7
- package/dist-lib/hooks/useEngineBufferEvents.d.ts +12 -0
- package/dist-lib/hooks/useEngineBufferEvents.d.ts.map +1 -0
- package/dist-lib/hooks/useEngineBufferEvents.js +52 -0
- package/dist-lib/hooks/useEngineTasks.d.ts.map +1 -1
- package/dist-lib/hooks/useEngineTasks.js +8 -18
- package/dist-lib/hooks/useKnownOrders.d.ts +1 -0
- package/dist-lib/hooks/useKnownOrders.d.ts.map +1 -1
- package/dist-lib/hooks/useKnownOrders.js +28 -2
- package/dist-lib/hooks/useOrderPickDates.d.ts +1 -1
- package/dist-lib/hooks/useOrderPickDates.d.ts.map +1 -1
- package/dist-lib/hooks/useOrderPickDates.js +5 -3
- package/dist-lib/hooks/usePickAgenda.d.ts +4 -0
- package/dist-lib/hooks/usePickAgenda.d.ts.map +1 -1
- package/dist-lib/hooks/usePickAgenda.js +23 -16
- package/dist-lib/hooks/usePickMode.d.ts +8 -2
- package/dist-lib/hooks/usePickMode.d.ts.map +1 -1
- package/dist-lib/hooks/usePickMode.js +17 -9
- package/dist-lib/index.d.ts +1 -2
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +1 -33
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +3256 -191
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +3256 -191
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/moduleClient.d.ts +3257 -190
- package/dist-lib/integrations/supabase/moduleClient.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +3354 -261
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/afhandelingSettings.functions.d.ts +6144 -14
- package/dist-lib/lib/afhandelingSettings.functions.d.ts.map +1 -1
- package/dist-lib/lib/carriers.functions.d.ts +15375 -40
- package/dist-lib/lib/carriers.functions.d.ts.map +1 -1
- package/dist-lib/lib/cartAssignments.functions.d.ts +15360 -35
- package/dist-lib/lib/cartAssignments.functions.d.ts.map +1 -1
- package/dist-lib/lib/cartControl.functions.d.ts +33902 -104
- package/dist-lib/lib/cartControl.functions.d.ts.map +1 -1
- package/dist-lib/lib/cartControl.functions.js +389 -259
- package/dist-lib/lib/deliveryNote.functions.d.ts +3256 -191
- package/dist-lib/lib/deliveryNote.functions.d.ts.map +1 -1
- package/dist-lib/lib/engineLayouts.functions.d.ts +3256 -191
- package/dist-lib/lib/engineLayouts.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday.functions.d.ts +6146 -14
- package/dist-lib/lib/floriday.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday.functions.js +1 -0
- package/dist-lib/lib/orderFulfilment.functions.d.ts +3256 -191
- package/dist-lib/lib/orderFulfilment.functions.d.ts.map +1 -1
- package/dist-lib/lib/pickDates.functions.d.ts +3260 -190
- package/dist-lib/lib/pickDates.functions.d.ts.map +1 -1
- package/dist-lib/lib/pickDates.functions.js +30 -3
- package/dist-lib/lib/pickMode.d.ts +20 -2
- package/dist-lib/lib/pickMode.d.ts.map +1 -1
- package/dist-lib/lib/pickMode.js +65 -16
- package/dist-lib/lib/realtimeBus.d.ts +20 -0
- package/dist-lib/lib/realtimeBus.d.ts.map +1 -0
- package/dist-lib/lib/realtimeBus.js +56 -0
- package/dist-lib/lib/serverFnError.d.ts +12 -0
- package/dist-lib/lib/serverFnError.d.ts.map +1 -1
- package/dist-lib/lib/serverFnError.js +48 -1
- package/dist-lib/moduleDefinition.d.ts +3 -0
- package/dist-lib/moduleDefinition.d.ts.map +1 -0
- package/dist-lib/moduleDefinition.js +36 -0
- 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
|
-
|
|
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
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 =
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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 ??
|
|
166
|
+
cartNumber: r.cart_number ?? null,
|
|
162
167
|
orderId: r.order_id,
|
|
163
168
|
orderSource,
|
|
164
169
|
cartIndex: r.cart_index,
|
|
165
|
-
cartSeq
|
|
170
|
+
cartSeq,
|
|
166
171
|
cartTotal,
|
|
172
|
+
originalPickDate,
|
|
167
173
|
orderNumber,
|
|
168
174
|
orderNumbers,
|
|
169
175
|
customerName: order?.customerName ?? "",
|
|
170
|
-
carrierName:
|
|
176
|
+
carrierName: null,
|
|
171
177
|
deliveryDate: order?.deliveryDate ?? null,
|
|
172
|
-
pickupDate:
|
|
173
|
-
pickupTime:
|
|
174
|
-
country:
|
|
175
|
-
cratesOnCart:
|
|
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 ?
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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("
|
|
397
|
-
.
|
|
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("
|
|
416
|
-
|
|
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])
|