@flowselections/pick-schermen-kwekers 1.0.17 → 1.0.19

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 (51) hide show
  1. package/README.md +24 -24
  2. package/dist-lib/components/TemplateModulePage.d.ts +2 -0
  3. package/dist-lib/components/TemplateModulePage.d.ts.map +1 -0
  4. package/dist-lib/components/TemplateModulePage.js +44 -0
  5. package/dist-lib/components/pick/PickScherm.d.ts +1 -1
  6. package/dist-lib/components/pick/PickScherm.d.ts.map +1 -1
  7. package/dist-lib/components/pick/PickScherm.js +541 -60
  8. package/dist-lib/components/pick/PreviewDialog.d.ts.map +1 -1
  9. package/dist-lib/components/pick/PreviewDialog.js +11 -2
  10. package/dist-lib/components/pick/QueueTable.d.ts +4 -1
  11. package/dist-lib/components/pick/QueueTable.d.ts.map +1 -1
  12. package/dist-lib/components/pick/QueueTable.js +70 -10
  13. package/dist-lib/components/pick/ShippingNoteScan.d.ts +15 -0
  14. package/dist-lib/components/pick/ShippingNoteScan.d.ts.map +1 -0
  15. package/dist-lib/components/pick/ShippingNoteScan.js +89 -0
  16. package/dist-lib/hooks/usePickQueue.d.ts +9 -0
  17. package/dist-lib/hooks/usePickQueue.d.ts.map +1 -1
  18. package/dist-lib/hooks/usePickQueue.js +50 -2
  19. package/dist-lib/hooks/usePrintJob.d.ts +23 -0
  20. package/dist-lib/hooks/usePrintJob.d.ts.map +1 -1
  21. package/dist-lib/hooks/usePrintJob.js +83 -1
  22. package/dist-lib/index.d.ts +2 -0
  23. package/dist-lib/index.d.ts.map +1 -1
  24. package/dist-lib/index.js +2 -0
  25. package/dist-lib/integrations/supabase/auth-middleware.d.ts +3821 -1197
  26. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  27. package/dist-lib/integrations/supabase/client.d.ts +3821 -1197
  28. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  29. package/dist-lib/integrations/supabase/client.server.d.ts +3821 -1197
  30. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  31. package/dist-lib/integrations/supabase/types.d.ts +3876 -1226
  32. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  33. package/dist-lib/integrations/supabase/types.js +1 -0
  34. package/dist-lib/lib/layoutMaker/pakbonHtml.js +49 -49
  35. package/dist-lib/lib/layoutRenderer.js +17 -17
  36. package/dist-lib/lib/pickApi.d.ts +80 -3
  37. package/dist-lib/lib/pickApi.d.ts.map +1 -1
  38. package/dist-lib/lib/pickApi.js +197 -2
  39. package/dist-lib/lib/pickPreview.d.ts +9 -4
  40. package/dist-lib/lib/pickPreview.d.ts.map +1 -1
  41. package/dist-lib/lib/pickPreview.js +41 -80
  42. package/dist-lib/lib/pickTypes.d.ts +82 -1
  43. package/dist-lib/lib/pickTypes.d.ts.map +1 -1
  44. package/dist-lib/lib/pickTypes.js +213 -1
  45. package/dist-lib/lib/pickUiText.d.ts +7 -0
  46. package/dist-lib/lib/pickUiText.d.ts.map +1 -1
  47. package/dist-lib/lib/pickUiText.js +14 -0
  48. package/dist-lib/lib/shippingNote.d.ts +41 -0
  49. package/dist-lib/lib/shippingNote.d.ts.map +1 -0
  50. package/dist-lib/lib/shippingNote.js +77 -0
  51. package/package.json +1 -1
@@ -1,19 +1,26 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { Badge, Button, Card, CardContent, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, toast, } from "@flowselections/core";
4
- import { AlertTriangle, ArrowLeft, CheckCircle2, Globe, Loader2, Printer, RefreshCw, ScanLine, Search, WifiOff, Zap, } from "lucide-react";
4
+ import { AlertTriangle, ArrowLeft, CheckCircle2, ChevronLeft, ChevronRight, Globe, Loader2, Printer, RefreshCw, Search, WifiOff, Zap, } from "lucide-react";
5
5
  import { LanguageTiles } from "./LanguageTiles";
6
6
  import { QueueTable } from "./QueueTable";
7
7
  import { PreviewDialog } from "./PreviewDialog";
8
+ import { ShippingNoteScan } from "./ShippingNoteScan";
8
9
  import { getSessionId, pickApi } from "../../lib/pickApi";
9
10
  import { completeLogisticsTaskForItem } from "../../lib/pickPrint";
11
+ import { isPdfOnlyFailure, printedCartIdsOf, pdfSaveWarning, } from "../../lib/pickTypes";
10
12
  import { getPickTranslator } from "../../lib/pickI18n";
11
13
  import { friendlyPickError, getPickUiTexts } from "../../lib/pickUiText";
12
14
  import { getPrintJobTexts, printJobStatusLabel } from "../../lib/pickPrintJobText";
13
- import { defaultFilters, usePickHeartbeat, usePickQueue, } from "../../hooks/usePickQueue";
15
+ import { defaultFilters, usePickDays, usePickHeartbeat, usePickQueue, } from "../../hooks/usePickQueue";
14
16
  import { usePickDocumentTypes, usePickStatuses, useStationPrintPlek, } from "../../hooks/usePickConfig";
15
- import { usePrintJob } from "../../hooks/usePrintJob";
17
+ import { usePrintBatch, usePrintJob } from "../../hooks/usePrintJob";
16
18
  const LANG_STORAGE_KEY = "pick.language";
