@flowselections/pick-schermen-kwekers 1.0.17 → 1.0.18

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 (45) hide show
  1. package/dist-lib/components/pick/PickScherm.d.ts +1 -1
  2. package/dist-lib/components/pick/PickScherm.d.ts.map +1 -1
  3. package/dist-lib/components/pick/PickScherm.js +513 -60
  4. package/dist-lib/components/pick/PreviewDialog.d.ts.map +1 -1
  5. package/dist-lib/components/pick/PreviewDialog.js +11 -2
  6. package/dist-lib/components/pick/QueueTable.d.ts +4 -1
  7. package/dist-lib/components/pick/QueueTable.d.ts.map +1 -1
  8. package/dist-lib/components/pick/QueueTable.js +70 -10
  9. package/dist-lib/components/pick/ShippingNoteScan.d.ts +15 -0
  10. package/dist-lib/components/pick/ShippingNoteScan.d.ts.map +1 -0
  11. package/dist-lib/components/pick/ShippingNoteScan.js +89 -0
  12. package/dist-lib/hooks/usePickQueue.d.ts +7 -0
  13. package/dist-lib/hooks/usePickQueue.d.ts.map +1 -1
  14. package/dist-lib/hooks/usePickQueue.js +20 -2
  15. package/dist-lib/hooks/usePrintJob.d.ts +23 -0
  16. package/dist-lib/hooks/usePrintJob.d.ts.map +1 -1
  17. package/dist-lib/hooks/usePrintJob.js +83 -1
  18. package/dist-lib/index.d.ts +2 -0
  19. package/dist-lib/index.d.ts.map +1 -1
  20. package/dist-lib/index.js +2 -0
  21. package/dist-lib/integrations/supabase/auth-middleware.d.ts +3649 -1219
  22. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  23. package/dist-lib/integrations/supabase/client.d.ts +3649 -1219
  24. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  25. package/dist-lib/integrations/supabase/client.server.d.ts +3649 -1219
  26. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  27. package/dist-lib/integrations/supabase/types.d.ts +3704 -1248
  28. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  29. package/dist-lib/integrations/supabase/types.js +1 -0
  30. package/dist-lib/lib/pickApi.d.ts +61 -3
  31. package/dist-lib/lib/pickApi.d.ts.map +1 -1
  32. package/dist-lib/lib/pickApi.js +169 -2
  33. package/dist-lib/lib/pickPreview.d.ts +9 -4
  34. package/dist-lib/lib/pickPreview.d.ts.map +1 -1
  35. package/dist-lib/lib/pickPreview.js +41 -80
  36. package/dist-lib/lib/pickTypes.d.ts +82 -1
  37. package/dist-lib/lib/pickTypes.d.ts.map +1 -1
  38. package/dist-lib/lib/pickTypes.js +213 -1
  39. package/dist-lib/lib/pickUiText.d.ts +7 -0
  40. package/dist-lib/lib/pickUiText.d.ts.map +1 -1
  41. package/dist-lib/lib/pickUiText.js +14 -0
  42. package/dist-lib/lib/shippingNote.d.ts +41 -0
  43. package/dist-lib/lib/shippingNote.d.ts.map +1 -0
  44. package/dist-lib/lib/shippingNote.js +77 -0
  45. 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,27 @@ 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 [activeCartId, setActiveCartId] = useState(null);
29
39
  const [lastPrinted, setLastPrinted] = useState(null);
30
40
  const [previewItem, setPreviewItem] = useState(null);
31
41
  const [emptyNotice, setEmptyNotice] = useState(null);
32
42
  const resetDone = useRef(null);
33
43
  const autoFetched = useRef(false);
34
44
  const retryAfterExpiry = useRef(false);
45
+ const daySelectionVersion = useRef(0);
35
46
  const { data: statuses } = usePickStatuses();
36
47
  const { data: documentTypes } = usePickDocumentTypes();
37
48
  const { online } = usePickHeartbeat(station.id, null, sessionId);
38
49
  const { data: printPlek, isLoading: plekLoading } = useStationPrintPlek(station.id);
39
50
  const { tracked, track, clear: clearJob } = usePrintJob();
51
+ const { batch, trackBatch, clearBatch } = usePrintBatch();
52
+ const finalizedBatch = useRef(null);
40
53
  const t = useMemo(() => getPickTranslator(lang ?? "nl"), [lang]);
