@flowselections/logistiek-engine 1.0.138 → 1.0.147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist-lib/components/GearchiveerdeOrdersCard.d.ts +2 -0
  2. package/dist-lib/components/GearchiveerdeOrdersCard.d.ts.map +1 -0
  3. package/dist-lib/components/GearchiveerdeOrdersCard.js +32 -0
  4. package/dist-lib/components/GeblokkeerdeOrdersCard.d.ts.map +1 -1
  5. package/dist-lib/components/GeblokkeerdeOrdersCard.js +1 -1
  6. package/dist-lib/components/PlanWachtrijPage.d.ts.map +1 -1
  7. package/dist-lib/components/PlanWachtrijPage.js +20 -13
  8. package/dist-lib/components/RuleEnginePage.d.ts.map +1 -1
  9. package/dist-lib/components/RuleEnginePage.js +24 -3
  10. package/dist-lib/hooks/useGeblokkeerdeOrders.d.ts +20 -0
  11. package/dist-lib/hooks/useGeblokkeerdeOrders.d.ts.map +1 -1
  12. package/dist-lib/hooks/useGeblokkeerdeOrders.js +57 -0
  13. package/dist-lib/index.d.ts +1 -0
  14. package/dist-lib/index.d.ts.map +1 -1
  15. package/dist-lib/index.js +1 -0
  16. package/dist-lib/integrations/supabase/types.d.ts +728 -0
  17. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  18. package/dist-lib/lib/briefTransport.server.d.ts +17 -0
  19. package/dist-lib/lib/briefTransport.server.d.ts.map +1 -0
  20. package/dist-lib/lib/briefTransport.server.js +41 -0
  21. package/dist-lib/lib/controlScan.server.d.ts.map +1 -1
  22. package/dist-lib/lib/controlScan.server.js +16 -1
  23. package/dist-lib/lib/handlers/logistics-tick.server.js +21 -0
  24. package/dist-lib/lib/layoutPrint.server.d.ts +18 -0
  25. package/dist-lib/lib/layoutPrint.server.d.ts.map +1 -1
  26. package/dist-lib/lib/layoutPrint.server.js +32 -1
  27. package/dist-lib/lib/logisticsTasks.d.ts +1 -0
  28. package/dist-lib/lib/logisticsTasks.d.ts.map +1 -1
  29. package/dist-lib/lib/logisticsTasks.js +2 -1
  30. package/dist-lib/lib/modulePrompts.d.ts +1 -0
  31. package/dist-lib/lib/modulePrompts.d.ts.map +1 -1
  32. package/dist-lib/lib/modulePrompts.js +338 -1
  33. package/dist-lib/lib/moduleVersion.d.ts +1 -1
  34. package/dist-lib/lib/moduleVersion.js +1 -1
  35. package/dist-lib/lib/planQueue.server.d.ts.map +1 -1
  36. package/dist-lib/lib/planQueue.server.js +13 -0
  37. package/dist-lib/lib/ruleEngineSchema.d.ts.map +1 -1
  38. package/dist-lib/lib/ruleEngineSchema.js +18 -0
  39. package/dist-lib/lib/shippingNoteQueue.server.d.ts.map +1 -1
  40. package/dist-lib/lib/shippingNoteQueue.server.js +18 -1
  41. package/dist-lib/lib/stickerQueue.server.d.ts +15 -0
  42. package/dist-lib/lib/stickerQueue.server.d.ts.map +1 -0
  43. package/dist-lib/lib/stickerQueue.server.js +349 -0
  44. package/dist-lib/lib/stickerRules.d.ts +38 -0
  45. package/dist-lib/lib/stickerRules.d.ts.map +1 -0
  46. package/dist-lib/lib/stickerRules.js +111 -0
  47. package/package.json +2 -2