19
+ /** Melding als de planning voor deze order geen karren heeft vastgelegd. */
20
+ /** Papier is geprint, maar de PDF-kopie op de werkplek is niet opgeslagen. */
21
+ const PDF_SAVE_NOTICE = "De pakbon is geprint, maar de PDF is niet op de werkplek opgeslagen. Geef de PDF-map in Layout maker opnieuw vrij.";
22
+ /** Melding als de planning voor deze order geen karren heeft vastgelegd. */
23
+ const NO_CART_PLAN_NOTICE = "De karplanning voor deze order is nog niet klaar — de logistiek is automatisch op de hoogte gebracht. De pakbon is als één document geprint.";
17
24
  /**
18
25
  * Pickscherm. De Logistiek Engine bepaalt welke documenten zichtbaar zijn en
19
26
  * in welke volgorde; dit scherm toont uitsluitend wat de wachtrij teruggeeft.
@@ -22,21 +29,28 @@ export function PickScherm({ station, onBack }) {
22
29
  const sessionId = getSessionId();
23
30
  const [lang, setLang] = useState(null);
24
31
  const [filters, setFilters] = useState(defaultFilters);
32
+ const [chosenDay, setChosenDay] = useState(null);
33
+ const [loadingDay, setLoadingDay] = useState(false);
25
34
  const [searchInput, setSearchInput] = useState("");
26
- const [scanInput, setScanInput] = useState("");
27
35
  const [busyId, setBusyId] = useState(null);
28
36
  const [pending, setPending] = useState(null);
37
+ const [cartPlan, setCartPlan] = useState(null);
38
+ const [planReason, setPlanReason] = useState(null);
39
+ const [activeCartId, setActiveCartId] = useState(null);
29
40
  const [lastPrinted, setLastPrinted] = useState(null);
30
41
  const [previewItem, setPreviewItem] = useState(null);
31
42
  const [emptyNotice, setEmptyNotice] = useState(null);
32
43
  const resetDone = useRef(null);
33
44
  const autoFetched = useRef(false);
34
45
  const retryAfterExpiry = useRef(false);
46
+ const daySelectionVersion = useRef(0);
35
47
  const { data: statuses } = usePickStatuses();
36
48
  const { data: documentTypes } = usePickDocumentTypes();
37
49
  const { online } = usePickHeartbeat(station.id, null, sessionId);
38
50
  const { data: printPlek, isLoading: plekLoading } = useStationPrintPlek(station.id);
39
51
  const { tracked, track, clear: clearJob } = usePrintJob();
52
+ const { batch, trackBatch, clearBatch } = usePrintBatch();
53
+ const finalizedBatch = useRef(null);
40
54
  const t = useMemo(() => getPickTranslator(lang ?? "nl"), [lang]);
41
55
  const jobTexts = useMemo(() => getPrintJobTexts(t.lang), [t]);
42
56
  const ui = useMemo(() => getPickUiTexts(t.lang), [t]);
@@ -60,28 +74,125 @@ export function PickScherm({ station, onBack }) {
60
74
  return () => clearTimeout(timer);
61
75
  }, [searchInput]);
62
76
  const effectiveFilters = useMemo(() => ({ ...filters, country: null, sortRules: [] }), [filters]);
63
- const { items, total, loading, lockedIds, refresh, pendingCount, printedCount, urgentCount, realtimeOk, } = usePickQueue({
77
+ const { items, total, loading, lockedIds, refresh, pendingCount, printedCount, urgentCount, realtimeOk, pickDay, rebuiltAt, } = usePickQueue({
64
78
  stationId: station.id,
65
79
  filters: effectiveFilters,
66
80
  enabled: lang !== null,
81
+ pickDayOverride: chosenDay,
67
82
  });
68
- const runPrint = useCallback(async (item) => {
69
- const updated = await pickApi.completePrint(item.id, sessionId);
70
- const jobId = updated?.print_job_id ?? null;
71
- await completeLogisticsTaskForItem(item, jobId);
72
- setLastPrinted(updated ?? item);
73
- if (jobId) {
74
- resetDone.current = null;
75
- track({
76
- jobId,
77
- queueItemId: item.id,
78
- documentNumber: (updated ?? item).document_number ?? null,
79
- });
83
+ const { data: pickDays } = usePickDays(lang !== null);
84
+ const safePickDays = Array.isArray(pickDays) ? pickDays : [];
85
+ const dayLabel = useCallback((day) => new Date(`${day}T00:00:00`).toLocaleDateString(t.lang === "en" ? "en-GB" : "nl-NL", {
86
+ weekday: "short",
87
+ day: "numeric",
88
+ month: "short",
89
+ }), [t.lang]);
90
+ /**
91
+ * De Engine heeft gemeld dat een buffer is gesloten of dat een order is
92
+ * losgekoppeld. De regels zijn opnieuw opgebouwd; de openstaande selectie en
93
+ * het oude karplan gaan weg zodat nooit een verouderde groepering blijft
94
+ * staan. Kar-id's, karnummers en barcodes van geprinte karren wijzigen niet.
95
+ */
96
+ useEffect(() => {
97
+ if (rebuiltAt === null)
98
+ return;
99
+ daySelectionVersion.current += 1;
100
+ setPending(null);
101
+ setCartPlan(null);
102
+ setActiveCartId(null);
103
+ autoFetched.current = false;
104
+ }, [rebuiltAt]);
105
+ /** Kiest een andere pickdag en laat de Engine die dag alvast klaarzetten. */
106
+ const chooseDay = useCallback(async (value) => {
107
+ const day = value === "current" ? null : value;
108
+ const previousPending = pending;
109
+ daySelectionVersion.current += 1;
110
+ setPending(null);
111
+ setCartPlan(null);
112
+ setActiveCartId(null);
113
+ setEmptyNotice(null);
114
+ autoFetched.current = false;
115
+ setChosenDay(day);
116
+ setFilters((f) => ({ ...f, page: 0 }));
117
+ if (previousPending) {
118
+ try {
119
+ await pickApi.releaseLock(previousPending.id);
120
+ }
121
+ catch {
122
+ // De opdracht was al geprint of de lock was al verlopen.
123
+ }
80
124
  }
81
- toast.success(`${item.document_number ?? t("document")} ${t("sentToPrinter")}`);
82
- refresh();
83
- }, [sessionId, refresh, t, track]);
84
- /** Mislukte printopdracht: document terugzetten zodat het weer in de wachtrij komt. */
125
+ if (!day)
126
+ return;
127
+ setLoadingDay(true);
128
+ try {
129
+ await pickApi.syncPickDay(day);
130
+ refresh();
131
+ toast.success(ui.pickDayLoaded);
132
+ }
133
+ catch (error) {
134
+ toast.error(error instanceof Error ? error.message : ui.genericError);
135
+ }
136
+ finally {
137
+ setLoadingDay(false);
138
+ }
139
+ }, [pending, refresh, ui]);
140
+ /**
141
+ * Verschuift de pickdag één dag vooruit of achteruit. Zonder handmatige keuze
142
+ * is de kalenderdag van vandaag het startpunt, niet de dag van de Engine.
143
+ */
144
+ const shiftDay = useCallback((delta) => {
145
+ const todayIso = new Date().toLocaleDateString("sv-SE", { timeZone: "Europe/Amsterdam" });
146
+ const base = chosenDay ?? todayIso;
147
+ const next = new Date(`${base}T00:00:00Z`);
148
+ next.setUTCDate(next.getUTCDate() + delta);
149
+ const iso = next.toISOString().slice(0, 10);
150
+ void chooseDay(iso === todayIso || (pickDay && iso === pickDay) ? "current" : iso);
151
+ }, [chosenDay, pickDay, chooseDay]);
152
+ /**
153
+ * Haalt het vastgelegde karplan uit de Engine. Heeft de order geen karren
154
+ * vastgelegd, dan is het antwoord null: het scherm valt dan terug op één
155
+ * document. De module verzint zelf nooit een karnummer of aantal.
156
+ */
157
+ const loadCartPlan = useCallback(async (item) => {
158
+ let plan = await pickApi.queueCartPlan(item.id);
159
+ if (!plan || plan.carts.length === 0) {
160
+ // Geen karplan: de Engine krijgt direct een seintje en er volgt één
161
+ // hercontrole. De module verzint zelf nog steeds geen karren.
162
+ try {
163
+ await pickApi.reportMissingCartPlans();
164
+ await new Promise((resolve) => setTimeout(resolve, 1500));
165
+ plan = await pickApi.queueCartPlan(item.id);
166
+ }
167
+ catch {
168
+ /* bewaking is aanvullend, nooit blokkerend */
169
+ }
170
+ }
171
+ if (!plan || plan.carts.length === 0) {
172
+ setCartPlan(null);
173
+ setActiveCartId(null);
174
+ // De reden komt letterlijk uit de Engine; deze module bedenkt niets.
175
+ try {
176
+ const ref = item.order_number ?? item.document_number;
177
+ const status = ref ? await pickApi.cartPlanStatus(ref) : null;
178
+ setPlanReason(status && (!status.ready || !status.released) ? status.reason ?? null : null);
179
+ }
180
+ catch {
181
+ setPlanReason(null);
182
+ }
183
+ return null;
184
+ }
185
+ setPlanReason(null);
186
+ setCartPlan(plan);
187
+ const printed = new Set(printedCartIdsOf(item));
188
+ setActiveCartId(plan.carts.find((cart) => !printed.has(cart.cartId))?.cartId ?? plan.carts[0].cartId);
189
+ return plan;
190
+ }, []);
191
+ /**
192
+ * Mislukte printopdracht: alleen de betreffende kar terugzetten. Ging alleen
193
+ * het opslaan van de PDF-kopie mis, dan is het papier wél geprint: de kar
194
+ * blijft dan geprint staan en er volgt enkel een waarschuwing.
195
+ */
85
196
  useEffect(() => {
86
197
  const job = tracked?.job;
87
198
  if (!tracked || !job)
@@ -91,9 +202,22 @@ export function PickScherm({ station, onBack }) {
91
202
  if (resetDone.current === tracked.jobId)
92
203
  return;
93
204
  resetDone.current = tracked.jobId;
205
+ if (isPdfOnlyFailure(job)) {
206
+ toast.warning(PDF_SAVE_NOTICE, {
207
+ description: pdfSaveWarning(job) ?? undefined,
208
+ });
209
+ refresh();
210
+ return;
211
+ }
94
212
  void (async () => {
95
213
  try {
96
- await pickApi.resetPrint(tracked.queueItemId, sessionId);
214
+ const restored = await pickApi.resetPrint(tracked.queueItemId, sessionId, tracked.cartId);
215
+ if (tracked.cartId) {
216
+ if (!cartPlan)
217
+ await loadCartPlan(restored);
218
+ setPending(restored);
219
+ setActiveCartId(tracked.cartId);
220
+ }
97
221
  toast.error(`${jobTexts.failed}: ${job.foutmelding ?? jobTexts.unknown}`);
98
222
  }
99
223
  catch (e) {
@@ -103,7 +227,24 @@ export function PickScherm({ station, onBack }) {
103
227
  refresh();
104
228
  }
105
229
  })();
106
- }, [tracked, sessionId, refresh, jobTexts]);
230
+ }, [tracked, sessionId, refresh, jobTexts, cartPlan, loadCartPlan]);
231
+ /**
232
+ * De print is gelukt, maar de PDF is niet in de map op de werkplek beland.
233
+ * Eén melding per printopdracht, met de tekst uit de database.
234
+ */
235
+ const pdfWarnedFor = useRef(null);
236
+ useEffect(() => {
237
+ const job = tracked?.job;
238
+ if (!tracked || !job)
239
+ return;
240
+ const warning = pdfSaveWarning(job);
241
+ if (!warning)
242
+ return;
243
+ if (pdfWarnedFor.current === tracked.jobId)
244
+ return;
245
+ pdfWarnedFor.current = tracked.jobId;
246
+ toast.warning(warning);
247
+ }, [tracked]);
107
248
  /**
108
249
  * Zet een printfout om in een begrijpelijke melding en meldt of de claim
109
250
  * verlopen was — dan kan het scherm zelf opnieuw ophalen.
@@ -123,7 +264,9 @@ export function PickScherm({ station, onBack }) {
123
264
  sessionId,
124
265
  timeout: station.lock_timeout_seconds ?? 120,
125
266
  });
126
- await runPrint(claimed ?? item);
267
+ const next = claimed ?? item;
268
+ await loadCartPlan(next);
269
+ setPending(next);
127
270
  }
128
271
  catch (e) {
129
272
  showPrintError(e);
@@ -138,7 +281,7 @@ export function PickScherm({ station, onBack }) {
138
281
  finally {
139
282
  setBusyId(null);
140
283
  }
141
- }, [station.id, station.lock_timeout_seconds, sessionId, runPrint, refresh, showPrintError]);
284
+ }, [station.id, station.lock_timeout_seconds, sessionId, loadCartPlan, refresh, showPrintError]);
142
285
  /**
143
286
  * Zet een nieuwe printopdracht in de wachtrij voor een al geprint document,
144
287
  * zonder de wachtrijstatus te wijzigen.
@@ -165,29 +308,166 @@ export function PickScherm({ station, onBack }) {
165
308
  setBusyId(null);
166
309
  }
167
310
  }, [t, sessionId, track, showPrintError]);
311
+ /** Open een geprinte order bovenaan zonder de wachtrijstatus te wijzigen. */
312
+ const selectPrintedForReprint = useCallback(async (item) => {
313
+ setBusyId(item.id);
314
+ try {
315
+ await loadCartPlan(item);
316
+ setPending(item);
317
+ setEmptyNotice(null);
318
+ window.scrollTo({ top: 0, behavior: "smooth" });
319
+ }
320
+ catch (e) {
321
+ showPrintError(e);
322
+ }
323
+ finally {
324
+ setBusyId(null);
325
+ }
326
+ }, [loadCartPlan, showPrintError]);
327
+ /**
328
+ * Printknop in de wachtrij: print in één keer alle pakbonnen (karren) van
329
+ * deze order. Nog niet geprint: claimen en alle karren in één batch.
330
+ * Al geprint: bewuste herdruk van alle karren.
331
+ * Zonder vastgelegd karplan wordt de pakbon als één document geprint.
332
+ */
333
+ const printAllForItem = useCallback(async (item) => {
334
+ setBusyId(item.id);
335
+ try {
336
+ if (item.printed_at) {
337
+ const plan = await loadCartPlan(item);
338
+ setPending(item);
339
+ if (!plan) {
340
+ toast.warning(NO_CART_PLAN_NOTICE);
341
+ await handleReprint(item);
342
+ refresh();
343
+ return;
344
+ }
345
+ const jobs = [];
346
+ for (const cart of plan.carts) {
347
+ const jobId = await pickApi.reprintCart(item.id, sessionId, cart.cartId);
348
+ if (jobId)
349
+ jobs.push({ cartId: cart.cartId, cartLabel: cart.cartLabel, jobId });
350
+ }
351
+ setLastPrinted(item);
352
+ if (jobs.length > 0) {
353
+ finalizedBatch.current = null;
354
+ trackBatch({
355
+ queueItemId: item.id,
356
+ documentNumber: item.document_number ?? null,
357
+ jobs,
358
+ });
359
+ }
360
+ toast.success(`${jobs.length} ${jobs.length === 1 ? "pakbon" : "pakbonnen"} ${t("sentToPrinter")}`);
361
+ refresh();
362
+ return;
363
+ }
364
+ const claimed = await pickApi.claim({
365
+ itemId: item.id,
366
+ stationId: station.id,
367
+ screenId: null,
368
+ sessionId,
369
+ timeout: station.lock_timeout_seconds ?? 120,
370
+ });
371
+ const target = claimed ?? item;
372
+ const plan = await loadCartPlan(target);
373
+ setPending(target);
374
+ if (!plan) {
375
+ toast.warning(NO_CART_PLAN_NOTICE);
376
+ const printed = await pickApi.completePrint(target.id, sessionId);
377
+ setLastPrinted(printed);
378
+ setPending(printed);
379
+ refresh();
380
+ return;
381
+ }
382
+ const already = new Set(printedCartIdsOf(target));
383
+ const todo = plan.carts.filter((cart) => !already.has(cart.cartId));
384
+ const result = await pickApi.completePrintCarts(target.id, sessionId, todo.map((cart) => cart.cartId));
385
+ setLastPrinted(result.item);
386
+ setPending(result.item);
387
+ finalizedBatch.current = null;
388
+ trackBatch({
389
+ queueItemId: target.id,
390
+ documentNumber: result.item.document_number ?? null,
391
+ jobs: result.jobs,
392
+ });
393
+ toast.success(`${result.jobs.length} ${result.jobs.length === 1 ? "pakbon" : "pakbonnen"} ${t("sentToPrinter")}`);
394
+ refresh();
395
+ }
396
+ catch (e) {
397
+ showPrintError(e);
398
+ if (!item.printed_at) {
399
+ try {
400
+ await pickApi.releaseLock(item.id);
401
+ }
402
+ catch {
403
+ // heartbeat ruimt de lock alsnog op
404
+ }
405
+ setPending(null);
406
+ setCartPlan(null);
407
+ setActiveCartId(null);
408
+ }
409
+ refresh();
410
+ }
411
+ finally {
412
+ setBusyId(null);
413
+ }
414
+ }, [
415
+ station.id,
416
+ station.lock_timeout_seconds,
417
+ sessionId,
418
+ loadCartPlan,
419
+ trackBatch,
420
+ refresh,
421
+ t,
422
+ showPrintError,
423
+ ]);
168
424
  const fetchNext = useCallback(async (silent = false) => {
425
+ const requestedDayVersion = daySelectionVersion.current;
169
426
  setBusyId("next");
427
+ let claimed = null;
170
428
  try {
171
- const claimed = await pickApi.claimNext({
429
+ claimed = await pickApi.claimNext({
172
430
  stationId: station.id,
173
431
  screenId: null,
174
432
  country: null,
175
433
  sessionId,
176
434
  timeout: station.lock_timeout_seconds ?? 120,
435
+ pickDay: chosenDay ?? pickDay ?? null,
177
436
  });
178
437
  if (!claimed)
179
438
  throw new Error(t("noDocument"));
439
+ if (requestedDayVersion !== daySelectionVersion.current) {
440
+ await pickApi.releaseLock(claimed.id);
441
+ return null;
442
+ }
443
+ await loadCartPlan(claimed);
444
+ if (requestedDayVersion !== daySelectionVersion.current) {
445
+ await pickApi.releaseLock(claimed.id);
446
+ setCartPlan(null);
447
+ setActiveCartId(null);
448
+ return null;
449
+ }
180
450
  setPending(claimed);
181
451
  setEmptyNotice(null);
182
452
  return claimed;
183
453
  }
184
454
  catch (e) {
455
+ if (claimed) {
456
+ try {
457
+ await pickApi.releaseLock(claimed.id);
458
+ }
459
+ catch {
460
+ // lock verloopt automatisch
461
+ }
462
+ }
185
463
  const { message } = friendlyPickError(e, t.lang);
186
464
  if (silent)
187
465
  setEmptyNotice(message);
188
466
  else
189
467
  toast.error(message);
190
468
  setPending(null);
469
+ setCartPlan(null);
470
+ setActiveCartId(null);
191
471
  if (!silent)
192
472
  refresh();
193
473
  return null;
@@ -195,18 +475,78 @@ export function PickScherm({ station, onBack }) {
195
475
  finally {
196
476
  setBusyId(null);
197
477
  }
198
- }, [station.id, station.lock_timeout_seconds, sessionId, refresh, t]);
199
- /** Bevestigt de opdracht, print deze en haalt direct de volgende op. */
478
+ }, [station.id, station.lock_timeout_seconds, sessionId, refresh, t, loadCartPlan, chosenDay, pickDay]);
479
+ /**
480
+ * Print alle nog niet geprinte karren (pakbonnen) van deze order in één keer
481
+ * en haalt daarna een nieuwe order op.
482
+ */
200
483
  const confirmPending = useCallback(async () => {
201
484
  const item = pending;
485
+ const plan = cartPlan;
202
486
  if (!item)
203
487
  return;
488
+ if (!plan) {
489
+ toast.warning(NO_CART_PLAN_NOTICE);
490
+ if (item.printed_at) {
491
+ await handleReprint(item);
492
+ return;
493
+ }
494
+ setBusyId("confirm");
495
+ try {
496
+ const printed = await pickApi.completePrint(item.id, sessionId);
497
+ setLastPrinted(printed);
498
+ setPending(printed);
499
+ refresh();
500
+ }
501
+ catch (e) {
502
+ showPrintError(e);
503
+ }
504
+ finally {
505
+ setBusyId(null);
506
+ }
507
+ return;
508
+ }
509
+ const alreadyPrinted = new Set(printedCartIdsOf(item));
510
+ const todo = plan.carts.filter((cart) => !alreadyPrinted.has(cart.cartId));
511
+ if (todo.length === 0) {
512
+ // Alles al geprint: bewuste herdruk van ALLE pakbonnen van deze order.
513
+ setBusyId("confirm");
514
+ try {
515
+ const jobs = [];
516
+ for (const cart of plan.carts) {
517
+ const jobId = await pickApi.reprintCart(item.id, sessionId, cart.cartId);
518
+ if (jobId)
519
+ jobs.push({ cartId: cart.cartId, cartLabel: cart.cartLabel, jobId });
520
+ }
521
+ setLastPrinted(item);
522
+ if (jobs.length > 0) {
523
+ finalizedBatch.current = null;
524
+ trackBatch({ queueItemId: item.id, documentNumber: item.document_number ?? null, jobs });
525
+ }
526
+ toast.success(`${jobs.length} ${jobs.length === 1 ? "pakbon" : "pakbonnen"} ${t("sentToPrinter")}`);
527
+ refresh();
528
+ }
529
+ catch (e) {
530
+ showPrintError(e);
531
+ }
532
+ finally {
533
+ setBusyId(null);
534
+ }
535
+ return;
536
+ }
204
537
  setBusyId("confirm");
205
538
  try {
206
- await runPrint(item);
207
- setPending(null);
208
- setBusyId(null);
209
- await fetchNext();
539
+ const result = await pickApi.completePrintCarts(item.id, sessionId, todo.map((cart) => cart.cartId));
540
+ setLastPrinted(result.item);
541
+ setPending(result.item);
542
+ finalizedBatch.current = null;
543
+ trackBatch({
544
+ queueItemId: item.id,
545
+ documentNumber: result.item.document_number ?? null,
546
+ jobs: result.jobs,
547
+ });
548
+ toast.success(`${result.jobs.length} ${result.jobs.length === 1 ? "pakbon" : "pakbonnen"} ${t("sentToPrinter")}`);
549
+ refresh();
210
550
  }
211
551
  catch (e) {
212
552
  const expired = showPrintError(e);
@@ -217,9 +557,10 @@ export function PickScherm({ station, onBack }) {
217
557
  // heartbeat ruimt de lock alsnog op
218
558
  }
219
559
  setPending(null);
560
+ setCartPlan(null);
561
+ setActiveCartId(null);
220
562
  refresh();
221
563
  setBusyId(null);
222
- // Verlopen claim: zelf een verse opdracht ophalen in plaats van blijven hangen.
223
564
  if (expired && !retryAfterExpiry.current) {
224
565
  retryAfterExpiry.current = true;
225
566
  try {
@@ -230,28 +571,97 @@ export function PickScherm({ station, onBack }) {
230
571
  }
231
572
  }
232
573
  }
233
- }, [pending, runPrint, fetchNext, refresh, showPrintError]);
234
- /** Scanner: barcode, documentnummer of ordernummer selecteert direct de regel. */
235
- const handleScan = useCallback(async (raw) => {
236
- const code = raw.trim();
237
- if (!code)
574
+ }, [pending, cartPlan, handleReprint, sessionId, trackBatch, t, fetchNext, refresh, showPrintError]);
575
+ /** Rond de order pas af wanneer alle batchopdrachten een eindstatus hebben. */
576
+ useEffect(() => {
577
+ if (!batch || batch.jobs.some((entry) => !entry.job || !["voltooid", "mislukt", "geannuleerd"].includes(entry.job.status)))
238
578
  return;
239
- setBusyId("scan");
240
- try {
241
- const found = await pickApi.findByCode(code, station.id);
242
- if (!found) {
243
- toast.error(ui.scanNotFound);
244
- return;
579
+ const batchKey = batch.jobs.map((entry) => entry.jobId).join(",");
580
+ if (finalizedBatch.current === batchKey)
581
+ return;
582
+ finalizedBatch.current = batchKey;
583
+ void (async () => {
584
+ const ended = batch.jobs.filter((entry) => entry.job?.status === "mislukt" || entry.job?.status === "geannuleerd");
585
+ // Alleen het opslaan van de PDF misging: het papier is geprint, dus de
586
+ // kar blijft geprint staan; we waarschuwen wel over de PDF-map.
587
+ const pdfOnly = ended.filter((entry) => isPdfOnlyFailure(entry.job));
588
+ if (pdfOnly.length > 0) {
589
+ toast.warning(PDF_SAVE_NOTICE, {
590
+ description: pdfSaveWarning(pdfOnly[0]?.job) ?? undefined,
591
+ });
245
592
  }
246
- setScanInput("");
247
- if (found.printed_at) {
248
- toast.error(ui.scanAlreadyPrinted);
249
- setFilters((f) => ({ ...f, search: code, page: 0 }));
250
- setSearchInput(code);
593
+ const failed = ended.filter((entry) => !isPdfOnlyFailure(entry.job));
594
+ if (failed.length > 0) {
595
+ const restored = await pickApi.resetPrintJobs(batch.queueItemId, sessionId, failed.map((entry) => ({ jobId: entry.jobId, cartId: entry.cartId })));
596
+ setPending(restored);
597
+ setActiveCartId(failed[0]?.cartId ?? null);
598
+ const firstError = failed[0]?.job?.foutmelding ?? jobTexts.unknown;
599
+ toast.error(`${failed.length} ${failed.length === 1 ? "pakbon is" : "pakbonnen zijn"} mislukt: ${firstError}`);
600
+ clearBatch();
601
+ setBusyId(null);
602
+ refresh();
251
603
  return;
252
604
  }
605
+ const item = pending;
606
+ if (item)
607
+ await completeLogisticsTaskForItem(item, batch.jobs[batch.jobs.length - 1]?.jobId ?? null);
608
+ clearBatch();
609
+ setPending(null);
610
+ setCartPlan(null);
611
+ setActiveCartId(null);
612
+ setBusyId(null);
613
+ refresh();
614
+ await fetchNext();
615
+ })().catch((error) => {
616
+ finalizedBatch.current = null;
617
+ setBusyId(null);
618
+ showPrintError(error);
619
+ });
620
+ }, [batch, sessionId, pending, jobTexts, clearBatch, refresh, fetchNext, showPrintError]);
621
+ /** Tik op een al geprinte kar: die pakbon nog een keer naar de printer sturen. */
622
+ const reprintCart = useCallback(async (item, cart) => {
623
+ setBusyId("confirm");
624
+ try {
625
+ const jobId = await pickApi.reprintCart(item.id, sessionId, cart.cartId);
626
+ setLastPrinted(item);
627
+ if (jobId) {
628
+ resetDone.current = jobId;
629
+ track({
630
+ jobId,
631
+ queueItemId: item.id,
632
+ documentNumber: item.document_number ?? null,
633
+ cartLabel: cart.cartLabel,
634
+ });
635
+ }
636
+ toast.success(`${cart.cartLabel} · ${item.document_number ?? t("document")} ${t("sentToPrinter")}`);
637
+ }
638
+ catch (e) {
639
+ showPrintError(e);
640
+ }
641
+ finally {
253
642
  setBusyId(null);
254
- await handlePick(found);
643
+ }
644
+ }, [sessionId, t, track, showPrintError]);
645
+ /** Eén losse pakbon (kar) printen vanaf de knop op de karregel. */
646
+ const printSingleCart = useCallback(async (item, cart) => {
647
+ const done = printedCartIdsOf(item).includes(cart.cartId);
648
+ if (done) {
649
+ await reprintCart(item, cart);
650
+ return;
651
+ }
652
+ setBusyId("confirm");
653
+ try {
654
+ const result = await pickApi.completePrintCarts(item.id, sessionId, [cart.cartId]);
655
+ setLastPrinted(result.item);
656
+ setPending(result.item);
657
+ finalizedBatch.current = null;
658
+ trackBatch({
659
+ queueItemId: item.id,
660
+ documentNumber: result.item.document_number ?? null,
661
+ jobs: result.jobs,
662
+ });
663
+ toast.success(`${cart.cartLabel} · ${t("sentToPrinter")}`);
664
+ refresh();
255
665
  }
256
666
  catch (e) {
257
667
  showPrintError(e);
@@ -259,10 +669,39 @@ export function PickScherm({ station, onBack }) {
259
669
  finally {
260
670
  setBusyId(null);
261
671
  }
262
- }, [station.id, ui, handlePick, showPrintError]);
672
+ }, [reprintCart, sessionId, trackBatch, t, refresh, showPrintError]);
673
+ /**
674
+ * Handmatig afmelden: het papier is geprint maar de regel bleef openstaan.
675
+ * Zet de order op "Geprint" zonder nieuwe printopdracht en logt de reden.
676
+ */
677
+ const markPrinted = useCallback(async (item) => {
678
+ const label = item.document_number ?? t("document");
679
+ if (typeof window !== "undefined" && !window.confirm(`${label} als geprint afmelden zonder opnieuw te printen?`))
680
+ return;
681
+ setBusyId(item.id);
682
+ try {
683
+ const marked = await pickApi.markPrinted(item.id, sessionId, "Handmatig afgemeld in het pickstation");
684
+ if (pending?.id === item.id) {
685
+ setPending(null);
686
+ setCartPlan(null);
687
+ setActiveCartId(null);
688
+ }
689
+ setLastPrinted(marked);
690
+ toast.success(`${label} staat nu op geprint.`);
691
+ }
692
+ catch (e) {
693
+ showPrintError(e);
694
+ }
695
+ finally {
696
+ setBusyId(null);
697
+ refresh();
698
+ }
699
+ }, [pending, sessionId, t, refresh, showPrintError]);
263
700
  const cancelPending = useCallback(async () => {
264
701
  const item = pending;
265
702
  setPending(null);
703
+ setCartPlan(null);
704
+ setActiveCartId(null);
266
705
  if (item) {
267
706
  try {
268
707
  await pickApi.releaseLock(item.id);
@@ -273,6 +712,28 @@ export function PickScherm({ station, onBack }) {
273
712
  }
274
713
  refresh();
275
714
  }, [pending, refresh]);
715
+ /** De pickdag die nu in beeld is; zonder dag mag er niets klaarstaan. */
716
+ const selectedDay = chosenDay ?? pickDay ?? null;
717
+ /**
718
+ * Een opdracht mag alleen bovenaan staan op zijn eigen pickdag. Hoort de
719
+ * klaargezette opdracht bij een andere dag, dan verdwijnt hij direct en geeft
720
+ * het scherm de lock terug.
721
+ */
722
+ useEffect(() => {
723
+ if (!pending)
724
+ return;
725
+ const payload = (pending.payload && typeof pending.payload === "object" ? pending.payload : {});
726
+ const itemDay = typeof payload["pickDate"] === "string" ? payload["pickDate"].slice(0, 10) : null;
727
+ if (!itemDay || !selectedDay || itemDay === selectedDay)
728
+ return;
729
+ const id = pending.id;
730
+ setPending(null);
731
+ setCartPlan(null);
732
+ setActiveCartId(null);
733
+ void pickApi.releaseLock(id).catch(() => {
734
+ // De lock verloopt automatisch.
735
+ });
736
+ }, [pending, selectedDay]);
276
737
  /** Zet automatisch de volgende opdracht klaar, zodat één klik op printen volstaat. */
277
738
  useEffect(() => {
278
739
  if (lang === null || loading)
@@ -281,11 +742,13 @@ export function PickScherm({ station, onBack }) {
281
742
  return;
282
743
  if (autoFetched.current)
283
744
  return;
745
+ if (!selectedDay)
746
+ return;
284
747
  if (pendingCount === 0)
285
748
  return;
286
749
  autoFetched.current = true;
287
750
  void fetchNext(true);
288
- }, [lang, loading, pending, busyId, pendingCount, fetchNext]);
751
+ }, [lang, loading, pending, busyId, pendingCount, fetchNext, selectedDay]);
289
752
  /** Pas opnieuw proberen zodra er echt weer iets te printen staat. */
290
753
  useEffect(() => {
291
754
  if (pendingCount === 0)
@@ -298,13 +761,31 @@ export function PickScherm({ station, onBack }) {
298
761
  const totalPages = Math.max(1, Math.ceil(total / filters.pageSize));
299
762
  const jobStatus = tracked?.job?.status ?? null;
300
763
  const jobFailed = jobStatus === "mislukt" || jobStatus === "geannuleerd";
301
- return (_jsxs("div", { className: "space-y-4 pb-24 md:pb-0", children: [_jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:flex-wrap md:items-center md:justify-between", children: [_jsxs("div", { className: "grid grid-cols-[auto_minmax(0,1fr)] items-center gap-3", children: [_jsxs(Button, { variant: "outline", size: "lg", className: "h-12 shrink-0 px-3 sm:px-4", onClick: () => setLang(null), "aria-label": t("changeLanguage"), children: [_jsx(Globe, { className: "h-5 w-5 sm:mr-2" }), _jsx("span", { className: "hidden sm:inline", children: t("changeLanguage") })] }), _jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "truncate text-lg font-bold text-foreground sm:text-xl", children: station.name }), _jsxs("p", { className: "truncate text-sm text-muted-foreground", children: [pendingCount, " ", t("statusToPrint"), " \u00B7 ", printedCount, " ", t("statusPrinted")] })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [!online && (_jsxs(Badge, { className: "bg-destructive text-destructive-foreground", children: [_jsx(WifiOff, { className: "mr-1 h-3.5 w-3.5" }), " ", t("offline")] })), _jsx(Button, { variant: "outline", size: "icon", className: "h-12 w-12 shrink-0", onClick: refresh, "aria-label": t("refresh"), children: _jsx(RefreshCw, { className: "h-4 w-4" }) }), _jsxs(Button, { size: "lg", className: "hidden h-14 px-8 text-lg md:inline-flex", onClick: () => (pending ? confirmPending() : fetchNext()), disabled: busyId !== null, children: [busyId !== null ? (_jsx(Loader2, { className: "mr-2 h-5 w-5 animate-spin" })) : (_jsx(Printer, { className: "mr-2 h-5 w-5" })), t("printNext"), pending?.document_number ? ` · ${pending.document_number}` : ""] })] })] }), !plekLoading && !printPlek && (_jsxs("div", { className: "flex items-start gap-2 rounded-md border-2 border-destructive/50 bg-destructive/5 p-3 text-sm text-destructive", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsx("span", { children: "Nog geen printplek gekoppeld \u2014 de opdracht blijft in de wachtrij staan." })] })), !pending && emptyNotice && (_jsx("p", { className: "text-sm text-muted-foreground", children: emptyNotice })), pending && (_jsx(Card, { className: "border-2 border-primary", children: _jsxs(CardContent, { className: "flex flex-col gap-4 p-4 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between sm:p-5", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-sm font-medium text-muted-foreground", children: t("confirmPrintTitle") }), _jsx("p", { className: "truncate text-xl font-bold text-foreground sm:text-2xl", children: pending.document_number ?? t("document") }), _jsx("p", { className: "truncate text-sm text-muted-foreground", children: [pending.order_number, pending.customer_name].filter(Boolean).join(" · ") })] }), _jsxs("div", { className: "flex flex-col gap-2 sm:flex-row", children: [_jsx(Button, { variant: "outline", size: "lg", className: "h-12 w-full sm:w-auto", onClick: cancelPending, disabled: busyId !== null, children: t("cancel") }), _jsxs(Button, { size: "lg", className: "h-14 w-full px-8 text-lg sm:w-auto", onClick: confirmPending, disabled: busyId !== null, children: [busyId === "confirm" ? (_jsx(Loader2, { className: "mr-2 h-5 w-5 animate-spin" })) : (_jsx(Printer, { className: "mr-2 h-5 w-5" })), t("print")] })] })] }) })), tracked && (_jsx(Card, { className: jobFailed ? "border-2 border-destructive" : "border-2 border-primary/40", children: _jsxs(CardContent, { className: "flex flex-col gap-4 p-4 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between", children: [_jsxs("div", { className: "flex min-w-0 items-start gap-3", children: [jobFailed ? (_jsx(AlertTriangle, { className: "h-6 w-6 shrink-0 text-destructive" })) : jobStatus === "voltooid" ? (_jsx(CheckCircle2, { className: "h-6 w-6 shrink-0 text-primary" })) : (_jsx(Loader2, { className: "h-6 w-6 shrink-0 animate-spin text-muted-foreground" })), _jsxs("div", { className: "min-w-0", children: [_jsxs("p", { className: "truncate text-sm font-medium text-muted-foreground", children: [jobTexts.jobTitle, " ", tracked.documentNumber ? `· ${tracked.documentNumber}` : ""] }), _jsx("p", { className: "text-lg font-semibold text-foreground", children: printJobStatusLabel(jobStatus, t.lang) }), tracked.job?.printer_naam && (_jsxs("p", { className: "truncate text-sm text-muted-foreground", children: [jobTexts.printerLabel, ": ", tracked.job.printer_naam] })), jobFailed && (_jsxs("p", { className: "text-sm text-destructive", children: [tracked.job?.foutmelding ?? jobTexts.unknown, " \u00B7 ", jobTexts.requeued] }))] })] }), _jsxs("div", { className: "flex flex-col gap-2 sm:flex-row", children: [jobFailed && lastPrinted && (_jsxs(Button, { size: "lg", className: "h-12 w-full sm:w-auto", onClick: () => {
764
+ const batchCompleted = batch?.jobs.filter((entry) => entry.job?.status === "voltooid").length ?? 0;
765
+ const batchFailed = batch?.jobs.filter((entry) => entry.job?.status === "mislukt" || entry.job?.status === "geannuleerd").length ?? 0;
766
+ return (_jsxs("div", { className: "space-y-4 pb-24 md:pb-0", children: [_jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:flex-wrap md:items-center md:justify-between", children: [_jsxs("div", { className: "grid grid-cols-[auto_minmax(0,1fr)] items-center gap-3", children: [_jsxs(Button, { variant: "outline", size: "lg", className: "h-12 shrink-0 px-3 sm:px-4", onClick: () => setLang(null), "aria-label": t("changeLanguage"), children: [_jsx(Globe, { className: "h-5 w-5 sm:mr-2" }), _jsx("span", { className: "hidden sm:inline", children: t("changeLanguage") })] }), _jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "truncate text-lg font-bold text-foreground sm:text-xl", children: station.name }), _jsxs("p", { className: "truncate text-sm text-muted-foreground", children: [pendingCount, " ", t("statusToPrint"), " \u00B7 ", printedCount, " ", t("statusPrinted")] })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", size: "lg", className: "h-12 px-3", onClick: () => shiftDay(-1), disabled: loadingDay || !(chosenDay ?? pickDay), "aria-label": ui.pickDayPrev, children: [_jsx(ChevronLeft, { className: "h-5 w-5 sm:mr-1" }), _jsx("span", { className: "hidden sm:inline", children: ui.pickDayPrev })] }), _jsx("div", { className: "flex h-12 items-center rounded-md border bg-muted px-4 text-sm font-semibold text-foreground", children: chosenDay ? dayLabel(chosenDay) : ui.pickDayToday }), _jsxs(Button, { variant: "outline", size: "lg", className: "h-12 px-3", onClick: () => shiftDay(1), disabled: loadingDay || !(chosenDay ?? pickDay), "aria-label": ui.pickDayNext, children: [_jsx("span", { className: "hidden sm:inline", children: ui.pickDayNext }), _jsx(ChevronRight, { className: "h-5 w-5 sm:ml-1" })] }), chosenDay && (_jsx(Button, { variant: "secondary", size: "lg", className: "h-12 px-4", onClick: () => void chooseDay("current"), disabled: loadingDay, children: ui.pickDayBackToToday }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [!online && (_jsxs(Badge, { className: "bg-destructive text-destructive-foreground", children: [_jsx(WifiOff, { className: "mr-1 h-3.5 w-3.5" }), " ", t("offline")] })), _jsx(Button, { variant: "outline", size: "icon", className: "h-12 w-12 shrink-0", onClick: refresh, "aria-label": t("refresh"), children: _jsx(RefreshCw, { className: "h-4 w-4" }) }), _jsxs(Button, { size: "lg", className: "hidden h-14 px-8 text-lg md:inline-flex", onClick: () => (pending ? confirmPending() : fetchNext()), disabled: busyId !== null, children: [busyId !== null ? (_jsx(Loader2, { className: "mr-2 h-5 w-5 animate-spin" })) : (_jsx(Printer, { className: "mr-2 h-5 w-5" })), t("printNext"), pending?.document_number ? ` · ${pending.document_number}` : ""] })] })] }), !plekLoading && !printPlek && (_jsxs("div", { className: "flex items-start gap-2 rounded-md border-2 border-destructive/50 bg-destructive/5 p-3 text-sm text-destructive", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsx("span", { children: "Nog geen printplek gekoppeld \u2014 de opdracht blijft in de wachtrij staan." })] })), _jsx(ShippingNoteScan, { stationId: station.id }), !pending && emptyNotice && (_jsx("p", { className: "text-sm text-muted-foreground", children: emptyNotice })), pending && (_jsxs(Card, { className: "border-2 border-primary", children: [_jsxs(CardContent, { className: "flex flex-col gap-4 p-4 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between sm:p-5", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-sm font-medium text-muted-foreground", children: t("confirmPrintTitle") }), _jsx("p", { className: "truncate text-xl font-bold text-foreground sm:text-2xl", children: pending.document_number ?? t("document") }), _jsx("p", { className: "truncate text-sm text-muted-foreground", children: [pending.order_number, pending.customer_name].filter(Boolean).join(" · ") })] }), _jsxs("div", { className: "flex flex-col gap-2 sm:flex-row", children: [_jsx(Button, { variant: "outline", size: "lg", className: "h-12 w-full sm:w-auto", onClick: cancelPending, disabled: busyId !== null, children: t("cancel") }), _jsxs(Button, { size: "lg", className: "h-14 w-full px-8 text-lg sm:w-auto", onClick: confirmPending, disabled: busyId !== null, children: [busyId === "confirm" ? (_jsx(Loader2, { className: "mr-2 h-5 w-5 animate-spin" })) : (_jsx(Printer, { className: "mr-2 h-5 w-5" })), t("print")] })] })] }), !cartPlan && (_jsx(CardContent, { className: "border-t p-4 sm:p-5", children: _jsxs("div", { className: "flex items-start gap-2 rounded-md border bg-muted/30 p-3 text-sm text-muted-foreground", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsx("span", { children: planReason
767
+ ? `${planReason} De logistiek is automatisch op de hoogte gebracht.`
768
+ : "De karplanning voor deze order is nog niet klaar — de logistiek is automatisch op de hoogte gebracht. Zolang die ontbreekt, wordt de pakbon als één document geprint." })] }) })), cartPlan && (_jsx(CardContent, { className: "border-t p-4 sm:p-5", children: _jsxs("div", { className: "grid gap-3 lg:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]", children: [_jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-sm font-semibold text-foreground", children: "Karren van deze order" }), _jsx("div", { className: "max-h-72 space-y-2 overflow-y-auto rounded-md border p-2", children: cartPlan.carts.map((cart) => {
769
+ const done = printedCartIdsOf(pending).includes(cart.cartId);
770
+ const active = cart.cartId === activeCartId;
771
+ return (_jsxs("div", { role: "button", tabIndex: 0, onClick: () => setActiveCartId(cart.cartId), onKeyDown: (event) => {
772
+ if (event.key === "Enter" || event.key === " ") {
773
+ event.preventDefault();
774
+ setActiveCartId(cart.cartId);
775
+ }
776
+ }, className: "flex w-full cursor-pointer items-center justify-between gap-2 rounded-md border p-3 text-left transition hover:bg-muted/50 " +
777
+ (active ? "border-primary bg-primary/5" : ""), children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate text-sm font-semibold text-foreground", children: cart.cartLabel }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [cart.isFullCart ? "Volle kar" : "Halve kar", " · ", cart.items.length, " ", cart.items.length === 1 ? "product" : "producten", cart.isShared ? " · gedeelde kar" : "", done ? " · geprint" : ""] })] }), _jsxs(Button, { type: "button", variant: done ? "outline" : "secondary", size: "sm", disabled: busyId !== null, onClick: (event) => {
778
+ event.stopPropagation();
779
+ setActiveCartId(cart.cartId);
780
+ void printSingleCart(pending, cart);
781
+ }, children: [_jsx(Printer, { className: "mr-2 h-4 w-4" }), done ? "Opnieuw" : "Printen"] })] }, cart.cartId));
782
+ }) })] }), activeCartId && (() => {
783
+ const activeCart = cartPlan.carts.find((cart) => cart.cartId === activeCartId);
784
+ if (!activeCart)
785
+ return null;
786
+ return (_jsxs("div", { className: "rounded-md border bg-muted/20 p-4", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [_jsxs("div", { children: [_jsx("p", { className: "text-lg font-bold text-foreground", children: activeCart.cartLabel }), _jsxs("p", { className: "text-sm text-muted-foreground", children: [activeCart.isFullCart ? "Volle kar" : "Halve kar (deel van de order)", activeCart.isShared ? " · gedeeld" : ""] })] }), activeCart.barcode && _jsx(Badge, { variant: "outline", children: activeCart.barcode })] }), _jsx("div", { className: "mt-4 divide-y rounded-md border bg-background", children: activeCart.items.length > 0 ? activeCart.items.map((cartItem, index) => (_jsxs("div", { className: "flex items-center justify-between gap-3 p-3", children: [_jsx("span", { className: "min-w-0 truncate text-sm text-foreground", children: cartItem.productName }), _jsx("span", { className: "shrink-0 text-sm font-semibold text-foreground", children: cartItem.quantity ?? cartItem.crates ?? 0 })] }, cartItem.itemId ?? cartItem.orderItemId ?? `${activeCart.cartId}-${index}`))) : (_jsx("p", { className: "p-3 text-sm text-muted-foreground", children: "Geen regels voor deze kar." })) })] }));
787
+ })()] }) }))] })), batch && (_jsx(Card, { className: batchFailed > 0 ? "border-2 border-destructive" : "border-2 border-primary/40", children: _jsxs(CardContent, { className: "flex items-center gap-3 p-4", children: [batchCompleted + batchFailed === batch.jobs.length ? (batchFailed > 0 ? _jsx(AlertTriangle, { className: "h-6 w-6 shrink-0 text-destructive" }) : _jsx(CheckCircle2, { className: "h-6 w-6 shrink-0 text-primary" })) : (_jsx(Loader2, { className: "h-6 w-6 shrink-0 animate-spin text-muted-foreground" })), _jsxs("div", { className: "min-w-0", children: [_jsxs("p", { className: "truncate text-sm text-muted-foreground", children: [jobTexts.jobTitle, batch.documentNumber ? ` · ${batch.documentNumber}` : ""] }), _jsxs("p", { className: "font-semibold text-foreground", children: [batchCompleted, " van ", batch.jobs.length, " pakbonnen voltooid", batchFailed > 0 ? ` · ${batchFailed} mislukt` : ""] })] })] }) })), tracked && (_jsx(Card, { className: jobFailed ? "border-2 border-destructive" : "border-2 border-primary/40", children: _jsxs(CardContent, { className: "flex flex-col gap-4 p-4 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between", children: [_jsxs("div", { className: "flex min-w-0 items-start gap-3", children: [jobFailed ? (_jsx(AlertTriangle, { className: "h-6 w-6 shrink-0 text-destructive" })) : jobStatus === "voltooid" ? (_jsx(CheckCircle2, { className: "h-6 w-6 shrink-0 text-primary" })) : (_jsx(Loader2, { className: "h-6 w-6 shrink-0 animate-spin text-muted-foreground" })), _jsxs("div", { className: "min-w-0", children: [_jsxs("p", { className: "truncate text-sm font-medium text-muted-foreground", children: [jobTexts.jobTitle, " ", tracked.documentNumber ? `· ${tracked.documentNumber}` : "", tracked.cartLabel ? ` · ${tracked.cartLabel}` : ""] }), _jsx("p", { className: "text-lg font-semibold text-foreground", children: printJobStatusLabel(jobStatus, t.lang) }), tracked.job?.printer_naam && (_jsxs("p", { className: "truncate text-sm text-muted-foreground", children: [jobTexts.printerLabel, ": ", tracked.job.printer_naam] })), jobFailed && (_jsxs("p", { className: "text-sm text-destructive", children: [tracked.job?.foutmelding ?? jobTexts.unknown, " \u00B7 ", jobTexts.requeued] })), !jobFailed && pdfSaveWarning(tracked.job) && (_jsx("p", { className: "text-sm font-medium text-destructive", children: pdfSaveWarning(tracked.job) }))] })] }), _jsxs("div", { className: "flex flex-col gap-2 sm:flex-row", children: [jobFailed && lastPrinted && (_jsxs(Button, { size: "lg", className: "h-12 w-full sm:w-auto", onClick: () => {
302
788
  clearJob();
303
- void handlePick(lastPrinted);
304
- }, disabled: busyId !== null, children: [_jsx(Printer, { className: "mr-2 h-5 w-5" }), " ", jobTexts.retry] })), _jsx(Button, { variant: "outline", size: "lg", className: "h-12 w-full sm:w-auto", onClick: clearJob, children: jobTexts.close })] })] }) })), _jsx(Card, { children: _jsxs(CardContent, { className: "flex flex-wrap items-center gap-3 p-3 sm:p-4", children: [_jsxs("div", { className: "relative w-full min-w-0 flex-1 sm:min-w-64", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { className: "h-12 pl-9 text-base", placeholder: t("search"), value: searchInput, onChange: (e) => setSearchInput(e.target.value) })] }), _jsxs("div", { className: "relative w-full min-w-0 sm:w-56", children: [_jsx(ScanLine, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { className: "h-12 pl-9 text-base", placeholder: ui.scanPlaceholder, value: scanInput, onChange: (e) => setScanInput(e.target.value), onKeyDown: (e) => {
305
- if (e.key !== "Enter")
306
- return;
307
- e.preventDefault();
308
- void handleScan(scanInput);
309
- } })] }), _jsxs(Select, { value: filters.documentTypeId ?? "all", onValueChange: (v) => setFilters((f) => ({ ...f, documentTypeId: v === "all" ? null : v, page: 0 })), children: [_jsx(SelectTrigger, { className: "h-12 w-full sm:w-52", children: _jsx(SelectValue, { placeholder: ui.allDocumentTypes }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: ui.allDocumentTypes }), (Array.isArray(documentTypes) ? documentTypes : []).map((dt) => (_jsx(SelectItem, { value: dt.id, children: dt.name }, dt.id)))] })] }), _jsx(Button, { variant: filters.onlyPending ? "default" : "outline", className: "h-12", onClick: () => setFilters((f) => ({ ...f, onlyPending: !f.onlyPending, page: 0 })), children: ui.onlyPending }), _jsxs(Button, { variant: filters.urgentOnly ? "default" : "outline", className: "h-12", onClick: () => setFilters((f) => ({ ...f, urgentOnly: !f.urgentOnly, page: 0 })), children: [_jsx(Zap, { className: "mr-2 h-4 w-4" }), ui.onlyUrgent, urgentCount > 0 ? ` (${urgentCount})` : ""] })] }) }), !realtimeOk && (_jsx("p", { className: "text-sm text-muted-foreground", children: ui.connectionLost })), loading ? (_jsxs("div", { className: "flex items-center justify-center py-16 text-muted-foreground", children: [_jsx(Loader2, { className: "mr-2 h-5 w-5 animate-spin" }), " ", t("loadingQueue")] })) : (_jsx(QueueTable, { items: items, lockedIds: lockedIds, statuses: Array.isArray(statuses) ? statuses : [], documentTypes: Array.isArray(documentTypes) ? documentTypes : [], busyId: busyId, onPick: handlePick, onReprint: handleReprint, onPreview: setPreviewItem, t: t })), _jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("span", { className: "text-center text-sm text-muted-foreground sm:text-left", children: [t("page"), " ", filters.page + 1, " ", t("of"), " ", totalPages] }), _jsxs("div", { className: "grid grid-cols-2 gap-2 sm:flex", children: [_jsx(Button, { variant: "outline", className: "h-12 w-full sm:w-auto", disabled: filters.page === 0, onClick: () => setFilters((f) => ({ ...f, page: Math.max(0, f.page - 1) })), children: t("previous") }), _jsx(Button, { variant: "outline", className: "h-12 w-full sm:w-auto", disabled: filters.page + 1 >= totalPages, onClick: () => setFilters((f) => ({ ...f, page: f.page + 1 })), children: t("next") })] })] }), _jsx("div", { className: "fixed inset-x-0 bottom-0 z-40 border-t bg-background/95 p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] backdrop-blur md:hidden", children: _jsxs(Button, { size: "lg", className: "h-14 w-full text-lg", onClick: () => (pending ? confirmPending() : fetchNext()), disabled: busyId !== null, children: [busyId !== null ? (_jsx(Loader2, { className: "mr-2 h-5 w-5 animate-spin" })) : (_jsx(Printer, { className: "mr-2 h-5 w-5" })), _jsxs("span", { className: "truncate", children: [t("printNext"), pending?.document_number ? ` · ${pending.document_number}` : ""] })] }) }), _jsx(PreviewDialog, { item: previewItem, stationSlug: station.slug, onClose: () => setPreviewItem(null) })] }));
789
+ void confirmPending();
790
+ }, disabled: busyId !== null || !pending, children: [_jsx(Printer, { className: "mr-2 h-5 w-5" }), " ", jobTexts.retry] })), _jsx(Button, { variant: "outline", size: "lg", className: "h-12 w-full sm:w-auto", onClick: clearJob, children: jobTexts.close })] })] }) })), _jsx(Card, { children: _jsxs(CardContent, { className: "flex flex-wrap items-center gap-3 p-3 sm:p-4", children: [_jsxs("div", { className: "relative w-full min-w-0 flex-1 sm:min-w-64", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { className: "h-12 pl-9 text-base", placeholder: t("search"), value: searchInput, onChange: (e) => setSearchInput(e.target.value) })] }), _jsxs(Select, { value: chosenDay ?? "current", onValueChange: (v) => void chooseDay(v), children: [_jsx(SelectTrigger, { className: "h-12 w-full sm:w-56", disabled: loadingDay, children: _jsx(SelectValue, { placeholder: ui.pickDay }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "current", children: ui.pickDayToday }), safePickDays.map((d) => (_jsxs(SelectItem, { value: d.day, children: [dayLabel(d.day), d.orders > 0 ? ` (${d.orders})` : ""] }, d.day)))] })] }), _jsxs(Select, { value: filters.documentTypeId ?? "all", onValueChange: (v) => setFilters((f) => ({ ...f, documentTypeId: v === "all" ? null : v, page: 0 })), children: [_jsx(SelectTrigger, { className: "h-12 w-full sm:w-52", children: _jsx(SelectValue, { placeholder: ui.allDocumentTypes }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: ui.allDocumentTypes }), (Array.isArray(documentTypes) ? documentTypes : []).map((dt) => (_jsx(SelectItem, { value: dt.id, children: dt.name }, dt.id)))] })] }), _jsx(Button, { variant: filters.onlyPending ? "default" : "outline", className: "h-12", onClick: () => setFilters((f) => ({ ...f, onlyPending: !f.onlyPending, page: 0 })), children: ui.onlyPending }), _jsxs(Button, { variant: filters.urgentOnly ? "default" : "outline", className: "h-12", onClick: () => setFilters((f) => ({ ...f, urgentOnly: !f.urgentOnly, page: 0 })), children: [_jsx(Zap, { className: "mr-2 h-4 w-4" }), ui.onlyUrgent, urgentCount > 0 ? ` (${urgentCount})` : ""] })] }) }), !realtimeOk && (_jsx("p", { className: "text-sm text-muted-foreground", children: ui.connectionLost })), loading ? (_jsxs("div", { className: "flex items-center justify-center py-16 text-muted-foreground", children: [_jsx(Loader2, { className: "mr-2 h-5 w-5 animate-spin" }), " ", t("loadingQueue")] })) : (_jsx(QueueTable, { items: items, lockedIds: lockedIds, statuses: Array.isArray(statuses) ? statuses : [], documentTypes: Array.isArray(documentTypes) ? documentTypes : [], busyId: busyId, onPick: handlePick, onReprint: handleReprint, onPrintAll: printAllForItem, onSelectPrinted: selectPrintedForReprint, onMarkPrinted: markPrinted, onPreview: setPreviewItem, t: t })), _jsxs("div", { className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("span", { className: "text-center text-sm text-muted-foreground sm:text-left", children: [t("page"), " ", filters.page + 1, " ", t("of"), " ", totalPages] }), _jsxs("div", { className: "grid grid-cols-2 gap-2 sm:flex", children: [_jsx(Button, { variant: "outline", className: "h-12 w-full sm:w-auto", disabled: filters.page === 0, onClick: () => setFilters((f) => ({ ...f, page: Math.max(0, f.page - 1) })), children: t("previous") }), _jsx(Button, { variant: "outline", className: "h-12 w-full sm:w-auto", disabled: filters.page + 1 >= totalPages, onClick: () => setFilters((f) => ({ ...f, page: f.page + 1 })), children: t("next") })] })] }), _jsx("div", { className: "fixed inset-x-0 bottom-0 z-40 border-t bg-background/95 p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] backdrop-blur md:hidden", children: _jsxs(Button, { size: "lg", className: "h-14 w-full text-lg", onClick: () => (pending ? confirmPending() : fetchNext()), disabled: busyId !== null, children: [busyId !== null ? (_jsx(Loader2, { className: "mr-2 h-5 w-5 animate-spin" })) : (_jsx(Printer, { className: "mr-2 h-5 w-5" })), _jsxs("span", { className: "truncate", children: [t("printNext"), pending?.document_number ? ` · ${pending.document_number}` : ""] })] }) }), _jsx(PreviewDialog, { item: previewItem, stationSlug: station.slug, onClose: () => setPreviewItem(null) })] }));
310
791
  }