@flowselections/floriday-afhandeling-module 1.0.7 → 1.0.9
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/LeveringenPage.d.ts.map +1 -1
- package/dist-lib/components/afhandeling/LeveringenPage.js +9 -67
- package/dist-lib/components/afhandeling/NieuweLeveringPage.d.ts +2 -0
- package/dist-lib/components/afhandeling/NieuweLeveringPage.d.ts.map +1 -0
- package/dist-lib/components/afhandeling/NieuweLeveringPage.js +7 -0
- package/dist-lib/components/afhandeling/PickAgendaView.d.ts +6 -0
- package/dist-lib/components/afhandeling/PickAgendaView.d.ts.map +1 -0
- package/dist-lib/components/afhandeling/PickAgendaView.js +31 -0
- package/dist-lib/components/afhandeling/PickingPage.d.ts +2 -0
- package/dist-lib/components/afhandeling/PickingPage.d.ts.map +1 -0
- package/dist-lib/components/afhandeling/PickingPage.js +5 -0
- package/dist-lib/components/afhandeling/PlanningAgendaPage.d.ts +9 -0
- package/dist-lib/components/afhandeling/PlanningAgendaPage.d.ts.map +1 -0
- package/dist-lib/components/afhandeling/PlanningAgendaPage.js +7 -0
- package/dist-lib/components/afhandeling/VervoerderForm.d.ts.map +1 -1
- package/dist-lib/components/afhandeling/VervoerderForm.js +24 -10
- package/dist-lib/components/afhandeling/WeekAgendaView.d.ts +12 -0
- package/dist-lib/components/afhandeling/WeekAgendaView.d.ts.map +1 -0
- package/dist-lib/components/afhandeling/WeekAgendaView.js +107 -0
- package/dist-lib/components/pickstation/LanguagePicker.d.ts +5 -0
- package/dist-lib/components/pickstation/LanguagePicker.d.ts.map +1 -0
- package/dist-lib/components/pickstation/LanguagePicker.js +5 -0
- package/dist-lib/components/pickstation/PakbonTemplate.d.ts +17 -0
- package/dist-lib/components/pickstation/PakbonTemplate.d.ts.map +1 -0
- package/dist-lib/components/pickstation/PakbonTemplate.js +74 -0
- package/dist-lib/components/pickstation/printDocuments.d.ts +3 -0
- package/dist-lib/components/pickstation/printDocuments.d.ts.map +1 -0
- package/dist-lib/components/pickstation/printDocuments.js +53 -0
- package/dist-lib/components/settings/AfhandelingSettingsCard.d.ts +2 -0
- package/dist-lib/components/settings/AfhandelingSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/AfhandelingSettingsCard.js +60 -0
- package/dist-lib/components/settings/VervoerdersSettingsCard.d.ts +2 -0
- package/dist-lib/components/settings/VervoerdersSettingsCard.d.ts.map +1 -0
- package/dist-lib/components/settings/VervoerdersSettingsCard.js +30 -0
- package/dist-lib/hooks/useOrderPlanning.d.ts +14 -0
- package/dist-lib/hooks/useOrderPlanning.d.ts.map +1 -0
- package/dist-lib/hooks/useOrderPlanning.js +90 -0
- package/dist-lib/index.d.ts +6 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +17 -6
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +2206 -138
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +2206 -138
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +2206 -138
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +2252 -126
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +9 -1
- package/dist-lib/lib/afhandelingSettings.functions.d.ts +14338 -0
- package/dist-lib/lib/afhandelingSettings.functions.d.ts.map +1 -0
- package/dist-lib/lib/afhandelingSettings.functions.js +73 -0
- package/dist-lib/lib/carriers.functions.d.ts +19092 -10812
- package/dist-lib/lib/carriers.functions.d.ts.map +1 -1
- package/dist-lib/lib/carriers.functions.js +43 -6
- package/dist-lib/lib/floriday.functions.d.ts +2207 -138
- package/dist-lib/lib/floriday.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday.functions.js +6 -1
- package/dist-lib/lib/pickDate.d.ts +21 -0
- package/dist-lib/lib/pickDate.d.ts.map +1 -0
- package/dist-lib/lib/pickDate.js +71 -0
- package/dist-lib/lib/pickstationI18n.d.ts +31 -0
- package/dist-lib/lib/pickstationI18n.d.ts.map +1 -0
- package/dist-lib/lib/pickstationI18n.js +147 -0
- package/dist-lib/lib/pickstationOrders.server.d.ts +9 -0
- package/dist-lib/lib/pickstationOrders.server.d.ts.map +1 -0
- package/dist-lib/lib/pickstationOrders.server.js +244 -0
- package/dist-lib/lib/pickstationTypes.d.ts +17 -0
- package/dist-lib/lib/pickstationTypes.d.ts.map +1 -0
- package/dist-lib/lib/pickstationTypes.js +1 -0
- package/dist-lib/lib/planning.d.ts +66 -0
- package/dist-lib/lib/planning.d.ts.map +1 -0
- package/dist-lib/lib/planning.js +180 -0
- package/package.json +12 -6
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// Server-only helper. Imported by /api/public/pickstation.* routes.
|
|
2
|
+
// Uses the service-role admin client to bypass RLS for the *public* page.
|
|
3
|
+
// Returns ONLY safe fields (no email/phone/address/prices).
|
|
4
|
+
import { calculateOrderPlanning } from "./planning";
|
|
5
|
+
const FLORIDAY_BASE_URLS = {
|
|
6
|
+
staging: "https://api.staging.floriday.io/suppliers-api-2026v1",
|
|
7
|
+
production: "https://api.floriday.io/suppliers-api-2026v1",
|
|
8
|
+
};
|
|
9
|
+
function pickStr(r, ...keys) {
|
|
10
|
+
for (const k of keys) {
|
|
11
|
+
const v = r[k];
|
|
12
|
+
if (typeof v === "string" && v.length > 0)
|
|
13
|
+
return v;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
function pickCustomer(raw) {
|
|
18
|
+
const buyer = raw.buyer;
|
|
19
|
+
const customer = raw.customer;
|
|
20
|
+
return (pickStr(buyer ?? {}, "organizationName", "name") ??
|
|
21
|
+
pickStr(customer ?? {}, "organizationName", "name") ??
|
|
22
|
+
pickStr(raw, "customerName", "buyerName") ??
|
|
23
|
+
"—");
|
|
24
|
+
}
|
|
25
|
+
function pickCountry(raw) {
|
|
26
|
+
const buyer = raw.buyer;
|
|
27
|
+
const customer = raw.customer;
|
|
28
|
+
const addr = raw.deliveryAddress ??
|
|
29
|
+
raw.shipTo ??
|
|
30
|
+
buyer?.address ??
|
|
31
|
+
customer?.address;
|
|
32
|
+
return (pickStr(addr ?? {}, "countryCode", "country") ??
|
|
33
|
+
pickStr(raw, "deliveryCountry", "countryCode") ??
|
|
34
|
+
null);
|
|
35
|
+
}
|
|
36
|
+
function pickDeliveryDate(raw) {
|
|
37
|
+
const candidates = [
|
|
38
|
+
raw.deliveryDate,
|
|
39
|
+
raw.requestedDeliveryDate,
|
|
40
|
+
raw.expectedDeliveryDate,
|
|
41
|
+
raw.loadingDateTime,
|
|
42
|
+
raw.deliveryDateTime,
|
|
43
|
+
];
|
|
44
|
+
const found = candidates.find((v) => typeof v === "string" && v);
|
|
45
|
+
return found ?? null;
|
|
46
|
+
}
|
|
47
|
+
function toIsoDay(d) {
|
|
48
|
+
const y = d.getFullYear();
|
|
49
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
50
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
51
|
+
return `${y}-${m}-${day}`;
|
|
52
|
+
}
|
|
53
|
+
async function getActiveConnection(admin) {
|
|
54
|
+
const { data, error } = await admin
|
|
55
|
+
.from("floriday_connections")
|
|
56
|
+
.select("api_key, access_token, environment")
|
|
57
|
+
.eq("is_active", true)
|
|
58
|
+
.order("updated_at", { ascending: false })
|
|
59
|
+
.limit(1)
|
|
60
|
+
.maybeSingle();
|
|
61
|
+
if (error)
|
|
62
|
+
throw new Error(`Floriday-verbinding: ${error.message}`);
|
|
63
|
+
if (!data)
|
|
64
|
+
throw new Error("Geen actieve Floriday-verbinding gevonden");
|
|
65
|
+
if (!data.access_token)
|
|
66
|
+
throw new Error("Floriday access-token ontbreekt");
|
|
67
|
+
return data;
|
|
68
|
+
}
|
|
69
|
+
async function getSettings(admin) {
|
|
70
|
+
const { data: settingsRow } = await admin
|
|
71
|
+
.from("afhandeling_settings")
|
|
72
|
+
.select("pick_days_before_transport")
|
|
73
|
+
.order("updated_at", { ascending: false })
|
|
74
|
+
.limit(1)
|
|
75
|
+
.maybeSingle();
|
|
76
|
+
const { data: defCarrier } = await admin
|
|
77
|
+
.from("carriers")
|
|
78
|
+
.select("pickup_moments, delivery_days, signup_days_before_delivery, signup_time_1")
|
|
79
|
+
.eq("is_default", true)
|
|
80
|
+
.limit(1)
|
|
81
|
+
.maybeSingle();
|
|
82
|
+
const { DEFAULT_PICKUP_MOMENT } = await import("./planning");
|
|
83
|
+
const raw = (defCarrier?.pickup_moments ?? []);
|
|
84
|
+
const moments = Array.isArray(raw) && raw.length > 0
|
|
85
|
+
? raw
|
|
86
|
+
: [DEFAULT_PICKUP_MOMENT];
|
|
87
|
+
return {
|
|
88
|
+
pickDays: settingsRow?.pick_days_before_transport ?? 1,
|
|
89
|
+
pickupMoments: moments,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function fetchFloridayOrders(conn) {
|
|
93
|
+
const baseUrl = FLORIDAY_BASE_URLS[conn.environment] ?? FLORIDAY_BASE_URLS.staging;
|
|
94
|
+
const res = await fetch(`${baseUrl}/sales-orders/sync/0?limit=200`, {
|
|
95
|
+
headers: {
|
|
96
|
+
Authorization: `Bearer ${conn.access_token}`,
|
|
97
|
+
"x-api-key": conn.api_key,
|
|
98
|
+
Accept: "application/json",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const body = await res.text().catch(() => "");
|
|
103
|
+
throw new Error(`Floriday API ${res.status}: ${body.slice(0, 200)}`);
|
|
104
|
+
}
|
|
105
|
+
const json = (await res.json());
|
|
106
|
+
if (Array.isArray(json))
|
|
107
|
+
return json;
|
|
108
|
+
const obj = json;
|
|
109
|
+
if (Array.isArray(obj.results))
|
|
110
|
+
return obj.results;
|
|
111
|
+
if (Array.isArray(obj.items))
|
|
112
|
+
return obj.items;
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
function mapLines(raw) {
|
|
116
|
+
const list = (raw.salesOrderLines ?? raw.lines ?? raw.orderLines);
|
|
117
|
+
if (!Array.isArray(list))
|
|
118
|
+
return [];
|
|
119
|
+
return list.map((l) => {
|
|
120
|
+
const r = (l ?? {});
|
|
121
|
+
const tradeItem = r.tradeItem;
|
|
122
|
+
const product = r.product;
|
|
123
|
+
return {
|
|
124
|
+
description: pickStr(r, "description", "name") ??
|
|
125
|
+
pickStr(tradeItem ?? {}, "name", "description") ??
|
|
126
|
+
pickStr(product ?? {}, "name") ??
|
|
127
|
+
"—",
|
|
128
|
+
quantity: (typeof r.numberOfPieces === "number" && r.numberOfPieces) ||
|
|
129
|
+
(typeof r.quantity === "number" && r.quantity) ||
|
|
130
|
+
(typeof r.numberOfTradeUnits === "number" && r.numberOfTradeUnits) ||
|
|
131
|
+
null,
|
|
132
|
+
unit: pickStr(r, "packingUnit", "unit", "uom"),
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
export async function loadTodaysPickstationOrders() {
|
|
137
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
138
|
+
const admin = supabaseAdmin;
|
|
139
|
+
const [conn, settings] = await Promise.all([getActiveConnection(admin), getSettings(admin)]);
|
|
140
|
+
const raw = await fetchFloridayOrders(conn);
|
|
141
|
+
const todayIso = toIsoDay(new Date());
|
|
142
|
+
const filtered = [];
|
|
143
|
+
for (const r of raw) {
|
|
144
|
+
const deliveryDate = pickDeliveryDate(r);
|
|
145
|
+
const planning = calculateOrderPlanning(deliveryDate, {
|
|
146
|
+
pickupMoments: settings.pickupMoments,
|
|
147
|
+
pickDurationWorkdays: settings.pickDays,
|
|
148
|
+
orderCreatedAt: r.createDateTime ?? r.createdAt ?? null,
|
|
149
|
+
});
|
|
150
|
+
if (!planning)
|
|
151
|
+
continue;
|
|
152
|
+
const pickIso = toIsoDay(planning.pickStartDate);
|
|
153
|
+
if (pickIso !== todayIso)
|
|
154
|
+
continue;
|
|
155
|
+
const id = r.id ?? r.salesOrderId ?? crypto.randomUUID();
|
|
156
|
+
filtered.push({
|
|
157
|
+
id,
|
|
158
|
+
orderNumber: pickStr(r, "salesOrderNumber", "orderNumber", "number") ?? "—",
|
|
159
|
+
customerName: pickCustomer(r),
|
|
160
|
+
country: pickCountry(r),
|
|
161
|
+
deliveryDate,
|
|
162
|
+
pickDate: pickIso,
|
|
163
|
+
lineCount: Array.isArray(r.salesOrderLines ?? r.lines ?? r.orderLines)
|
|
164
|
+
? (r.salesOrderLines ?? r.lines ?? r.orderLines).length
|
|
165
|
+
: 0,
|
|
166
|
+
lines: mapLines(r),
|
|
167
|
+
printedCount: 0,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// Hang print counts on each order (today only)
|
|
171
|
+
if (filtered.length > 0) {
|
|
172
|
+
const sinceIso = new Date();
|
|
173
|
+
sinceIso.setHours(0, 0, 0, 0);
|
|
174
|
+
const { data: logs } = await admin
|
|
175
|
+
.from("pickstation_print_log")
|
|
176
|
+
.select("order_id")
|
|
177
|
+
.gte("printed_at", sinceIso.toISOString());
|
|
178
|
+
if (logs) {
|
|
179
|
+
const counts = new Map();
|
|
180
|
+
for (const l of logs) {
|
|
181
|
+
counts.set(l.order_id, (counts.get(l.order_id) ?? 0) + 1);
|
|
182
|
+
}
|
|
183
|
+
for (const o of filtered)
|
|
184
|
+
o.printedCount = counts.get(o.id) ?? 0;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Sort: not-printed first, then by delivery date
|
|
188
|
+
filtered.sort((a, b) => {
|
|
189
|
+
if ((a.printedCount === 0) !== (b.printedCount === 0)) {
|
|
190
|
+
return a.printedCount === 0 ? -1 : 1;
|
|
191
|
+
}
|
|
192
|
+
return (a.deliveryDate ?? "").localeCompare(b.deliveryDate ?? "");
|
|
193
|
+
});
|
|
194
|
+
return filtered;
|
|
195
|
+
}
|
|
196
|
+
export async function fetchFloridayAuctionLetterPdf(orderId) {
|
|
197
|
+
// Best-effort: probe a couple of likely endpoints from Floriday's swagger.
|
|
198
|
+
// If none returns a PDF, return null so the UI silently skips.
|
|
199
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
200
|
+
const admin = supabaseAdmin;
|
|
201
|
+
const conn = await getActiveConnection(admin);
|
|
202
|
+
const baseUrl = FLORIDAY_BASE_URLS[conn.environment] ?? FLORIDAY_BASE_URLS.staging;
|
|
203
|
+
const candidates = [
|
|
204
|
+
`${baseUrl}/sales-orders/${orderId}/auction-letter`,
|
|
205
|
+
`${baseUrl}/sales-orders/${orderId}/shipping-documents`,
|
|
206
|
+
`${baseUrl}/sales-orders/${orderId}/documents/auction-letter`,
|
|
207
|
+
];
|
|
208
|
+
for (const url of candidates) {
|
|
209
|
+
try {
|
|
210
|
+
const res = await fetch(url, {
|
|
211
|
+
headers: {
|
|
212
|
+
Authorization: `Bearer ${conn.access_token}`,
|
|
213
|
+
"x-api-key": conn.api_key,
|
|
214
|
+
Accept: "application/pdf",
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
if (!res.ok)
|
|
218
|
+
continue;
|
|
219
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
220
|
+
if (!ct.includes("pdf"))
|
|
221
|
+
continue;
|
|
222
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
223
|
+
let bin = "";
|
|
224
|
+
for (let i = 0; i < buf.length; i++)
|
|
225
|
+
bin += String.fromCharCode(buf[i]);
|
|
226
|
+
return { base64: btoa(bin), mime: "application/pdf" };
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// try next
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
export async function logPrint(orderId, orderNumber, customerName, documents, ipAddress) {
|
|
235
|
+
const { supabaseAdmin } = await import("../integrations/supabase/client.server");
|
|
236
|
+
const admin = supabaseAdmin;
|
|
237
|
+
await admin.from("pickstation_print_log").insert({
|
|
238
|
+
order_id: orderId,
|
|
239
|
+
order_number: orderNumber,
|
|
240
|
+
customer_name: customerName,
|
|
241
|
+
documents,
|
|
242
|
+
ip_address: ipAddress,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface PickstationLine {
|
|
2
|
+
description: string;
|
|
3
|
+
quantity: number | null;
|
|
4
|
+
unit: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface PickstationOrder {
|
|
7
|
+
id: string;
|
|
8
|
+
orderNumber: string;
|
|
9
|
+
customerName: string;
|
|
10
|
+
country: string | null;
|
|
11
|
+
deliveryDate: string | null;
|
|
12
|
+
pickDate: string;
|
|
13
|
+
lineCount: number;
|
|
14
|
+
lines: PickstationLine[];
|
|
15
|
+
printedCount: number;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=pickstationTypes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pickstationTypes.d.ts","sourceRoot":"","sources":["../../src/lib/pickstationTypes.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centrale planningsmodule — werkdagen-rekenkern, ophaalmomenten,
|
|
3
|
+
* order-planning en pickverdeling. Eén bron van waarheid voor alle agenda-views.
|
|
4
|
+
*
|
|
5
|
+
* Model — bezorgdag = dag 0. Elk ophaalmoment definieert per stap (aanmelden,
|
|
6
|
+
* ophalen, bezorgen) hoeveel werkdagen vóór de bezorgdag de stap plaatsvindt
|
|
7
|
+
* en op welk tijdstip.
|
|
8
|
+
*/
|
|
9
|
+
export interface PickupMoment {
|
|
10
|
+
/** Werkdagen vóór bezorgdag waarop transport aangemeld wordt. */
|
|
11
|
+
signup_days: number;
|
|
12
|
+
/** Aanmeldtijd ("HH:MM"). */
|
|
13
|
+
signup_time: string;
|
|
14
|
+
/** Werkdagen vóór bezorgdag waarop opgehaald wordt. */
|
|
15
|
+
pickup_days: number;
|
|
16
|
+
/** Ophaaltijd ("HH:MM"). */
|
|
17
|
+
pickup_time: string;
|
|
18
|
+
/** Werkdagen vóór bezorgdag waarop bezorgd wordt (meestal 0). */
|
|
19
|
+
delivery_days: number;
|
|
20
|
+
/** Bezorgtijd ("HH:MM"). */
|
|
21
|
+
delivery_time: string;
|
|
22
|
+
}
|
|
23
|
+
export declare const DEFAULT_PICKUP_MOMENT: PickupMoment;
|
|
24
|
+
export interface PlanningRules {
|
|
25
|
+
/** Beschikbare ophaalmomenten — minimaal één. */
|
|
26
|
+
pickupMoments: PickupMoment[];
|
|
27
|
+
/** Werkdagen pickduur — terugtellen vanaf ophaaldag. */
|
|
28
|
+
pickDurationWorkdays: number;
|
|
29
|
+
/** Tijdstip waarop de order is binnengekomen (voor moment-selectie). */
|
|
30
|
+
orderCreatedAt?: string | Date | null;
|
|
31
|
+
holidays?: Date[];
|
|
32
|
+
}
|
|
33
|
+
export interface OrderPlanning {
|
|
34
|
+
deliveryDate: Date;
|
|
35
|
+
pickupDate: Date;
|
|
36
|
+
transportRegisterDate: Date;
|
|
37
|
+
pickStartDate: Date;
|
|
38
|
+
scheduledPickDate: Date;
|
|
39
|
+
movedEarlier: boolean;
|
|
40
|
+
/** Welk moment is gekozen (handig voor debug / UI). */
|
|
41
|
+
selectedMoment: PickupMoment;
|
|
42
|
+
}
|
|
43
|
+
export declare function isWorkday(d: Date, holidays?: Date[]): boolean;
|
|
44
|
+
export declare function subtractWorkdays(date: Date, n: number, holidays?: Date[]): Date;
|
|
45
|
+
export declare function addWorkdays(date: Date, n: number, holidays?: Date[]): Date;
|
|
46
|
+
/**
|
|
47
|
+
* Bepaal de aanmelddeadline van een moment voor een gegeven bezorgdag.
|
|
48
|
+
*/
|
|
49
|
+
export declare function getSignupDeadline(deliveryDate: Date, moment: PickupMoment, holidays?: Date[]): Date;
|
|
50
|
+
/**
|
|
51
|
+
* Kies het juiste ophaalmoment op basis van het ordertijdstip.
|
|
52
|
+
* - Voorkeursvolgorde: vroegst gesorteerd op aanmelddeadline.
|
|
53
|
+
* - Eerste moment waarvan deadline nog niet is verstreken wint.
|
|
54
|
+
* - Als alle deadlines verstreken zijn → laatste moment (best effort).
|
|
55
|
+
*/
|
|
56
|
+
export declare function selectPickupMoment(deliveryDate: Date, orderCreatedAt: Date | null | undefined, moments: PickupMoment[], holidays?: Date[]): PickupMoment;
|
|
57
|
+
export declare function calculateOrderPlanning(deliveryDate: string | Date | null | undefined, rules: PlanningRules): OrderPlanning | null;
|
|
58
|
+
export interface PickDistributionOptions {
|
|
59
|
+
enabled: boolean;
|
|
60
|
+
maxWorkdaysEarlier: number;
|
|
61
|
+
holidays?: Date[];
|
|
62
|
+
}
|
|
63
|
+
export declare function distributePickDates<T extends {
|
|
64
|
+
planning: OrderPlanning | null;
|
|
65
|
+
}>(items: T[], options: PickDistributionOptions): T[];
|
|
66
|
+
//# sourceMappingURL=planning.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planning.d.ts","sourceRoot":"","sources":["../../src/lib/planning.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,YAAY;IAC3B,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAC;IACpB,6BAA6B;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,aAAa,EAAE,MAAM,CAAC;IACtB,4BAA4B;IAC5B,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,eAAO,MAAM,qBAAqB,EAAE,YAOnC,CAAC;AAEF,MAAM,WAAW,aAAa;IAC5B,iDAAiD;IACjD,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,wDAAwD;IACxD,oBAAoB,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;IACtC,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,IAAI,CAAC;IACnB,UAAU,EAAE,IAAI,CAAC;IACjB,qBAAqB,EAAE,IAAI,CAAC;IAC5B,aAAa,EAAE,IAAI,CAAC;IACpB,iBAAiB,EAAE,IAAI,CAAC;IACxB,YAAY,EAAE,OAAO,CAAC;IACtB,uDAAuD;IACvD,cAAc,EAAE,YAAY,CAAC;CAC9B;AAUD,wBAAgB,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,EAAE,IAAI,EAAE,GAAG,OAAO,CAK7D;AAQD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,IAAI,EAAE,GAAG,IAAI,CAQ/E;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,IAAI,EAAE,GAAG,IAAI,CAQ1E;AAcD;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,IAAI,EAClB,MAAM,EAAE,YAAY,EACpB,QAAQ,CAAC,EAAE,IAAI,EAAE,GAChB,IAAI,CAGN;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,IAAI,EAClB,cAAc,EAAE,IAAI,GAAG,IAAI,GAAG,SAAS,EACvC,OAAO,EAAE,YAAY,EAAE,EACvB,QAAQ,CAAC,EAAE,IAAI,EAAE,GAChB,YAAY,CAgBd;AAED,wBAAgB,sBAAsB,CACpC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,SAAS,EAC9C,KAAK,EAAE,aAAa,GACnB,aAAa,GAAG,IAAI,CA6BtB;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC;CACnB;AAED,wBAAgB,mBAAmB,CAAC,CAAC,SAAS;IAAE,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAA;CAAE,EAC9E,KAAK,EAAE,CAAC,EAAE,EACV,OAAO,EAAE,uBAAuB,GAC/B,CAAC,EAAE,CA2DL"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centrale planningsmodule — werkdagen-rekenkern, ophaalmomenten,
|
|
3
|
+
* order-planning en pickverdeling. Eén bron van waarheid voor alle agenda-views.
|
|
4
|
+
*
|
|
5
|
+
* Model — bezorgdag = dag 0. Elk ophaalmoment definieert per stap (aanmelden,
|
|
6
|
+
* ophalen, bezorgen) hoeveel werkdagen vóór de bezorgdag de stap plaatsvindt
|
|
7
|
+
* en op welk tijdstip.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_PICKUP_MOMENT = {
|
|
10
|
+
signup_days: 2,
|
|
11
|
+
signup_time: "16:00",
|
|
12
|
+
pickup_days: 1,
|
|
13
|
+
pickup_time: "17:30",
|
|
14
|
+
delivery_days: 0,
|
|
15
|
+
delivery_time: "08:00",
|
|
16
|
+
};
|
|
17
|
+
function sameDay(a, b) {
|
|
18
|
+
return (a.getFullYear() === b.getFullYear() &&
|
|
19
|
+
a.getMonth() === b.getMonth() &&
|
|
20
|
+
a.getDate() === b.getDate());
|
|
21
|
+
}
|
|
22
|
+
export function isWorkday(d, holidays) {
|
|
23
|
+
const day = d.getDay();
|
|
24
|
+
if (day === 0 || day === 6)
|
|
25
|
+
return false;
|
|
26
|
+
if (holidays && holidays.some((h) => sameDay(h, d)))
|
|
27
|
+
return false;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
function normalize(d) {
|
|
31
|
+
const out = new Date(d);
|
|
32
|
+
out.setHours(0, 0, 0, 0);
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
export function subtractWorkdays(date, n, holidays) {
|
|
36
|
+
const out = normalize(date);
|
|
37
|
+
let remaining = Math.max(0, Math.floor(n));
|
|
38
|
+
while (remaining > 0) {
|
|
39
|
+
out.setDate(out.getDate() - 1);
|
|
40
|
+
if (isWorkday(out, holidays))
|
|
41
|
+
remaining--;
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
export function addWorkdays(date, n, holidays) {
|
|
46
|
+
const out = normalize(date);
|
|
47
|
+
let remaining = Math.max(0, Math.floor(n));
|
|
48
|
+
while (remaining > 0) {
|
|
49
|
+
out.setDate(out.getDate() + 1);
|
|
50
|
+
if (isWorkday(out, holidays))
|
|
51
|
+
remaining--;
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
function parseHHMM(time) {
|
|
56
|
+
const [h = "0", m = "0"] = (time ?? "").split(":");
|
|
57
|
+
return { h: Number(h) || 0, m: Number(m) || 0 };
|
|
58
|
+
}
|
|
59
|
+
function atTime(date, time) {
|
|
60
|
+
const { h, m } = parseHHMM(time);
|
|
61
|
+
const out = new Date(date);
|
|
62
|
+
out.setHours(h, m, 0, 0);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Bepaal de aanmelddeadline van een moment voor een gegeven bezorgdag.
|
|
67
|
+
*/
|
|
68
|
+
export function getSignupDeadline(deliveryDate, moment, holidays) {
|
|
69
|
+
const day = subtractWorkdays(deliveryDate, moment.signup_days, holidays);
|
|
70
|
+
return atTime(day, moment.signup_time);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Kies het juiste ophaalmoment op basis van het ordertijdstip.
|
|
74
|
+
* - Voorkeursvolgorde: vroegst gesorteerd op aanmelddeadline.
|
|
75
|
+
* - Eerste moment waarvan deadline nog niet is verstreken wint.
|
|
76
|
+
* - Als alle deadlines verstreken zijn → laatste moment (best effort).
|
|
77
|
+
*/
|
|
78
|
+
export function selectPickupMoment(deliveryDate, orderCreatedAt, moments, holidays) {
|
|
79
|
+
if (moments.length === 0)
|
|
80
|
+
return DEFAULT_PICKUP_MOMENT;
|
|
81
|
+
if (moments.length === 1)
|
|
82
|
+
return moments[0];
|
|
83
|
+
const now = orderCreatedAt ?? new Date();
|
|
84
|
+
const sorted = [...moments].sort((a, b) => {
|
|
85
|
+
const da = getSignupDeadline(deliveryDate, a, holidays).getTime();
|
|
86
|
+
const db = getSignupDeadline(deliveryDate, b, holidays).getTime();
|
|
87
|
+
return da - db;
|
|
88
|
+
});
|
|
89
|
+
for (const m of sorted) {
|
|
90
|
+
const deadline = getSignupDeadline(deliveryDate, m, holidays);
|
|
91
|
+
if (now.getTime() <= deadline.getTime())
|
|
92
|
+
return m;
|
|
93
|
+
}
|
|
94
|
+
return sorted[sorted.length - 1];
|
|
95
|
+
}
|
|
96
|
+
export function calculateOrderPlanning(deliveryDate, rules) {
|
|
97
|
+
if (!deliveryDate)
|
|
98
|
+
return null;
|
|
99
|
+
const d = typeof deliveryDate === "string" ? new Date(deliveryDate) : deliveryDate;
|
|
100
|
+
if (Number.isNaN(d.getTime()))
|
|
101
|
+
return null;
|
|
102
|
+
const delivery = normalize(d);
|
|
103
|
+
const moments = rules.pickupMoments.length > 0 ? rules.pickupMoments : [DEFAULT_PICKUP_MOMENT];
|
|
104
|
+
const orderCreated = rules.orderCreatedAt == null
|
|
105
|
+
? null
|
|
106
|
+
: rules.orderCreatedAt instanceof Date
|
|
107
|
+
? rules.orderCreatedAt
|
|
108
|
+
: new Date(rules.orderCreatedAt);
|
|
109
|
+
const moment = selectPickupMoment(delivery, orderCreated, moments, rules.holidays);
|
|
110
|
+
const pickup = subtractWorkdays(delivery, moment.pickup_days, rules.holidays);
|
|
111
|
+
const transportRegister = subtractWorkdays(delivery, moment.signup_days, rules.holidays);
|
|
112
|
+
const pickStart = subtractWorkdays(pickup, rules.pickDurationWorkdays, rules.holidays);
|
|
113
|
+
return {
|
|
114
|
+
deliveryDate: delivery,
|
|
115
|
+
pickupDate: pickup,
|
|
116
|
+
transportRegisterDate: transportRegister,
|
|
117
|
+
pickStartDate: pickStart,
|
|
118
|
+
scheduledPickDate: pickStart,
|
|
119
|
+
movedEarlier: false,
|
|
120
|
+
selectedMoment: moment,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function distributePickDates(items, options) {
|
|
124
|
+
if (!options.enabled || options.maxWorkdaysEarlier <= 0) {
|
|
125
|
+
return items.map((it) => {
|
|
126
|
+
if (!it.planning)
|
|
127
|
+
return it;
|
|
128
|
+
return {
|
|
129
|
+
...it,
|
|
130
|
+
planning: {
|
|
131
|
+
...it.planning,
|
|
132
|
+
scheduledPickDate: it.planning.pickStartDate,
|
|
133
|
+
movedEarlier: false,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const sorted = [...items].sort((a, b) => {
|
|
139
|
+
const ad = a.planning?.pickStartDate.getTime() ?? Infinity;
|
|
140
|
+
const bd = b.planning?.pickStartDate.getTime() ?? Infinity;
|
|
141
|
+
return ad - bd;
|
|
142
|
+
});
|
|
143
|
+
const load = new Map();
|
|
144
|
+
const key = (d) => `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
|
145
|
+
const result = new Map();
|
|
146
|
+
for (const item of sorted) {
|
|
147
|
+
if (!item.planning) {
|
|
148
|
+
result.set(item, item);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const deadline = item.planning.pickStartDate;
|
|
152
|
+
const candidates = [deadline];
|
|
153
|
+
let cursor = deadline;
|
|
154
|
+
for (let i = 0; i < options.maxWorkdaysEarlier; i++) {
|
|
155
|
+
cursor = subtractWorkdays(cursor, 1, options.holidays);
|
|
156
|
+
candidates.unshift(cursor);
|
|
157
|
+
}
|
|
158
|
+
let best = candidates[0];
|
|
159
|
+
let bestLoad = load.get(key(best)) ?? 0;
|
|
160
|
+
for (let i = 1; i < candidates.length; i++) {
|
|
161
|
+
const c = candidates[i];
|
|
162
|
+
const l = load.get(key(c)) ?? 0;
|
|
163
|
+
if (l < bestLoad) {
|
|
164
|
+
best = c;
|
|
165
|
+
bestLoad = l;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
load.set(key(best), bestLoad + 1);
|
|
169
|
+
const moved = best.getTime() !== deadline.getTime();
|
|
170
|
+
result.set(item, {
|
|
171
|
+
...item,
|
|
172
|
+
planning: {
|
|
173
|
+
...item.planning,
|
|
174
|
+
scheduledPickDate: best,
|
|
175
|
+
movedEarlier: moved,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return items.map((it) => result.get(it) ?? it);
|
|
180
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowselections/floriday-afhandeling-module",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@cloudflare/vite-plugin": "^1.25.5",
|
|
42
42
|
"@eslint/js": "^9.32.0",
|
|
43
43
|
"@flowselections/core": "^1.0.11",
|
|
44
|
-
"@lovable.dev/vite-tanstack-config": "2.
|
|
44
|
+
"@lovable.dev/vite-tanstack-config": "2.6.2",
|
|
45
45
|
"@rsbuild/core": "^1.0.0",
|
|
46
46
|
"@tailwindcss/vite": "^4.2.1",
|
|
47
47
|
"@tanstack/react-query": "^5.83.0",
|
|
@@ -71,12 +71,18 @@
|
|
|
71
71
|
{
|
|
72
72
|
"export": "LeveringenPage",
|
|
73
73
|
"route": "afhandeling/leveringen",
|
|
74
|
-
"title": "Leveringen"
|
|
74
|
+
"title": "Planning en Leveringen"
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
|
-
"export": "
|
|
78
|
-
"route": "afhandeling/
|
|
79
|
-
"title": "
|
|
77
|
+
"export": "NieuweLeveringPage",
|
|
78
|
+
"route": "afhandeling/leveringen/nieuw",
|
|
79
|
+
"title": "Levering aanmaken",
|
|
80
|
+
"nav": false
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"export": "PickingPage",
|
|
84
|
+
"route": "afhandeling/picking",
|
|
85
|
+
"title": "Pick Agenda"
|
|
80
86
|
}
|
|
81
87
|
]
|
|
82
88
|
}
|