41
54
  const jobTexts = useMemo(() => getPrintJobTexts(t.lang), [t]);
42
55
  const ui = useMemo(() => getPickUiTexts(t.lang), [t]);
@@ -60,28 +73,100 @@ export function PickScherm({ station, onBack }) {
60
73
  return () => clearTimeout(timer);
61
74
  }, [searchInput]);
62
75
  const effectiveFilters = useMemo(() => ({ ...filters, country: null, sortRules: [] }), [filters]);
63
- const { items, total, loading, lockedIds, refresh, pendingCount, printedCount, urgentCount, realtimeOk, } = usePickQueue({
76
+ const { items, total, loading, lockedIds, refresh, pendingCount, printedCount, urgentCount, realtimeOk, pickDay, } = usePickQueue({
64
77
  stationId: station.id,
65
78
  filters: effectiveFilters,
66
79
  enabled: lang !== null,
80
+ pickDayOverride: chosenDay,
67
81
  });
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
- });
82
+ const { data: pickDays } = usePickDays(lang !== null);
83
+ const safePickDays = Array.isArray(pickDays) ? pickDays : [];
84
+ const dayLabel = useCallback((day) => new Date(`${day}T00:00:00`).toLocaleDateString(t.lang === "en" ? "en-GB" : "nl-NL", {
85
+ weekday: "short",
86
+ day: "numeric",
87
+ month: "short",
88
+ }), [t.lang]);
89
+ /** Kiest een andere pickdag en laat de Engine die dag alvast klaarzetten. */
90
+ const chooseDay = useCallback(async (value) => {
91
+ const day = value === "current" ? null : value;
92
+ const previousPending = pending;
93
+ daySelectionVersion.current += 1;
94
+ setPending(null);
95
+ setCartPlan(null);
96
+ setActiveCartId(null);
97
+ setEmptyNotice(null);
98
+ autoFetched.current = false;
99
+ setChosenDay(day);
100
+ setFilters((f) => ({ ...f, page: 0 }));
101
+ if (previousPending) {
102
+ try {
103
+ await pickApi.releaseLock(previousPending.id);
104
+ }
105
+ catch {
106
+ // De opdracht was al geprint of de lock was al verlopen.
107
+ }
80
108
  }
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. */
109
+ if (!day)
110
+ return;
111
+ setLoadingDay(true);
112
+ try {
113
+ await pickApi.syncPickDay(day);
114
+ refresh();
115
+ toast.success(ui.pickDayLoaded);
116
+ }
117
+ catch (error) {
118
+ toast.error(error instanceof Error ? error.message : ui.genericError);
119
+ }
120
+ finally {
121
+ setLoadingDay(false);
122
+ }
123
+ }, [pending, refresh, ui]);
124
+ /**
125
+ * Verschuift de pickdag één dag vooruit of achteruit. Zonder handmatige keuze
126
+ * is de kalenderdag van vandaag het startpunt, niet de dag van de Engine.
127
+ */
128
+ const shiftDay = useCallback((delta) => {
129
+ const todayIso = new Date().toLocaleDateString("sv-SE", { timeZone: "Europe/Amsterdam" });
130
+ const base = chosenDay ?? todayIso;
131
+ const next = new Date(`${base}T00:00:00Z`);
132
+ next.setUTCDate(next.getUTCDate() + delta);
133
+ const iso = next.toISOString().slice(0, 10);
134
+ void chooseDay(iso === todayIso || (pickDay && iso === pickDay) ? "current" : iso);
135
+ }, [chosenDay, pickDay, chooseDay]);
136
+ /**
137
+ * Haalt het vastgelegde karplan uit de Engine. Heeft de order geen karren
138
+ * vastgelegd, dan is het antwoord null: het scherm valt dan terug op één
139
+ * document. De module verzint zelf nooit een karnummer of aantal.
140
+ */
141
+ const loadCartPlan = useCallback(async (item) => {
142
+ let plan = await pickApi.queueCartPlan(item.id);
143
+ if (!plan || plan.carts.length === 0) {
144
+ // Geen karplan: de Engine krijgt direct een seintje en er volgt één
145
+ // hercontrole. De module verzint zelf nog steeds geen karren.
146
+ try {
147
+ await pickApi.reportMissingCartPlans();
148
+ await new Promise((resolve) => setTimeout(resolve, 1500));
149
+ plan = await pickApi.queueCartPlan(item.id);
150
+ }
151
+ catch {
152
+ /* bewaking is aanvullend, nooit blokkerend */
153
+ }
154
+ }
155
+ if (!plan || plan.carts.length === 0) {
156
+ setCartPlan(null);
157
+ setActiveCartId(null);
158
+ return null;
159
+ }
160
+ setCartPlan(plan);
161
+ const printed = new Set(printedCartIdsOf(item));
162
+ setActiveCartId(plan.carts.find((cart) => !printed.has(cart.cartId))?.cartId ?? plan.carts[0].cartId);
163
+ return plan;
164
+ }, []);
165
+ /**
166
+ * Mislukte printopdracht: alleen de betreffende kar terugzetten. Ging alleen
167
+ * het opslaan van de PDF-kopie mis, dan is het papier wél geprint: de kar
168
+ * blijft dan geprint staan en er volgt enkel een waarschuwing.
169
+ */
85
170
  useEffect(() => {
86
171
  const job = tracked?.job;
87
172
  if (!tracked || !job)
@@ -91,9 +176,22 @@ export function PickScherm({ station, onBack }) {
91
176
  if (resetDone.current === tracked.jobId)
92
177
  return;
93
178
  resetDone.current = tracked.jobId;
179
+ if (isPdfOnlyFailure(job)) {
180
+ toast.warning(PDF_SAVE_NOTICE, {
181
+ description: pdfSaveWarning(job) ?? undefined,
182
+ });
183
+ refresh();
184
+ return;
185
+ }
94
186
  void (async () => {
95
187
  try {
96
- await pickApi.resetPrint(tracked.queueItemId, sessionId);
188
+ const restored = await pickApi.resetPrint(tracked.queueItemId, sessionId, tracked.cartId);
189
+ if (tracked.cartId) {
190
+ if (!cartPlan)
191
+ await loadCartPlan(restored);
192
+ setPending(restored);
193
+ setActiveCartId(tracked.cartId);
194
+ }
97
195
  toast.error(`${jobTexts.failed}: ${job.foutmelding ?? jobTexts.unknown}`);
98
196
  }
99
197
  catch (e) {
@@ -103,7 +201,24 @@ export function PickScherm({ station, onBack }) {
103
201
  refresh();
104
202
  }
105
203
  })();
106
- }, [tracked, sessionId, refresh, jobTexts]);
204
+ }, [tracked, sessionId, refresh, jobTexts, cartPlan, loadCartPlan]);
205
+ /**
206
+ * De print is gelukt, maar de PDF is niet in de map op de werkplek beland.
207
+ * Eén melding per printopdracht, met de tekst uit de database.
208
+ */
209
+ const pdfWarnedFor = useRef(null);
210
+ useEffect(() => {
211
+ const job = tracked?.job;
212
+ if (!tracked || !job)
213
+ return;
214
+ const warning = pdfSaveWarning(job);
215
+ if (!warning)
216
+ return;
217
+ if (pdfWarnedFor.current === tracked.jobId)
218
+ return;
219
+ pdfWarnedFor.current = tracked.jobId;
220
+ toast.warning(warning);
221
+ }, [tracked]);
107
222
  /**
108
223
  * Zet een printfout om in een begrijpelijke melding en meldt of de claim
109
224
  * verlopen was — dan kan het scherm zelf opnieuw ophalen.
@@ -123,7 +238,9 @@ export function PickScherm({ station, onBack }) {
123
238
  sessionId,
124
239
  timeout: station.lock_timeout_seconds ?? 120,
125
240
  });
126
- await runPrint(claimed ?? item);
241
+ const next = claimed ?? item;
242
+ await loadCartPlan(next);
243
+ setPending(next);
127
244
  }
128
245
  catch (e) {
129
246
  showPrintError(e);
@@ -138,7 +255,7 @@ export function PickScherm({ station, onBack }) {
138
255
  finally {
139
256
  setBusyId(null);
140
257
  }
141
- }, [station.id, station.lock_timeout_seconds, sessionId, runPrint, refresh, showPrintError]);
258
+ }, [station.id, station.lock_timeout_seconds, sessionId, loadCartPlan, refresh, showPrintError]);
142
259
  /**
143
260
  * Zet een nieuwe printopdracht in de wachtrij voor een al geprint document,
144
261
  * zonder de wachtrijstatus te wijzigen.
@@ -165,29 +282,166 @@ export function PickScherm({ station, onBack }) {
165
282
  setBusyId(null);
166
283
  }
167
284
  }, [t, sessionId, track, showPrintError]);
285
+ /** Open een geprinte order bovenaan zonder de wachtrijstatus te wijzigen. */
286
+ const selectPrintedForReprint = useCallback(async (item) => {
287
+ setBusyId(item.id);
288
+ try {
289
+ await loadCartPlan(item);
290
+ setPending(item);
291
+ setEmptyNotice(null);
292
+ window.scrollTo({ top: 0, behavior: "smooth" });
293
+ }
294
+ catch (e) {
295
+ showPrintError(e);
296
+ }
297
+ finally {
298
+ setBusyId(null);
299
+ }
300
+ }, [loadCartPlan, showPrintError]);
301
+ /**
302
+ * Printknop in de wachtrij: print in één keer alle pakbonnen (karren) van
303
+ * deze order. Nog niet geprint: claimen en alle karren in één batch.
304
+ * Al geprint: bewuste herdruk van alle karren.
305
+ * Zonder vastgelegd karplan wordt de pakbon als één document geprint.
306
+ */
307
+ const printAllForItem = useCallback(async (item) => {
308
+ setBusyId(item.id);
309
+ try {
310
+ if (item.printed_at) {
311
+ const plan = await loadCartPlan(item);
312
+ setPending(item);
313
+ if (!plan) {
314
+ toast.warning(NO_CART_PLAN_NOTICE);
315
+ await handleReprint(item);
316
+ refresh();
317
+ return;
318
+ }
319
+ const jobs = [];
320
+ for (const cart of plan.carts) {
321
+ const jobId = await pickApi.reprintCart(item.id, sessionId, cart.cartId);
322
+ if (jobId)
323
+ jobs.push({ cartId: cart.cartId, cartLabel: cart.cartLabel, jobId });
324
+ }
325
+ setLastPrinted(item);
326
+ if (jobs.length > 0) {
327
+ finalizedBatch.current = null;
328
+ trackBatch({
329
+ queueItemId: item.id,
330
+ documentNumber: item.document_number ?? null,
331
+ jobs,
332
+ });
333
+ }
334
+ toast.success(`${jobs.length} ${jobs.length === 1 ? "pakbon" : "pakbonnen"} ${t("sentToPrinter")}`);
335
+ refresh();
336
+ return;
337
+ }
338
+ const claimed = await pickApi.claim({
339
+ itemId: item.id,
340
+ stationId: station.id,
341
+ screenId: null,
342
+ sessionId,
343
+ timeout: station.lock_timeout_seconds ?? 120,
344
+ });
345
+ const target = claimed ?? item;
346
+ const plan = await loadCartPlan(target);
347
+ setPending(target);
348
+ if (!plan) {
349
+ toast.warning(NO_CART_PLAN_NOTICE);
350
+ const printed = await pickApi.completePrint(target.id, sessionId);
351
+ setLastPrinted(printed);
352
+ setPending(printed);
353
+ refresh();
354
+ return;
355
+ }
356
+ const already = new Set(printedCartIdsOf(target));
357
+ const todo = plan.carts.filter((cart) => !already.has(cart.cartId));
358
+ const result = await pickApi.completePrintCarts(target.id, sessionId, todo.map((cart) => cart.cartId));
359
+ setLastPrinted(result.item);
360
+ setPending(result.item);
361
+ finalizedBatch.current = null;
362
+ trackBatch({
363
+ queueItemId: target.id,
364
+ documentNumber: result.item.document_number ?? null,
365
+ jobs: result.jobs,
366
+ });
367
+ toast.success(`${result.jobs.length} ${result.jobs.length === 1 ? "pakbon" : "pakbonnen"} ${t("sentToPrinter")}`);
368
+ refresh();
369
+ }
370
+ catch (e) {
371
+ showPrintError(e);
372
+ if (!item.printed_at) {
373
+ try {
374
+ await pickApi.releaseLock(item.id);
375
+ }
376
+ catch {
377
+ // heartbeat ruimt de lock alsnog op
378
+ }
379
+ setPending(null);
380
+ setCartPlan(null);
381
+ setActiveCartId(null);
382
+ }
383
+ refresh();
384
+ }
385
+ finally {
386
+ setBusyId(null);
387
+ }
388
+ }, [
389
+ station.id,
390
+ station.lock_timeout_seconds,
391
+ sessionId,
392
+ loadCartPlan,
393
+ trackBatch,
394
+ refresh,
395
+ t,
396
+ showPrintError,
397
+ ]);
168
398
  const fetchNext = useCallback(async (silent = false) => {
399
+ const requestedDayVersion = daySelectionVersion.current;
169
400
  setBusyId("next");
401
+ let claimed = null;
170
402
  try {
171
- const claimed = await pickApi.claimNext({
403
+ claimed = await pickApi.claimNext({
172
404
  stationId: station.id,
173
405
  screenId: null,
174
406
  country: null,
175
407
  sessionId,
176
408
  timeout: station.lock_timeout_seconds ?? 120,
409
+ pickDay: chosenDay ?? pickDay ?? null,
177
410
  });
178
411
  if (!claimed)
179
412
  throw new Error(t("noDocument"));
413
+ if (requestedDayVersion !== daySelectionVersion.current) {
414
+ await pickApi.releaseLock(claimed.id);
415
+ return null;
416
+ }
417
+ await loadCartPlan(claimed);
418
+ if (requestedDayVersion !== daySelectionVersion.current) {
419
+ await pickApi.releaseLock(claimed.id);
420
+ setCartPlan(null);
421
+ setActiveCartId(null);
422
+ return null;
423
+ }
180
424
  setPending(claimed);
181
425
  setEmptyNotice(null);
182
426
  return claimed;
183
427
  }
184
428
  catch (e) {
429
+ if (claimed) {
430
+ try {
431
+ await pickApi.releaseLock(claimed.id);
432
+ }
433
+ catch {
434
+ // lock verloopt automatisch
435
+ }
436
+ }
185
437
  const { message } = friendlyPickError(e, t.lang);
186
438
  if (silent)
187
439
  setEmptyNotice(message);
188
440
  else
189
441
  toast.error(message);
190
442
  setPending(null);
443
+ setCartPlan(null);
444
+ setActiveCartId(null);
191
445
  if (!silent)
192
446
  refresh();
193
447
  return null;
@@ -195,18 +449,78 @@ export function PickScherm({ station, onBack }) {
195
449
  finally {
196
450
  setBusyId(null);
197
451
  }
198
- }, [station.id, station.lock_timeout_seconds, sessionId, refresh, t]);
199
- /** Bevestigt de opdracht, print deze en haalt direct de volgende op. */
452
+ }, [station.id, station.lock_timeout_seconds, sessionId, refresh, t, loadCartPlan, chosenDay, pickDay]);
453
+ /**
454
+ * Print alle nog niet geprinte karren (pakbonnen) van deze order in één keer
455
+ * en haalt daarna een nieuwe order op.
456
+ */
200
457
  const confirmPending = useCallback(async () => {
201
458
  const item = pending;
459
+ const plan = cartPlan;
202
460
  if (!item)
203
461
  return;
462
+ if (!plan) {
463
+ toast.warning(NO_CART_PLAN_NOTICE);
464
+ if (item.printed_at) {
465
+ await handleReprint(item);
466
+ return;
467
+ }
468
+ setBusyId("confirm");
469
+ try {
470
+ const printed = await pickApi.completePrint(item.id, sessionId);
471
+ setLastPrinted(printed);
472
+ setPending(printed);
473
+ refresh();
474
+ }
475
+ catch (e) {
476
+ showPrintError(e);
477
+ }
478
+ finally {
479
+ setBusyId(null);
480
+ }
481
+ return;
482
+ }
483
+ const alreadyPrinted = new Set(printedCartIdsOf(item));
484
+ const todo = plan.carts.filter((cart) => !alreadyPrinted.has(cart.cartId));
485
+ if (todo.length === 0) {
486
+ // Alles al geprint: bewuste herdruk van ALLE pakbonnen van deze order.
487
+ setBusyId("confirm");
488
+ try {
489
+ const jobs = [];
490
+ for (const cart of plan.carts) {
491
+ const jobId = await pickApi.reprintCart(item.id, sessionId, cart.cartId);
492
+ if (jobId)
493
+ jobs.push({ cartId: cart.cartId, cartLabel: cart.cartLabel, jobId });
494
+ }
495
+ setLastPrinted(item);
496
+ if (jobs.length > 0) {
497
+ finalizedBatch.current = null;
498
+ trackBatch({ queueItemId: item.id, documentNumber: item.document_number ?? null, jobs });
499
+ }
500
+ toast.success(`${jobs.length} ${jobs.length === 1 ? "pakbon" : "pakbonnen"} ${t("sentToPrinter")}`);
501
+ refresh();
502
+ }
503
+ catch (e) {
504
+ showPrintError(e);
505
+ }
506
+ finally {
507
+ setBusyId(null);
508
+ }
509
+ return;
510
+ }
204
511
  setBusyId("confirm");
205
512
  try {
206
- await runPrint(item);
207
- setPending(null);
208
- setBusyId(null);
209
- await fetchNext();
513
+ const result = await pickApi.completePrintCarts(item.id, sessionId, todo.map((cart) => cart.cartId));
514
+ setLastPrinted(result.item);
515
+ setPending(result.item);
516
+ finalizedBatch.current = null;
517
+ trackBatch({
518
+ queueItemId: item.id,
519
+ documentNumber: result.item.document_number ?? null,
520
+ jobs: result.jobs,
521
+ });
522
+ toast.success(`${result.jobs.length} ${result.jobs.length === 1 ? "pakbon" : "pakbonnen"} ${t("sentToPrinter")}`);
523
+ refresh();
210
524
  }
211
525
  catch (e) {
212
526
  const expired = showPrintError(e);
@@ -217,9 +531,10 @@ export function PickScherm({ station, onBack }) {
217
531
  // heartbeat ruimt de lock alsnog op
218
532
  }
219
533
  setPending(null);
534
+ setCartPlan(null);
535
+ setActiveCartId(null);
220
536
  refresh();
221
537
  setBusyId(null);
222
- // Verlopen claim: zelf een verse opdracht ophalen in plaats van blijven hangen.
223
538
  if (expired && !retryAfterExpiry.current) {
224
539
  retryAfterExpiry.current = true;
225
540
  try {
@@ -230,39 +545,137 @@ export function PickScherm({ station, onBack }) {
230
545
  }
231
546
  }
232
547
  }
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)
548
+ }, [pending, cartPlan, handleReprint, sessionId, trackBatch, t, fetchNext, refresh, showPrintError]);
549
+ /** Rond de order pas af wanneer alle batchopdrachten een eindstatus hebben. */
550
+ useEffect(() => {
551
+ if (!batch || batch.jobs.some((entry) => !entry.job || !["voltooid", "mislukt", "geannuleerd"].includes(entry.job.status)))
238
552
  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;
553
+ const batchKey = batch.jobs.map((entry) => entry.jobId).join(",");
554
+ if (finalizedBatch.current === batchKey)
555
+ return;
556
+ finalizedBatch.current = batchKey;
557
+ void (async () => {
558
+ const ended = batch.jobs.filter((entry) => entry.job?.status === "mislukt" || entry.job?.status === "geannuleerd");
559
+ // Alleen het opslaan van de PDF misging: het papier is geprint, dus de
560
+ // kar blijft geprint staan; we waarschuwen wel over de PDF-map.
561
+ const pdfOnly = ended.filter((entry) => isPdfOnlyFailure(entry.job));
562
+ if (pdfOnly.length > 0) {
563
+ toast.warning(PDF_SAVE_NOTICE, {
564
+ description: pdfSaveWarning(pdfOnly[0]?.job) ?? undefined,
565
+ });
245
566
  }
246
- setScanInput("");
247
- if (found.printed_at) {
248
- toast.error(ui.scanAlreadyPrinted);
249
- setFilters((f) => ({ ...f, search: code, page: 0 }));
250
- setSearchInput(code);
567
+ const failed = ended.filter((entry) => !isPdfOnlyFailure(entry.job));
568
+ if (failed.length > 0) {
569
+ const restored = await pickApi.resetPrintJobs(batch.queueItemId, sessionId, failed.map((entry) => ({ jobId: entry.jobId, cartId: entry.cartId })));
570
+ setPending(restored);
571
+ setActiveCartId(failed[0]?.cartId ?? null);
572
+ const firstError = failed[0]?.job?.foutmelding ?? jobTexts.unknown;
573
+ toast.error(`${failed.length} ${failed.length === 1 ? "pakbon is" : "pakbonnen zijn"} mislukt: ${firstError}`);
574
+ clearBatch();
575
+ setBusyId(null);
576
+ refresh();
251
577
  return;
252
578
  }
579
+ const item = pending;
580
+ if (item)
581
+ await completeLogisticsTaskForItem(item, batch.jobs[batch.jobs.length - 1]?.jobId ?? null);
582
+ clearBatch();
583
+ setPending(null);
584
+ setCartPlan(null);
585
+ setActiveCartId(null);
253
586
  setBusyId(null);
254
- await handlePick(found);
587
+ refresh();
588
+ await fetchNext();
589
+ })().catch((error) => {
590
+ finalizedBatch.current = null;
591
+ setBusyId(null);
592
+ showPrintError(error);
593
+ });
594
+ }, [batch, sessionId, pending, jobTexts, clearBatch, refresh, fetchNext, showPrintError]);
595
+ /** Tik op een al geprinte kar: die pakbon nog een keer naar de printer sturen. */
596
+ const reprintCart = useCallback(async (item, cart) => {
597
+ setBusyId("confirm");
598
+ try {
599
+ const jobId = await pickApi.reprintCart(item.id, sessionId, cart.cartId);
600
+ setLastPrinted(item);
601
+ if (jobId) {
602
+ resetDone.current = jobId;
603
+ track({
604
+ jobId,
605
+ queueItemId: item.id,
606
+ documentNumber: item.document_number ?? null,
607
+ cartLabel: cart.cartLabel,
608
+ });
609
+ }
610
+ toast.success(`${cart.cartLabel} · ${item.document_number ?? t("document")} ${t("sentToPrinter")}`);
611
+ }
612
+ catch (e) {
613
+ showPrintError(e);
614
+ }
615
+ finally {
616
+ setBusyId(null);
617
+ }
618
+ }, [sessionId, t, track, showPrintError]);
619
+ /** Eén losse pakbon (kar) printen vanaf de knop op de karregel. */
620
+ const printSingleCart = useCallback(async (item, cart) => {
621
+ const done = printedCartIdsOf(item).includes(cart.cartId);
622
+ if (done) {
623
+ await reprintCart(item, cart);
624
+ return;
625
+ }
626
+ setBusyId("confirm");
627
+ try {
628
+ const result = await pickApi.completePrintCarts(item.id, sessionId, [cart.cartId]);
629
+ setLastPrinted(result.item);
630
+ setPending(result.item);
631
+ finalizedBatch.current = null;
632
+ trackBatch({
633
+ queueItemId: item.id,
634
+ documentNumber: result.item.document_number ?? null,
635
+ jobs: result.jobs,
636
+ });
637
+ toast.success(`${cart.cartLabel} · ${t("sentToPrinter")}`);
638
+ refresh();
639
+ }
640
+ catch (e) {
641
+ showPrintError(e);
642
+ }
643
+ finally {
644
+ setBusyId(null);
645
+ }
646
+ }, [reprintCart, sessionId, trackBatch, t, refresh, showPrintError]);
647
+ /**
648
+ * Handmatig afmelden: het papier is geprint maar de regel bleef openstaan.
649
+ * Zet de order op "Geprint" zonder nieuwe printopdracht en logt de reden.
650
+ */
651
+ const markPrinted = useCallback(async (item) => {
652
+ const label = item.document_number ?? t("document");
653
+ if (typeof window !== "undefined" && !window.confirm(`${label} als geprint afmelden zonder opnieuw te printen?`))
654
+ return;
655
+ setBusyId(item.id);
656
+ try {
657
+ const marked = await pickApi.markPrinted(item.id, sessionId, "Handmatig afgemeld in het pickstation");
658
+ if (pending?.id === item.id) {
659
+ setPending(null);
660
+ setCartPlan(null);
661
+ setActiveCartId(null);
662
+ }
663
+ setLastPrinted(marked);
664
+ toast.success(`${label} staat nu op geprint.`);
255
665
  }
256
666
  catch (e) {
257
667
  showPrintError(e);
258
668
  }
259
669
  finally {
260
670
  setBusyId(null);
671
+ refresh();
261
672
  }
262
- }, [station.id, ui, handlePick, showPrintError]);
673
+ }, [pending, sessionId, t, refresh, showPrintError]);
263
674
  const cancelPending = useCallback(async () => {
264
675
  const item = pending;
265
676
  setPending(null);
677
+ setCartPlan(null);
678
+ setActiveCartId(null);
266
679
  if (item) {
267
680
  try {
268
681
  await pickApi.releaseLock(item.id);
@@ -273,6 +686,28 @@ export function PickScherm({ station, onBack }) {
273
686
  }
274
687
  refresh();
275
688
  }, [pending, refresh]);
689
+ /** De pickdag die nu in beeld is; zonder dag mag er niets klaarstaan. */
690
+ const selectedDay = chosenDay ?? pickDay ?? null;
691
+ /**
692
+ * Een opdracht mag alleen bovenaan staan op zijn eigen pickdag. Hoort de
693
+ * klaargezette opdracht bij een andere dag, dan verdwijnt hij direct en geeft
694
+ * het scherm de lock terug.
695
+ */
696
+ useEffect(() => {
697
+ if (!pending)
698
+ return;
699
+ const payload = (pending.payload && typeof pending.payload === "object" ? pending.payload : {});
700
+ const itemDay = typeof payload["pickDate"] === "string" ? payload["pickDate"].slice(0, 10) : null;
701
+ if (!itemDay || !selectedDay || itemDay === selectedDay)
702
+ return;
703
+ const id = pending.id;
704
+ setPending(null);
705
+ setCartPlan(null);
706
+ setActiveCartId(null);
707
+ void pickApi.releaseLock(id).catch(() => {
708
+ // De lock verloopt automatisch.
709
+ });
710
+ }, [pending, selectedDay]);
276
711
  /** Zet automatisch de volgende opdracht klaar, zodat één klik op printen volstaat. */
277
712
  useEffect(() => {
278
713
  if (lang === null || loading)
@@ -281,11 +716,13 @@ export function PickScherm({ station, onBack }) {
281
716
  return;
282
717
  if (autoFetched.current)
283
718
  return;
719
+ if (!selectedDay)
720
+ return;
284
721
  if (pendingCount === 0)
285
722
  return;
286
723
  autoFetched.current = true;
287
724
  void fetchNext(true);
288
- }, [lang, loading, pending, busyId, pendingCount, fetchNext]);
725
+ }, [lang, loading, pending, busyId, pendingCount, fetchNext, selectedDay]);
289
726
  /** Pas opnieuw proberen zodra er echt weer iets te printen staat. */
290
727
  useEffect(() => {
291
728
  if (pendingCount === 0)
@@ -298,13 +735,29 @@ export function PickScherm({ station, onBack }) {
298
735
  const totalPages = Math.max(1, Math.ceil(total / filters.pageSize));
299
736
  const jobStatus = tracked?.job?.status ?? null;
300
737
  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: () => {
738
+ const batchCompleted = batch?.jobs.filter((entry) => entry.job?.status === "voltooid").length ?? 0;
739
+ const batchFailed = batch?.jobs.filter((entry) => entry.job?.status === "mislukt" || entry.job?.status === "geannuleerd").length ?? 0;
740
+ 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: "De karplanning voor deze order is nog niet klaar \u2014 de logistiek is automatisch op de hoogte gebracht. Zolang die ontbreekt, wordt de pakbon als \u00E9\u00E9n 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) => {
741
+ const done = printedCartIdsOf(pending).includes(cart.cartId);
742
+ const active = cart.cartId === activeCartId;
743
+ return (_jsxs("div", { role: "button", tabIndex: 0, onClick: () => setActiveCartId(cart.cartId), onKeyDown: (event) => {
744
+ if (event.key === "Enter" || event.key === " ") {
745
+ event.preventDefault();
746
+ setActiveCartId(cart.cartId);
747
+ }
748
+ }, className: "flex w-full cursor-pointer items-center justify-between gap-2 rounded-md border p-3 text-left transition hover:bg-muted/50 " +
749
+ (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) => {
750
+ event.stopPropagation();
751
+ setActiveCartId(cart.cartId);
752
+ void printSingleCart(pending, cart);
753
+ }, children: [_jsx(Printer, { className: "mr-2 h-4 w-4" }), done ? "Opnieuw" : "Printen"] })] }, cart.cartId));
754
+ }) })] }), activeCartId && (() => {
755
+ const activeCart = cartPlan.carts.find((cart) => cart.cartId === activeCartId);
756
+ if (!activeCart)
757
+ return null;
758
+ 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." })) })] }));
759
+ })()] }) }))] })), 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
760
  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) })] }));
761
+ void confirmPending();
762
+ }, 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
763
  }