@@ -0,0 +1,349 @@
1
+ // ============================================================================
2
+ // src/lib/stickerQueue.server.ts
3
+ // Stickerronde van de Engine (eigenaar van print.sticker en print.traysticker).
4
+ //
5
+ // 1. Database rekent: sticker -> order -> kar (op id), aantal (fail closed),
6
+ // moment uit de werkwijze (logistics_sticker_sync).
7
+ // 2. Tray-stickers komen vrij zodra Floriday de fulfillment order ACCEPTED of
8
+ // DELIVERED meldt.
9
+ // 3. Vooraf waarschuwen als het formaat niet op het geladen etiket past.
10
+ // 4. Pdf via de Floriday-gateway, formaatcontrole, printen via Layouts.
11
+ // 5. Na de goedgekeurde controlescan: afmelden in Floriday via de koppeling.
12
+ //
13
+ // Geen omgevingswaarden; Floriday wordt nooit rechtstreeks aangeroepen.
14
+ // ============================================================================
15
+ import { trace, newTraceId } from "./trace.server.js";
16
+ import { printViaLayoutGecontroleerd, floridayLayout, bucketUitLink } from "./layoutPrint.server.js";
17
+ import { getFlorydayDocument, getFlorydayFulfillmentStatus } from "./floridayDocuments.server.js";
18
+ import { controleerEtiket, parseGeladenEtiketten, pdfFormaat, stickerBackoffMinuten, waarschuwingTekst, } from "./stickerRules.js";
19
+ const COLS = "id, soort, bron_id, order_id, order_number, cart_id, kopieen, breedte_mm, hoogte_mm, print_plek_id, fulfillment_order_id, status, attempts";
20
+ function formaat(r) {
21
+ return r.breedte_mm && r.hoogte_mm ? { breedteMm: Number(r.breedte_mm), hoogteMm: Number(r.hoogte_mm) } : null;
22
+ }
23
+ function base64NaarBytes(b64) {
24
+ const bin = atob(b64.replace(/^data:[^,]*,/, ""));
25
+ const out = new Uint8Array(bin.length);
26
+ for (let i = 0; i < bin.length; i++)
27
+ out[i] = bin.charCodeAt(i);
28
+ return out;
29
+ }
30
+ export async function runStickerQueue(sb, opts = {}) {
31
+ const limit = Math.max(1, opts.limit ?? 10);
32
+ const run = { synced: null, released: 0, printed: 0, blocked: 0, retrying: 0, handled: 0, warnings: 0, errors: [] };
33
+ const nu = () => new Date().toISOString();
34
+ const zet = (id, velden) => sb.from("logistics_sticker_prints").update(velden).eq("id", id);
35
+ // 1. Rekenen in de database.
36
+ {
37
+ const { data, error } = await sb.rpc("logistics_sticker_sync", { p_limit: 200 });
38
+ if (error)
39
+ run.errors.push(`sync: ${error.message}`);
40
+ run.synced = data ?? null;
41
+ }
42
+ // 2. Tray-stickers: vrij na geaccepteerde belading.
43
+ {
44
+ const { data } = await sb
45
+ .from("logistics_sticker_prints")
46
+ .select(COLS)
47
+ .eq("soort", "traysticker")
48
+ .eq("status", "wacht_op_moment")
49
+ .not("fulfillment_order_id", "is", null)
50
+ .limit(20);
51
+ for (const r of (Array.isArray(data) ? data : [])) {
52
+ const besluit = await getFlorydayFulfillmentStatus(sb, r.fulfillment_order_id);
53
+ if (besluit.kind === "klaar") {
54
+ await zet(r.id, { status: "klaar", reden: null });
55
+ run.released++;
56
+ }
57
+ else if (besluit.kind === "afgewezen" || besluit.kind === "geannuleerd") {
58
+ await zet(r.id, {
59
+ status: "niet_printen",
60
+ reden: `Belading ${besluit.kind === "afgewezen" ? "afgewezen" : "geannuleerd"} in Floriday; geen tray-stickers.`,
61
+ });
62
+ }
63
+ }
64
+ }
65
+ // Geladen etiketten per printplek (één keer per ronde).
66
+ const etiketCache = new Map();
67
+ const etiketten = async (plek) => {
68
+ if (etiketCache.has(plek))
69
+ return etiketCache.get(plek);
70
+ const { data, error } = await sb.rpc("geladen_etiketten", { _werkplek_id: null, _print_plek_id: plek });
71
+ const res = error ? null : parseGeladenEtiketten(data);
72
+ etiketCache.set(plek, res);
73
+ return res;
74
+ };
75
+ const plekNaam = async (plek) => {
76
+ const { data } = await sb.from("print_plekken").select("naam").eq("id", plek).maybeSingle();
77
+ const n = data?.["naam"];
78
+ return typeof n === "string" && n.trim() ? n : "deze printplek";
79
+ };
80
+ const meld = async (r, f, etiket, ok) => {
81
+ if (!r.order_id)
82
+ return;
83
+ if (ok) {
84
+ await sb
85
+ .from("logistics_order_sticker_warnings")
86
+ .update({ opgelost_op: nu() })
87
+ .eq("order_id", r.order_id)
88
+ .eq("soort", r.soort)
89
+ .eq("bron_id", r.bron_id)
90
+ .is("opgelost_op", null);
91
+ return;
92
+ }
93
+ const plek = r.print_plek_id ? await plekNaam(r.print_plek_id) : "deze printplek";
94
+ await sb.from("logistics_order_sticker_warnings").upsert({
95
+ order_id: r.order_id,
96
+ soort: r.soort,
97
+ bron_id: r.bron_id,
98
+ benodigd_breedte_mm: f.breedteMm,
99
+ benodigd_hoogte_mm: f.hoogteMm,
100
+ geladen_breedte_mm: etiket?.breedteMm ?? null,
101
+ geladen_hoogte_mm: etiket?.hoogteMm ?? null,
102
+ plek,
103
+ melding: waarschuwingTekst(f, etiket, plek),
104
+ opgelost_op: null,
105
+ }, { onConflict: "order_id,soort,bron_id" });
106
+ run.warnings++;
107
+ };
108
+ // 3. Vooraf waarschuwen (formaat bekend, plek bekend, nog niet geprint).
109
+ {
110
+ const { data } = await sb
111
+ .from("logistics_sticker_prints")
112
+ .select(COLS)
113
+ .in("status", ["wacht_op_moment", "klaar"])
114
+ .not("breedte_mm", "is", null)
115
+ .not("print_plek_id", "is", null)
116
+ .limit(200);
117
+ for (const r of (Array.isArray(data) ? data : [])) {
118
+ const f = formaat(r);
119
+ const lijst = await etiketten(r.print_plek_id);
120
+ if (lijst === null)
121
+ continue; // niet leesbaar: geen valse melding, printen blokkeert later
122
+ const c = controleerEtiket(f, lijst);
123
+ await meld(r, f, c.etiket, c.ok);
124
+ }
125
+ }
126
+ // 4. Printen.
127
+ const { data: klaar } = await sb
128
+ .from("logistics_sticker_prints")
129
+ .select(COLS)
130
+ .eq("status", "klaar")
131
+ .or(`next_attempt_at.is.null,next_attempt_at.lte.${nu()}`)
132
+ .order("created_at")
133
+ .limit(limit);
134
+ for (const r of (Array.isArray(klaar) ? klaar : [])) {
135
+ const traceId = newTraceId();
136
+ const basis = {
137
+ traceId,
138
+ orderRef: r.order_number ?? r.order_id ?? null,
139
+ orderSource: "floriday",
140
+ cartRef: r.cart_id ?? null,
141
+ phase: "stickers",
142
+ actor: "logistiek-engine:stickerronde",
143
+ };
144
+ const label = r.soort === "sticker" ? "Sticker" : "Tray-stickers";
145
+ const blokkeer = async (reden) => {
146
+ await zet(r.id, { status: "geblokkeerd", reden });
147
+ run.blocked++;
148
+ await trace(sb, { ...basis, step: `${label} niet geprint`, status: "waarschuwing", reason: reden });
149
+ };
150
+ const later = async (reden) => {
151
+ const attempts = (r.attempts ?? 0) + 1;
152
+ const volgende = new Date(Date.now() + stickerBackoffMinuten(attempts) * 60000).toISOString();
153
+ await zet(r.id, { attempts, next_attempt_at: volgende, reden });
154
+ run.retrying++;
155
+ await trace(sb, { ...basis, step: `${label}: opnieuw proberen`, status: "wachtend", reason: reden, attempts, nextAttemptAt: volgende });
156
+ };
157
+ if (!r.cart_id) {
158
+ await blokkeer("Geen kar bekend voor deze sticker.");
159
+ continue;
160
+ }
161
+ if (!r.print_plek_id) {
162
+ await blokkeer("Geen printplek bekend (geen pakbon met printplek en geen pickstation in de werkwijze).");
163
+ continue;
164
+ }
165
+ // Pdf ophalen via de Floriday-koppeling.
166
+ const doc = (await getFlorydayDocument(sb, (r.soort === "sticker"
167
+ ? { kind: "customer-sticker", stickerId: r.bron_id }
168
+ : { kind: "tray-stickers", deliveryOrderId: r.bron_id })));
169
+ if (!doc["ok"]) {
170
+ await later(`Floriday: ${String(doc["error"] ?? "pdf niet beschikbaar")}`);
171
+ continue;
172
+ }
173
+ const url = (doc["url"] ?? doc["pdfUrl"] ?? doc["documentUrl"] ?? null);
174
+ const b64 = (doc["pdfBase64"] ?? doc["base64"] ?? doc["pdf"] ?? null);
175
+ if (!url && !b64) {
176
+ await later("Floriday gaf nog geen bruikbaar bestand terug.");
177
+ continue;
178
+ }
179
+ // Formaat: van Floriday (stickerSize), anders uit de pdf zelf.
180
+ let f = formaat(r);
181
+ if (!f) {
182
+ try {
183
+ const bytes = b64 ? base64NaarBytes(b64) : new Uint8Array(await (await fetch(url)).arrayBuffer());
184
+ f = pdfFormaat(bytes);
185
+ if (f)
186
+ await zet(r.id, { breedte_mm: f.breedteMm, hoogte_mm: f.hoogteMm });
187
+ }
188
+ catch {
189
+ f = null;
190
+ }
191
+ }
192
+ const lijst = await etiketten(r.print_plek_id);
193
+ const controle = controleerEtiket(f, lijst, r.soort === "sticker" ? "sticker" : "tray-sticker");
194
+ if (f && lijst !== null)
195
+ await meld(r, f, controle.etiket, controle.ok);
196
+ if (!controle.ok) {
197
+ await blokkeer(controle.reden);
198
+ continue;
199
+ }
200
+ const layout = await floridayLayout(sb, r.soort);
201
+ if (!layout.layoutId) {
202
+ await blokkeer(layout.reden ?? `Geen actieve Floriday-layout voor ${r.soort}.`);
203
+ continue;
204
+ }
205
+ // Blijvende verwijzing naar de opgeslagen pdf (alleen bij precies één rij).
206
+ let pdfOpslag = null;
207
+ const bucket = bucketUitLink(url);
208
+ if (bucket) {
209
+ if (r.soort === "sticker") {
210
+ const { data } = await sb
211
+ .from("floriday_customer_sticker_pdfs_cache")
212
+ .select("storage_path")
213
+ .eq("sticker_id", r.bron_id)
214
+ .limit(2);
215
+ if (Array.isArray(data) && data.length === 1) {
216
+ const pad = String(data[0]["storage_path"] ?? "").trim();
217
+ if (pad)
218
+ pdfOpslag = { bucket, pad };
219
+ }
220
+ }
221
+ else if (typeof doc["storagePath"] === "string" && doc["storagePath"]) {
222
+ pdfOpslag = { bucket, pad: doc["storagePath"] };
223
+ }
224
+ }
225
+ const printed = await printViaLayoutGecontroleerd(sb, {
226
+ documenttype: r.soort,
227
+ documentnaam: `${label} ${r.order_number ?? ""}`.trim(),
228
+ pdfBase64: b64,
229
+ pdfUrl: url,
230
+ layoutId: layout.layoutId,
231
+ printPlekId: r.print_plek_id,
232
+ herkomst: "floriday",
233
+ kopieen: r.kopieen,
234
+ idempotentieSleutel: `${r.soort}:${r.bron_id}:${r.cart_id}:${layout.layoutId}`,
235
+ payload: {
236
+ source: "logistiek-engine",
237
+ documenttype: r.soort,
238
+ layout_id: layout.layoutId,
239
+ print_plek_id: r.print_plek_id,
240
+ order_id: r.order_id,
241
+ ordernummer: r.order_number,
242
+ order_number: r.order_number,
243
+ cart_id: r.cart_id,
244
+ kopieen: r.kopieen,
245
+ ...(r.soort === "sticker" ? { sticker_id: r.bron_id } : { delivery_order_id: r.bron_id }),
246
+ fulfillment_order_id: r.fulfillment_order_id,
247
+ sticker_formaat_mm: f ? { breedte: f.breedteMm, hoogte: f.hoogteMm } : null,
248
+ ...(pdfOpslag ? { pdf_opslag: pdfOpslag } : {}),
249
+ },
250
+ });
251
+ if (!printed.ok) {
252
+ if (["GEEN_PRINTER", "GEEN_LAYOUT", "GEEN_EXTERN_DOCUMENT"].includes(printed.errorCode) || printed.errorCode.startsWith("FORMAAT")) {
253
+ await blokkeer(printed.error);
254
+ }
255
+ else {
256
+ await later(printed.error);
257
+ }
258
+ continue;
259
+ }
260
+ await zet(r.id, { status: "in_printwachtrij", print_job_id: printed.jobId, reden: null, next_attempt_at: null });
261
+ run.printed++;
262
+ await trace(sb, {
263
+ ...basis,
264
+ step: printed.bestaand
265
+ ? `${label} staat in de printwachtrij (opdracht ${printed.jobId})`
266
+ : `${label} in de printwachtrij gezet (opdracht ${printed.jobId}, ${r.kopieen}×)`,
267
+ status: "geslaagd",
268
+ });
269
+ }
270
+ // 5. Afmelden in Floriday na de goedgekeurde controlescan.
271
+ run.handled = await afmelden(sb, run);
272
+ return run;
273
+ }
274
+ async function afmelden(sb, run) {
275
+ const nu = new Date().toISOString();
276
+ const { data } = await sb
277
+ .from("logistics_sticker_prints")
278
+ .select("id, bron_id, cart_id, status")
279
+ .eq("soort", "sticker")
280
+ .eq("status", "in_printwachtrij")
281
+ .is("handled_at", null)
282
+ .or(`handled_next_at.is.null,handled_next_at.lte.${nu}`)
283
+ .limit(100);
284
+ const rows = (Array.isArray(data) ? data : []);
285
+ if (rows.length === 0)
286
+ return 0;
287
+ const perSticker = new Map();
288
+ for (const r of rows)
289
+ perSticker.set(r.bron_id, [...(perSticker.get(r.bron_id) ?? []), r.id]);
290
+ const klaarVoorAfmelden = [];
291
+ for (const stickerId of perSticker.keys()) {
292
+ const { data: alle } = await sb
293
+ .from("logistics_sticker_prints")
294
+ .select("cart_id, status")
295
+ .eq("soort", "sticker")
296
+ .eq("bron_id", stickerId)
297
+ .neq("status", "niet_printen");
298
+ const regels = (Array.isArray(alle) ? alle : []);
299
+ if (regels.length === 0 || regels.some((x) => x.status !== "in_printwachtrij" || !x.cart_id))
300
+ continue;
301
+ const carts = regels.map((x) => x.cart_id);
302
+ const { data: scans } = await sb
303
+ .from("logistics_control_scan")
304
+ .select("cart_id")
305
+ .in("cart_id", carts)
306
+ .not("approved_at", "is", null);
307
+ const gescand = new Set((Array.isArray(scans) ? scans : []).map((s) => s.cart_id));
308
+ if (carts.every((c) => gescand.has(c)))
309
+ klaarVoorAfmelden.push(stickerId);
310
+ }
311
+ if (klaarVoorAfmelden.length === 0)
312
+ return 0;
313
+ const ids = klaarVoorAfmelden.flatMap((s) => perSticker.get(s) ?? []);
314
+ const gw = (await import("@flowselections/floriday-authenticatie-module/gateway"));
315
+ const fn = gw["runCustomerStickersHandled"];
316
+ const volgende = new Date(Date.now() + 60 * 60000).toISOString();
317
+ if (typeof fn !== "function") {
318
+ await sb
319
+ .from("logistics_sticker_prints")
320
+ .update({
321
+ handled_error: "De Floriday-koppeling heeft nog geen afmeldfunctie (runCustomerStickersHandled); wacht op update.",
322
+ handled_next_at: volgende,
323
+ })
324
+ .in("id", ids);
325
+ return 0;
326
+ }
327
+ try {
328
+ const res = (await fn(sb, {
329
+ stickerIds: klaarVoorAfmelden,
330
+ })) ?? {};
331
+ const ok = res["ok"] === true || res["payload"]?.["ok"] === true;
332
+ if (!ok)
333
+ throw new Error(String(res["error"] ?? res["payload"]?.["error"] ?? "onbekende fout"));
334
+ await sb
335
+ .from("logistics_sticker_prints")
336
+ .update({ status: "afgemeld", handled_at: new Date().toISOString(), handled_error: null })
337
+ .in("id", ids);
338
+ return klaarVoorAfmelden.length;
339
+ }
340
+ catch (e) {
341
+ const msg = String(e?.message ?? e);
342
+ run.errors.push(`afmelden: ${msg}`);
343
+ await sb
344
+ .from("logistics_sticker_prints")
345
+ .update({ handled_error: `Afmelden in Floriday mislukt: ${msg}`, handled_next_at: volgende })
346
+ .in("id", ids);
347
+ return 0;
348
+ }
349
+ }
@@ -0,0 +1,38 @@
1
+ export interface Formaat {
2
+ breedteMm: number;
3
+ hoogteMm: number;
4
+ }
5
+ export interface GeladenEtiket {
6
+ printerId: string | null;
7
+ printerNaam: string;
8
+ breedteMm: number;
9
+ hoogteMm: number;
10
+ }
11
+ /** Formaat van de eerste pagina uit de MediaBox van een pdf (null = onbekend). */
12
+ export declare function pdfFormaat(bytes: Uint8Array): Formaat | null;
13
+ export declare function formaatTekst(f: Formaat): string;
14
+ /** Past de sticker op het etiket, rechtop of gedraaid (1 mm speling). */
15
+ export declare function past(sticker: Formaat, etiket: Formaat, spelingMm?: number): boolean;
16
+ /**
17
+ * Antwoord van geladen_etiketten (Layouts en printers) naar een vaste vorm.
18
+ * Rijen zonder bruikbaar formaat vallen weg. null = antwoord niet leesbaar.
19
+ */
20
+ export declare function parseGeladenEtiketten(data: unknown): GeladenEtiket[] | null;
21
+ export type EtiketControle = {
22
+ ok: true;
23
+ etiket: GeladenEtiket;
24
+ } | {
25
+ ok: false;
26
+ reden: string;
27
+ etiket: GeladenEtiket | null;
28
+ };
29
+ /**
30
+ * Fail closed: geen leesbaar antwoord, geen labelprinter of één labelprinter
31
+ * waarop de sticker niet past = niet printen.
32
+ */
33
+ export declare function controleerEtiket(sticker: Formaat | null, etiketten: GeladenEtiket[] | null, bron?: string): EtiketControle;
34
+ /** Vroege waarschuwing bij de order, in de woorden van de gebruiker. */
35
+ export declare function waarschuwingTekst(sticker: Formaat, etiket: GeladenEtiket | null, plek: string): string;
36
+ /** Wachttijd na een mislukte poging: 5, 15, daarna 60 minuten. */
37
+ export declare function stickerBackoffMinuten(attempts: number): number;
38
+ //# sourceMappingURL=stickerRules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stickerRules.d.ts","sourceRoot":"","sources":["../../src/lib/stickerRules.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,OAAO;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAID,kFAAkF;AAClF,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,GAAG,IAAI,CAY5D;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAG/C;AAED,yEAAyE;AACzE,wBAAgB,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,SAAI,GAAG,OAAO,CAG9E;AAOD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,GAAG,aAAa,EAAE,GAAG,IAAI,CAsB3E;AAED,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,aAAa,CAAA;CAAE,GACnC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAAA;CAAE,CAAC;AAE/D;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,OAAO,GAAG,IAAI,EACvB,SAAS,EAAE,aAAa,EAAE,GAAG,IAAI,EACjC,IAAI,SAAY,GACf,cAAc,CAwBhB;AAED,wEAAwE;AACxE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAItG;AAED,kEAAkE;AAClE,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI9D"}
@@ -0,0 +1,111 @@
1
+ // ============================================================================
2
+ // src/lib/stickerRules.ts
3
+ // Pure regels voor stickers: formaat uit een pdf lezen en controleren of een
4
+ // sticker op het geladen etiket van een labelprinter past. Geen database,
5
+ // geen netwerk; daardoor volledig testbaar.
6
+ // ============================================================================
7
+ const PT_PER_MM = 72 / 25.4;
8
+ /** Formaat van de eerste pagina uit de MediaBox van een pdf (null = onbekend). */
9
+ export function pdfFormaat(bytes) {
10
+ if (!bytes || bytes.length === 0)
11
+ return null;
12
+ // Latin-1 decoderen houdt de byteposities gelijk; genoeg om de MediaBox te lezen.
13
+ let tekst = "";
14
+ const max = Math.min(bytes.length, 2000000);
15
+ for (let i = 0; i < max; i++)
16
+ tekst += String.fromCharCode(bytes[i]);
17
+ const m = /\/MediaBox\s*\[\s*(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s*\]/.exec(tekst);
18
+ if (!m)
19
+ return null;
20
+ const b = Math.abs(Number(m[3]) - Number(m[1])) / PT_PER_MM;
21
+ const h = Math.abs(Number(m[4]) - Number(m[2])) / PT_PER_MM;
22
+ if (!Number.isFinite(b) || !Number.isFinite(h) || b <= 0 || h <= 0)
23
+ return null;
24
+ return { breedteMm: Math.round(b * 10) / 10, hoogteMm: Math.round(h * 10) / 10 };
25
+ }
26
+ export function formaatTekst(f) {
27
+ const r = (n) => (Math.round(n * 10) / 10).toString().replace(".", ",");
28
+ return `${r(f.breedteMm)}×${r(f.hoogteMm)} mm`;
29
+ }
30
+ /** Past de sticker op het etiket, rechtop of gedraaid (1 mm speling). */
31
+ export function past(sticker, etiket, spelingMm = 1) {
32
+ const ok = (b, h) => b <= etiket.breedteMm + spelingMm && h <= etiket.hoogteMm + spelingMm;
33
+ return ok(sticker.breedteMm, sticker.hoogteMm) || ok(sticker.hoogteMm, sticker.breedteMm);
34
+ }
35
+ function num(v) {
36
+ const n = typeof v === "string" ? Number(v.replace(",", ".")) : typeof v === "number" ? v : NaN;
37
+ return Number.isFinite(n) && n > 0 ? n : null;
38
+ }
39
+ /**
40
+ * Antwoord van geladen_etiketten (Layouts en printers) naar een vaste vorm.
41
+ * Rijen zonder bruikbaar formaat vallen weg. null = antwoord niet leesbaar.
42
+ */
43
+ export function parseGeladenEtiketten(data) {
44
+ const lijst = Array.isArray(data)
45
+ ? data
46
+ : data && typeof data === "object" && Array.isArray(data["etiketten"])
47
+ ? data["etiketten"]
48
+ : null;
49
+ if (!lijst)
50
+ return null;
51
+ const uit = [];
52
+ for (const raw of lijst) {
53
+ if (!raw || typeof raw !== "object")
54
+ continue;
55
+ const r = raw;
56
+ const b = num(r["breedte_mm"] ?? r["etiket_breedte_mm"] ?? r["breedteMm"] ?? r["width_mm"]);
57
+ const h = num(r["hoogte_mm"] ?? r["etiket_hoogte_mm"] ?? r["hoogteMm"] ?? r["height_mm"]);
58
+ if (!b || !h)
59
+ continue;
60
+ uit.push({
61
+ printerId: (r["printer_id"] ?? r["printerId"] ?? null),
62
+ printerNaam: String(r["printer_naam"] ?? r["eigen_naam"] ?? r["naam"] ?? r["printerNaam"] ?? r["printer_id"] ?? "onbekende printer"),
63
+ breedteMm: b,
64
+ hoogteMm: h,
65
+ });
66
+ }
67
+ return uit;
68
+ }
69
+ /**
70
+ * Fail closed: geen leesbaar antwoord, geen labelprinter of één labelprinter
71
+ * waarop de sticker niet past = niet printen.
72
+ */
73
+ export function controleerEtiket(sticker, etiketten, bron = "sticker") {
74
+ if (!sticker) {
75
+ return { ok: false, reden: `Formaat van de ${bron} is onbekend; niet geprint.`, etiket: null };
76
+ }
77
+ if (etiketten === null) {
78
+ return {
79
+ ok: false,
80
+ reden: "Het geladen etiket kon niet worden gelezen (geladen_etiketten van Layouts en printers 1.56.0 ontbreekt of gaf een fout); niet geprint.",
81
+ etiket: null,
82
+ };
83
+ }
84
+ if (etiketten.length === 0) {
85
+ return { ok: false, reden: "Op deze plek staat geen labelprinter met een geladen etiket; niet geprint.", etiket: null };
86
+ }
87
+ for (const e of etiketten) {
88
+ if (!past(sticker, e)) {
89
+ return {
90
+ ok: false,
91
+ reden: `${bron} ${formaatTekst(sticker)} past niet op het geladen etiket ${formaatTekst(e)} van ${e.printerNaam}`,
92
+ etiket: e,
93
+ };
94
+ }
95
+ }
96
+ return { ok: true, etiket: etiketten[0] };
97
+ }
98
+ /** Vroege waarschuwing bij de order, in de woorden van de gebruiker. */
99
+ export function waarschuwingTekst(sticker, etiket, plek) {
100
+ return etiket
101
+ ? `Voor deze order is een sticker ${formaatTekst(sticker)} nodig; op ${plek} zit ${formaatTekst(etiket)}.`
102
+ : `Voor deze order is een sticker ${formaatTekst(sticker)} nodig; op ${plek} is geen geladen etiket bekend.`;
103
+ }
104
+ /** Wachttijd na een mislukte poging: 5, 15, daarna 60 minuten. */
105
+ export function stickerBackoffMinuten(attempts) {
106
+ if (attempts <= 1)
107
+ return 5;
108
+ if (attempts === 2)
109
+ return 15;
110
+ return 60;
111
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowselections/logistiek-engine",
3
- "version": "1.0.138",
3
+ "version": "1.0.147",
4
4
  "flowselections": {
5
5
  "moduleId": "logistiek-engine",
6
6
  "pages": [
@@ -139,7 +139,7 @@
139
139
  "@cloudflare/vite-plugin": "^1.25.5",
140
140
  "@eslint/js": "^9.32.0",
141
141
  "@flowselections/core": "1.0.29",
142
- "@lovable.dev/vite-tanstack-config": "2.25.3",
142
+ "@lovable.dev/vite-tanstack-config": "2.26.5",
143
143
  "@rsbuild/core": "^1.0.0",
144
144
  "@tailwindcss/vite": "^4.2.1",
145
145
  "@tanstack/react-query": "^5.83.0",