@flowselections/pick-schermen-kwekers 1.0.19 → 1.0.21

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 (35) hide show
  1. package/README.md +24 -24
  2. package/dist-lib/components/pick/PickScherm.d.ts.map +1 -1
  3. package/dist-lib/components/pick/PickScherm.js +322 -69
  4. package/dist-lib/components/pick/QueueTable.d.ts +6 -1
  5. package/dist-lib/components/pick/QueueTable.d.ts.map +1 -1
  6. package/dist-lib/components/pick/QueueTable.js +30 -22
  7. package/dist-lib/hooks/useShippingNoteStatus.d.ts +27 -0
  8. package/dist-lib/hooks/useShippingNoteStatus.d.ts.map +1 -0
  9. package/dist-lib/hooks/useShippingNoteStatus.js +132 -0
  10. package/dist-lib/index.d.ts +1 -0
  11. package/dist-lib/index.d.ts.map +1 -1
  12. package/dist-lib/index.js +1 -0
  13. package/dist-lib/integrations/supabase/auth-middleware.d.ts +740 -1
  14. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  15. package/dist-lib/integrations/supabase/client.d.ts +740 -1
  16. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  17. package/dist-lib/integrations/supabase/client.server.d.ts +740 -1
  18. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  19. package/dist-lib/integrations/supabase/types.d.ts +743 -2
  20. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  21. package/dist-lib/lib/layoutMaker/pakbonHtml.js +49 -49
  22. package/dist-lib/lib/layoutRenderer.js +17 -17
  23. package/dist-lib/lib/pickApi.d.ts +10 -0
  24. package/dist-lib/lib/pickApi.d.ts.map +1 -1
  25. package/dist-lib/lib/pickApi.js +21 -0
  26. package/dist-lib/lib/pickTypes.d.ts +59 -2
  27. package/dist-lib/lib/pickTypes.d.ts.map +1 -1
  28. package/dist-lib/lib/pickTypes.js +149 -24
  29. package/dist-lib/lib/shippingNoteStatus.d.ts +35 -0
  30. package/dist-lib/lib/shippingNoteStatus.d.ts.map +1 -0
  31. package/dist-lib/lib/shippingNoteStatus.js +118 -0
  32. package/package.json +2 -2
  33. package/dist-lib/components/TemplateModulePage.d.ts +0 -2
  34. package/dist-lib/components/TemplateModulePage.d.ts.map +0 -1
  35. package/dist-lib/components/TemplateModulePage.js +0 -44
package/README.md CHANGED
@@ -1,24 +1,24 @@
1
- # pick schermen kwekers
2
-
3
- blank project start
4
-
5
- This project was built with [Lovable](https://lovable.dev).
6
-
7
- ## Build with Lovable
8
-
9
- Continue developing this project in the [Lovable editor](https://lovable.dev/projects/f7462658-1d60-42f1-8e22-dbefe36f44ef).
10
-
11
- - **Ship faster**: describe what you want to build and Lovable handles the code.
12
- - **Stay in sync**: every change made in Lovable is committed straight to this repository.
13
- - **Full ownership**: this code is yours. Push to `main` on GitHub and your changes sync back into Lovable, ready for your next prompt.
14
-
15
- ## Development
16
-
17
- Prefer working locally? You need Node.js and npm — [install with nvm](https://github.com/nvm-sh/nvm#installing-and-updating).
18
-
19
- ```sh
20
- git clone <this-repository-url>
21
- cd <repository-name>
22
- npm i
23
- npm run dev
24
- ```
1
+ # pick schermen kwekers
2
+
3
+ blank project start
4
+
5
+ This project was built with [Lovable](https://lovable.dev).
6
+
7
+ ## Build with Lovable
8
+
9
+ Continue developing this project in the [Lovable editor](https://lovable.dev/projects/f7462658-1d60-42f1-8e22-dbefe36f44ef).
10
+
11
+ - **Ship faster**: describe what you want to build and Lovable handles the code.
12
+ - **Stay in sync**: every change made in Lovable is committed straight to this repository.
13
+ - **Full ownership**: this code is yours. Push to `main` on GitHub and your changes sync back into Lovable, ready for your next prompt.
14
+
15
+ ## Development
16
+
17
+ Prefer working locally? You need Node.js and npm — [install with nvm](https://github.com/nvm-sh/nvm#installing-and-updating).
18
+
19
+ ```sh
20
+ git clone <this-repository-url>
21
+ cd <repository-name>
22
+ npm i
23
+ npm run dev
24
+ ```
@@ -1 +1 @@
1
- {"version":3,"file":"PickScherm.d.ts","sourceRoot":"","sources":["../../../src/components/pick/PickScherm.tsx"],"names":[],"mappings":"AAkCA,OAAO,EAOL,KAAK,WAAW,EACjB,MAAM,qBAAqB,CAAC;AAmB7B,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAcD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,KAAK,+BAwwCpD"}
1
+ {"version":3,"file":"PickScherm.d.ts","sourceRoot":"","sources":["../../../src/components/pick/PickScherm.tsx"],"names":[],"mappings":"AAqCA,OAAO,EAUL,KAAK,WAAW,EACjB,MAAM,qBAAqB,CAAC;AA0B7B,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAeD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,KAAK,+BAgrDpD"}
@@ -1,26 +1,30 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { useQuery } from "@tanstack/react-query";
3
4
  import { Badge, Button, Card, CardContent, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, toast, } from "@flowselections/core";
4
- import { AlertTriangle, ArrowLeft, CheckCircle2, ChevronLeft, ChevronRight, Globe, Loader2, Printer, RefreshCw, Search, WifiOff, Zap, } from "lucide-react";
5
+ import { AlertTriangle, ArrowLeft, CheckCircle2, ChevronLeft, ChevronRight, FileDown, Globe, Loader2, Printer, RefreshCw, Search, WifiOff, Zap, } from "lucide-react";
5
6
  import { LanguageTiles } from "./LanguageTiles";
6
7
  import { QueueTable } from "./QueueTable";
7
8
  import { PreviewDialog } from "./PreviewDialog";
8
9
  import { ShippingNoteScan } from "./ShippingNoteScan";
9
10
  import { getSessionId, pickApi } from "../../lib/pickApi";
11
+ import { sendLogisticsEvent } from "../../lib/logisticsApi";
10
12
  import { completeLogisticsTaskForItem } from "../../lib/pickPrint";
11
- import { isPdfOnlyFailure, printedCartIdsOf, pdfSaveWarning, } from "../../lib/pickTypes";
13
+ import { isPdfOnlyFailure, pickReadiness, printedCartIdsOf, pdfSaveWarning, unitRefOf, unitTitleOf, } from "../../lib/pickTypes";
12
14
  import { getPickTranslator } from "../../lib/pickI18n";
13
15
  import { friendlyPickError, getPickUiTexts } from "../../lib/pickUiText";
14
16
  import { getPrintJobTexts, printJobStatusLabel } from "../../lib/pickPrintJobText";
15
17
  import { defaultFilters, usePickDays, usePickHeartbeat, usePickQueue, } from "../../hooks/usePickQueue";
16
18
  import { usePickDocumentTypes, usePickStatuses, useStationPrintPlek, } from "../../hooks/usePickConfig";
17
19
  import { usePrintBatch, usePrintJob } from "../../hooks/usePrintJob";
20
+ import { useShippingNoteStatus } from "../../hooks/useShippingNoteStatus";
21
+ import { isWaitingShippingNote, shippingNoteMessage, shippingNoteSourceLabel, } from "../../lib/shippingNoteStatus";
18
22
  const LANG_STORAGE_KEY = "pick.language";
19
23
  /** Melding als de planning voor deze order geen karren heeft vastgelegd. */
20
24
  /** Papier is geprint, maar de PDF-kopie op de werkplek is niet opgeslagen. */
21
25
  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.";
26
+ /** Melding als de regel nog niet compleet is aangeleverd door de bron. */
27
+ const NOT_READY_NOTICE = "Deze pakbon is nog niet printklaar. Klik op \"Pakbon ophalen\" om de ontbrekende gegevens bij de bron op te vragen.";
24
28
  /**
25
29
  * Pickscherm. De Logistiek Engine bepaalt welke documenten zichtbaar zijn en
26
30
  * in welke volgorde; dit scherm toont uitsluitend wat de wachtrij teruggeeft.
@@ -36,6 +40,14 @@ export function PickScherm({ station, onBack }) {
36
40
  const [pending, setPending] = useState(null);
37
41
  const [cartPlan, setCartPlan] = useState(null);
38
42
  const [planReason, setPlanReason] = useState(null);
43
+ const [planStatus, setPlanStatus] = useState(null);
44
+ const [fetchingData, setFetchingData] = useState(false);
45
+ const [readinessById, setReadinessById] = useState({});
46
+ /** Voortgang van de pakboncontrole: hoeveel van de zichtbare regels zijn gecontroleerd. */
47
+ const [checkProgress, setCheckProgress] = useState({
48
+ done: 0,
49
+ total: 0,
50
+ });
39
51
  const [activeCartId, setActiveCartId] = useState(null);
40
52
  const [lastPrinted, setLastPrinted] = useState(null);
41
53
  const [previewItem, setPreviewItem] = useState(null);
@@ -48,9 +60,62 @@ export function PickScherm({ station, onBack }) {
48
60
  const { data: documentTypes } = usePickDocumentTypes();
49
61
  const { online } = usePickHeartbeat(station.id, null, sessionId);
50
62
  const { data: printPlek, isLoading: plekLoading } = useStationPrintPlek(station.id);
51
- const { tracked, track, clear: clearJob } = usePrintJob();
52
- const { batch, trackBatch, clearBatch } = usePrintBatch();
63
+ const { tracked, track: trackJobRaw, clear: clearJob } = usePrintJob();
64
+ const { batch, trackBatch: trackBatchRaw, clearBatch } = usePrintBatch();
65
+ const { shippingNotes, watchShippingNotes, clearShippingNotes, refreshShippingNotesNow, } = useShippingNoteStatus();
53
66
  const finalizedBatch = useRef(null);
67
+ const [fetchingNotes, setFetchingNotes] = useState(false);
68
+ /**
69
+ * Handmatig alle openstaande verzendbrieven laten ophalen. Eén aanroep per
70
+ * klik; idempotent en alleen-lezen richting Floriday.
71
+ */
72
+ const fetchShippingNotes = useCallback(async () => {
73
+ if (fetchingNotes)
74
+ return;
75
+ setFetchingNotes(true);
76
+ try {
77
+ const result = await refreshShippingNotesNow();
78
+ if (result.ok) {
79
+ toast.success(result.message ?? "Verzendbrieven worden opgehaald.");
80
+ }
81
+ else {
82
+ toast.info("Ophalen kon nu niet gestart worden — de verwerking gaat zelf verder.");
83
+ }
84
+ }
85
+ finally {
86
+ setFetchingNotes(false);
87
+ }
88
+ }, [fetchingNotes, refreshShippingNotesNow]);
89
+ /**
90
+ * Elke geslaagde pakbonprint start ook de statuscontrole van de verzendbrief
91
+ * bij dezelfde kar. Het teruggegeven print_job-id is de sleutel; opvragen is
92
+ * alleen-lezen en maakt nooit een nieuwe aanvraag of printopdracht.
93
+ */
94
+ const track = useCallback((args) => {
95
+ trackJobRaw(args);
96
+ watchShippingNotes([
97
+ { jobId: args.jobId, cartId: args.cartId ?? null, cartLabel: args.cartLabel ?? null },
98
+ ]);
99
+ }, [trackJobRaw, watchShippingNotes]);
100
+ const trackBatch = useCallback((args) => {
101
+ trackBatchRaw(args);
102
+ watchShippingNotes(args.jobs);
103
+ }, [trackBatchRaw, watchShippingNotes]);
104
+ /**
105
+ * Ingestelde werkwijze van de Logistiek Engine. cartsPerPrint bepaalt hoeveel
106
+ * karren de batchknop in één klik print. Is er geen actieve werkwijze (null),
107
+ * dan komt er geen knop; dit scherm valt nooit terug op een eigen aantal.
108
+ */
109
+ const { data: pickModeInfo } = useQuery({
110
+ queryKey: ["logistics-pick-mode"],
111
+ queryFn: () => pickApi.pickMode(),
112
+ refetchInterval: 30000,
113
+ staleTime: 15000,
114
+ });
115
+ const cartsPerPrint = pickModeInfo?.cartsPerPrint && pickModeInfo.cartsPerPrint > 1
116
+ ? pickModeInfo.cartsPerPrint
117
+ : null;
118
+ const [batchPrinting, setBatchPrinting] = useState(false);
54
119
  const t = useMemo(() => getPickTranslator(lang ?? "nl"), [lang]);
55
120
  const jobTexts = useMemo(() => getPrintJobTexts(t.lang), [t]);
56
121
  const ui = useMemo(() => getPickUiTexts(t.lang), [t]);
@@ -150,44 +215,149 @@ export function PickScherm({ station, onBack }) {
150
215
  void chooseDay(iso === todayIso || (pickDay && iso === pickDay) ? "current" : iso);
151
216
  }, [chosenDay, pickDay, chooseDay]);
152
217
  /**
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.
218
+ * Haalt het vastgelegde karplan én de toestand van de planning uit de bron.
219
+ * Er wordt niets verzonnen: is het plan er niet, of is het nog niet
220
+ * vrijgegeven, dan blijft de regel "wacht op gegevens".
156
221
  */
157
222
  const loadCartPlan = useCallback(async (item) => {
158
- let plan = await pickApi.queueCartPlan(item.id);
223
+ const plan = await pickApi.queueCartPlan(item.id);
224
+ const ref = unitRefOf(item);
225
+ let status = null;
226
+ try {
227
+ status = ref ? await pickApi.cartPlanStatus(ref) : null;
228
+ }
229
+ catch {
230
+ status = null;
231
+ }
232
+ setPlanStatus(status);
233
+ const readiness = pickReadiness(plan, status, item);
234
+ setPlanReason(readiness.ready ? null : readiness.reason);
159
235
  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.
236
+ setCartPlan(null);
237
+ setActiveCartId(null);
238
+ return { plan: null, readiness };
239
+ }
240
+ setCartPlan(plan);
241
+ const printed = new Set(printedCartIdsOf(item));
242
+ setActiveCartId(plan.carts.find((cart) => !printed.has(cart.cartId))?.cartId ?? plan.carts[0].cartId);
243
+ return { plan, readiness };
244
+ }, []);
245
+ /** Printklaar volgens de bron: plan aanwezig, vrijgegeven en met barcodes. */
246
+ const readiness = useMemo(() => pickReadiness(cartPlan, planStatus, pending), [cartPlan, planStatus, pending]);
247
+ /**
248
+ * Controleert iedere zichtbare regel rechtstreeks bij de Engine. Tijdens de
249
+ * controle blijft printen dicht; alleen een actuele ready+released-status en
250
+ * een volledig karplan met barcodes geven de printknop vrij.
251
+ */
252
+ const visibleIdsKey = useMemo(() => (Array.isArray(items) ? items.map((item) => item.id).join(",") : ""), [items]);
253
+ const itemsRef = useRef([]);
254
+ itemsRef.current = Array.isArray(items) ? items : [];
255
+ useEffect(() => {
256
+ let cancelled = false;
257
+ const verifyVisibleRows = async () => {
258
+ const visibleItems = itemsRef.current;
259
+ setCheckProgress({ done: 0, total: visibleItems.length });
260
+ if (visibleItems.length === 0)
261
+ return;
262
+ let done = 0;
263
+ await Promise.all(visibleItems.map(async (item) => {
264
+ let entry = {
265
+ checking: false,
266
+ ready: false,
267
+ reason: "De bron kon de pakbon nog niet bevestigen.",
268
+ };
269
+ try {
270
+ const [plan, status] = await Promise.all([
271
+ pickApi.queueCartPlan(item.id),
272
+ pickApi.cartPlanStatus(unitRefOf(item)),
273
+ ]);
274
+ const state = pickReadiness(plan, status, item);
275
+ entry = { checking: false, ready: state.ready, reason: state.reason };
276
+ }
277
+ catch {
278
+ // fail closed: de regel blijft "Pakbon ophalen"
279
+ }
280
+ if (cancelled)
281
+ return;
282
+ done += 1;
283
+ setReadinessById((prev) => ({ ...prev, [item.id]: entry }));
284
+ setCheckProgress({ done, total: visibleItems.length });
285
+ }));
286
+ };
287
+ void verifyVisibleRows();
288
+ const timer = window.setInterval(() => void verifyVisibleRows(), 15000);
289
+ return () => {
290
+ cancelled = true;
291
+ window.clearInterval(timer);
292
+ };
293
+ }, [visibleIdsKey]);
294
+ /**
295
+ * Vraagt de ontbrekende gegevens op bij de module die ze moet leveren en
296
+ * blijft daarna kijken tot de regel printklaar is. Deze module maakt of
297
+ * wijzigt zelf nooit karren, nummers of barcodes.
298
+ */
299
+ const requestData = useCallback(async (item) => {
300
+ setFetchingData(true);
301
+ const ref = unitRefOf(item);
302
+ try {
162
303
  try {
163
304
  await pickApi.reportMissingCartPlans();
164
- await new Promise((resolve) => setTimeout(resolve, 1500));
165
- plan = await pickApi.queueCartPlan(item.id);
166
305
  }
167
306
  catch {
168
307
  /* bewaking is aanvullend, nooit blokkerend */
169
308
  }
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);
309
+ if (ref) {
310
+ try {
311
+ await sendLogisticsEvent({
312
+ event_type: "order.changed",
313
+ order_ref: ref,
314
+ shadow: false,
315
+ payload: {
316
+ reason: "pick.data_incomplete",
317
+ missing: pickReadiness(cartPlan, planStatus, item).missing,
318
+ queue_item_id: item.id,
319
+ },
320
+ });
321
+ }
322
+ catch {
323
+ /* het seintje is aanvullend op de bewaking */
324
+ }
179
325
  }
180
- catch {
181
- setPlanReason(null);
326
+ for (let attempt = 0; attempt < 20; attempt += 1) {
327
+ await new Promise((resolve) => setTimeout(resolve, 3000));
328
+ const plan = await pickApi.queueCartPlan(item.id);
329
+ let status = null;
330
+ try {
331
+ status = ref ? await pickApi.cartPlanStatus(ref) : null;
332
+ }
333
+ catch {
334
+ status = null;
335
+ }
336
+ const next = pickReadiness(plan, status, item);
337
+ setPlanStatus(status);
338
+ setPlanReason(next.ready ? null : next.reason);
339
+ if (plan && plan.carts.length > 0) {
340
+ setCartPlan(plan);
341
+ const printed = new Set(printedCartIdsOf(item));
342
+ setActiveCartId(plan.carts.find((cart) => !printed.has(cart.cartId))?.cartId ?? plan.carts[0].cartId);
343
+ }
344
+ else {
345
+ setCartPlan(null);
346
+ setActiveCartId(null);
347
+ }
348
+ if (next.ready) {
349
+ toast.success("De pakbon is opgehaald — je kunt nu printen.");
350
+ refresh();
351
+ return true;
352
+ }
182
353
  }
183
- return null;
354
+ toast.warning(planReason ?? "De pakbon is nog niet compleet. De bron is op de hoogte gebracht.");
355
+ return false;
184
356
  }
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
- }, []);
357
+ finally {
358
+ setFetchingData(false);
359
+ }
360
+ }, [cartPlan, planStatus, planReason, refresh]);
191
361
  /**
192
362
  * Mislukte printopdracht: alleen de betreffende kar terugzetten. Ging alleen
193
363
  * het opslaan van de PDF-kopie mis, dan is het papier wél geprint: de kar
@@ -328,18 +498,19 @@ export function PickScherm({ station, onBack }) {
328
498
  * Printknop in de wachtrij: print in één keer alle pakbonnen (karren) van
329
499
  * deze order. Nog niet geprint: claimen en alle karren in één batch.
330
500
  * Al geprint: bewuste herdruk van alle karren.
331
- * Zonder vastgelegd karplan wordt de pakbon als één document geprint.
501
+ * Is de regel nog niet printklaar, dan wordt er niet geprint maar worden de
502
+ * ontbrekende gegevens bij de bron opgevraagd.
332
503
  */
333
504
  const printAllForItem = useCallback(async (item) => {
334
505
  setBusyId(item.id);
335
506
  try {
336
507
  if (item.printed_at) {
337
- const plan = await loadCartPlan(item);
508
+ const { plan, readiness: state } = await loadCartPlan(item);
338
509
  setPending(item);
339
- if (!plan) {
340
- toast.warning(NO_CART_PLAN_NOTICE);
341
- await handleReprint(item);
342
- refresh();
510
+ if (!plan || !state.ready) {
511
+ setBusyId(null);
512
+ toast.warning(state.reason ?? NOT_READY_NOTICE);
513
+ await requestData(item);
343
514
  return;
344
515
  }
345
516
  const jobs = [];
@@ -361,6 +532,17 @@ export function PickScherm({ station, onBack }) {
361
532
  refresh();
362
533
  return;
363
534
  }
535
+ // Ook bij een rij die zojuist als klaar is getoond, direct vóór het
536
+ // claimen opnieuw bij de Engine controleren. Een verouderde rij kan zo
537
+ // nooit alsnog een printopdracht starten.
538
+ const preflight = await loadCartPlan(item);
539
+ setPending(item);
540
+ if (!preflight.plan || !preflight.readiness.ready) {
541
+ setBusyId(null);
542
+ toast.warning(preflight.readiness.reason ?? NOT_READY_NOTICE);
543
+ await requestData(item);
544
+ return;
545
+ }
364
546
  const claimed = await pickApi.claim({
365
547
  itemId: item.id,
366
548
  stationId: station.id,
@@ -369,14 +551,12 @@ export function PickScherm({ station, onBack }) {
369
551
  timeout: station.lock_timeout_seconds ?? 120,
370
552
  });
371
553
  const target = claimed ?? item;
372
- const plan = await loadCartPlan(target);
554
+ const { plan, readiness: state } = await loadCartPlan(target);
373
555
  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();
556
+ if (!plan || !state.ready) {
557
+ setBusyId(null);
558
+ toast.warning(state.reason ?? NOT_READY_NOTICE);
559
+ await requestData(target);
380
560
  return;
381
561
  }
382
562
  const already = new Set(printedCartIdsOf(target));
@@ -416,11 +596,69 @@ export function PickScherm({ station, onBack }) {
416
596
  station.lock_timeout_seconds,
417
597
  sessionId,
418
598
  loadCartPlan,
599
+ requestData,
419
600
  trackBatch,
420
601
  refresh,
421
602
  t,
422
603
  showPrintError,
423
604
  ]);
605
+ /**
606
+ * Batchknop: print de bovenste n printklare regels in de bestaande volgorde
607
+ * (volle karren voor deelkarren, zoals de lijst sorteert). Per regel draait
608
+ * exact dezelfde printactie als de knop op die regel; karren van een eenheid
609
+ * die in deze klik al geprint zijn, worden overgeslagen zodat elke kar
610
+ * precies één printactie krijgt. Niet-printklare karren worden overgeslagen
611
+ * met een leesbare melding per kar.
612
+ */
613
+ const printNextCarts = useCallback(async () => {
614
+ if (!cartsPerPrint || batchPrinting)
615
+ return;
616
+ setBatchPrinting(true);
617
+ try {
618
+ const visible = (Array.isArray(items) ? items : []).filter((item) => !item.printed_at);
619
+ const targets = [];
620
+ const skipped = [];
621
+ for (const item of visible) {
622
+ if (targets.length >= cartsPerPrint)
623
+ break;
624
+ const state = readinessById[item.id];
625
+ if (!state || state.checking) {
626
+ skipped.push({ title: unitTitleOf(item), reason: "de pakbon wordt nog gecontroleerd" });
627
+ continue;
628
+ }
629
+ if (!state.ready) {
630
+ skipped.push({ title: unitTitleOf(item), reason: state.reason ?? "nog niet printklaar" });
631
+ continue;
632
+ }
633
+ targets.push(item);
634
+ }
635
+ if (targets.length === 0) {
636
+ toast.warning("Er is geen enkele kar printklaar.");
637
+ for (const s of skipped.slice(0, 5))
638
+ toast.warning(`${s.title}: ${s.reason}`);
639
+ return;
640
+ }
641
+ if (targets.length < cartsPerPrint) {
642
+ toast.info(`Er zijn maar ${targets.length} van de ${cartsPerPrint} karren printklaar; alleen die worden geprint.`);
643
+ }
644
+ for (const s of skipped.slice(0, 5))
645
+ toast.warning(`${s.title}: ${s.reason}`);
646
+ const doneUnits = new Set();
647
+ for (const item of targets) {
648
+ const unitKey = `${item.document_type_id ?? "-"}|${item.order_number ?? unitRefOf(item)}`;
649
+ if (doneUnits.has(unitKey))
650
+ continue;
651
+ await printAllForItem(item);
652
+ doneUnits.add(unitKey);
653
+ }
654
+ }
655
+ catch (e) {
656
+ showPrintError(e);
657
+ }
658
+ finally {
659
+ setBatchPrinting(false);
660
+ }
661
+ }, [cartsPerPrint, batchPrinting, items, readinessById, printAllForItem, showPrintError]);
424
662
  const fetchNext = useCallback(async (silent = false) => {
425
663
  const requestedDayVersion = daySelectionVersion.current;
426
664
  setBusyId("next");
@@ -485,25 +723,10 @@ export function PickScherm({ station, onBack }) {
485
723
  const plan = cartPlan;
486
724
  if (!item)
487
725
  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
- }
726
+ if (!plan || !readiness.ready) {
727
+ // Niet printklaar: nooit printen, altijd eerst de gegevens opvragen.
728
+ toast.warning(readiness.reason ?? NOT_READY_NOTICE);
729
+ await requestData(item);
507
730
  return;
508
731
  }
509
732
  const alreadyPrinted = new Set(printedCartIdsOf(item));
@@ -571,7 +794,7 @@ export function PickScherm({ station, onBack }) {
571
794
  }
572
795
  }
573
796
  }
574
- }, [pending, cartPlan, handleReprint, sessionId, trackBatch, t, fetchNext, refresh, showPrintError]);
797
+ }, [pending, cartPlan, readiness, requestData, handleReprint, sessionId, trackBatch, t, fetchNext, refresh, showPrintError]);
575
798
  /** Rond de order pas af wanneer alle batchopdrachten een eindstatus hebben. */
576
799
  useEffect(() => {
577
800
  if (!batch || batch.jobs.some((entry) => !entry.job || !["voltooid", "mislukt", "geannuleerd"].includes(entry.job.status)))
@@ -763,9 +986,26 @@ export function PickScherm({ station, onBack }) {
763
986
  const jobFailed = jobStatus === "mislukt" || jobStatus === "geannuleerd";
764
987
  const batchCompleted = batch?.jobs.filter((entry) => entry.job?.status === "voltooid").length ?? 0;
765
988
  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) => {
989
+ /**
990
+ * Laadbalk: zolang de wachtrij laadt of de bron de pakbonnen nog bevestigt,
991
+ * ziet de picker hoe ver het klaarzetten is. De teksten in de regels blijven
992
+ * daardoor niet onverwacht "springen" zonder uitleg.
993
+ */
994
+ const checkTotal = checkProgress.total;
995
+ const checkDone = Math.min(checkProgress.done, checkTotal);
996
+ const busyLoading = loading || loadingDay || (checkTotal > 0 && checkDone < checkTotal);
997
+ const loadPercent = loading || loadingDay
998
+ ? 15
999
+ : checkTotal === 0
1000
+ ? 100
1001
+ : Math.round((checkDone / checkTotal) * 100);
1002
+ return (_jsxs("div", { className: "space-y-4 pb-24 md:pb-0", children: [busyLoading && (_jsxs("div", { className: "space-y-1", role: "status", "aria-live": "polite", "aria-label": "Gegevens worden geladen", children: [_jsxs("div", { className: "flex items-center justify-between text-xs text-muted-foreground", children: [_jsx("span", { children: loading || loadingDay
1003
+ ? "Wachtrij wordt geladen…"
1004
+ : `Pakbonnen controleren bij de bron (${checkDone} van ${checkTotal})` }), _jsxs("span", { className: "tabular-nums", children: [loadPercent, "%"] })] }), _jsx("div", { className: "h-1.5 w-full overflow-hidden rounded-full bg-muted", children: _jsx("div", { className: "h-full rounded-full bg-primary transition-all duration-300", style: { width: `${Math.max(5, loadPercent)}%` } }) })] })), _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, { variant: "outline", size: "lg", className: "hidden h-14 px-5 md:inline-flex", disabled: fetchingNotes, onClick: () => void fetchShippingNotes(), children: [fetchingNotes ? (_jsx(Loader2, { className: "mr-2 h-5 w-5 animate-spin" })) : (_jsx(FileDown, { className: "mr-2 h-5 w-5" })), fetchingNotes ? "Ophalen…" : "Verzendbrieven ophalen"] }), cartsPerPrint != null && (_jsxs(Button, { size: "lg", className: "hidden h-14 px-6 text-lg md:inline-flex", disabled: loading || batchPrinting || busyId != null, onClick: () => void printNextCarts(), children: [batchPrinting ? (_jsx(Loader2, { className: "mr-2 h-5 w-5 animate-spin" })) : (_jsx(Printer, { className: "mr-2 h-5 w-5" })), "Print eerstvolgende ", cartsPerPrint, " karren"] })), _jsxs(Button, { size: "lg", className: "hidden h-14 px-8 text-lg md:inline-flex", onClick: () => (pending ? confirmPending() : fetchNext()), disabled: busyId !== null || fetchingData, children: [busyId !== null || fetchingData ? (_jsx(Loader2, { className: "mr-2 h-5 w-5 animate-spin" })) : pending && !readiness.ready ? (_jsx(RefreshCw, { className: "mr-2 h-5 w-5" })) : (_jsx(Printer, { className: "mr-2 h-5 w-5" })), pending && !readiness.ready ? "Pakbon ophalen" : 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 || fetchingData, variant: readiness.ready ? "default" : "secondary", children: [busyId === "confirm" || fetchingData ? (_jsx(Loader2, { className: "mr-2 h-5 w-5 animate-spin" })) : readiness.ready ? (_jsx(Printer, { className: "mr-2 h-5 w-5" })) : (_jsx(RefreshCw, { className: "mr-2 h-5 w-5" })), readiness.ready ? t("print") : "Pakbon ophalen"] })] })] }), !readiness.ready && (_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: fetchingData
1005
+ ? "De pakbon wordt bij de bron opgevraagd. Zodra alles compleet is, verschijnt de printknop weer."
1006
+ : planReason
1007
+ ? `${planReason} Klik op "Pakbon ophalen" om hem bij de bron op te vragen.`
1008
+ : NOT_READY_NOTICE })] }) })), 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
1009
  const done = printedCartIdsOf(pending).includes(cart.cartId);
770
1010
  const active = cart.cartId === activeCartId;
771
1011
  return (_jsxs("div", { role: "button", tabIndex: 0, onClick: () => setActiveCartId(cart.cartId), onKeyDown: (event) => {
@@ -787,5 +1027,18 @@ export function PickScherm({ station, onBack }) {
787
1027
  })()] }) }))] })), 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: () => {
788
1028
  clearJob();
789
1029
  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) })] }));
1030
+ }, 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: () => {
1031
+ clearJob();
1032
+ clearShippingNotes();
1033
+ }, children: jobTexts.close })] })] }) })), shippingNotes.length > 0 && (_jsx(Card, { className: "border-2 border-primary/20", children: _jsxs(CardContent, { className: "flex flex-col gap-2 p-4", children: [_jsx("p", { className: "text-sm font-semibold text-foreground", children: shippingNotes.every((note) => note.state.status === "geprint")
1034
+ ? "Printactie volledig geslaagd — pakbon en verzendbrief zijn geprint."
1035
+ : "Printactie nog niet volledig: de verzendbrief is nog niet geprint." }), shippingNotes.map((note) => {
1036
+ const message = shippingNoteMessage(note.state);
1037
+ const failed = note.state.status === "mislukt" || note.state.status === "geblokkeerd";
1038
+ const waiting = isWaitingShippingNote(note.state.status);
1039
+ const typeLabel = shippingNoteSourceLabel(note.state);
1040
+ return (_jsxs("div", { className: "flex items-start gap-3", children: [failed ? (_jsx(AlertTriangle, { className: "mt-0.5 h-5 w-5 shrink-0 text-destructive" })) : note.state.status === "geprint" ? (_jsx(CheckCircle2, { className: "mt-0.5 h-5 w-5 shrink-0 text-primary" })) : (_jsx(Loader2, { className: "mt-0.5 h-5 w-5 shrink-0 animate-spin text-muted-foreground" })), _jsxs("div", { className: "min-w-0", children: [(note.cartLabel || typeLabel) && (_jsx("p", { className: "truncate text-sm text-muted-foreground", children: [note.cartLabel, typeLabel].filter(Boolean).join(" · ") })), _jsx("p", { className: failed ? "text-sm font-medium text-destructive" : "text-sm text-foreground", children: message ?? "Verzendbriefstatus wordt opgehaald." }), waiting && (note.state.attempts !== null || note.state.nextAttemptAt) && (_jsxs("p", { className: "text-xs text-muted-foreground", children: [note.state.attempts !== null ? `Pogingen: ${note.state.attempts}` : "", note.state.attempts !== null && note.state.nextAttemptAt ? " · " : "", note.state.nextAttemptAt
1041
+ ? `Volgende poging: ${new Date(note.state.nextAttemptAt).toLocaleTimeString("nl-NL", { hour: "2-digit", minute: "2-digit" })}`
1042
+ : ""] }))] })] }, note.jobId));
1043
+ })] }) })), _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 })), cartsPerPrint != null && (_jsx("div", { className: "flex justify-end md:hidden", children: _jsxs(Button, { className: "h-12", disabled: loading || batchPrinting || busyId != null, onClick: () => void printNextCarts(), children: [batchPrinting ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Printer, { className: "mr-2 h-4 w-4" })), "Print eerstvolgende ", cartsPerPrint, " karren"] }) })), _jsx("div", { className: "md:hidden", children: _jsxs(Button, { variant: "outline", className: "h-12 w-full", disabled: fetchingNotes, onClick: () => void fetchShippingNotes(), children: [fetchingNotes ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(FileDown, { className: "mr-2 h-4 w-4" })), fetchingNotes ? "Ophalen…" : "Verzendbrieven ophalen"] }) }), 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, readinessById: readinessById, 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 || fetchingData, children: [busyId !== null || fetchingData ? (_jsx(Loader2, { className: "mr-2 h-5 w-5 animate-spin" })) : pending && !readiness.ready ? (_jsx(RefreshCw, { className: "mr-2 h-5 w-5" })) : (_jsx(Printer, { className: "mr-2 h-5 w-5" })), _jsxs("span", { className: "truncate", children: [pending && !readiness.ready ? "Pakbon ophalen" : t("printNext"), pending?.document_number ? ` · ${pending.document_number}` : ""] })] }) }), _jsx(PreviewDialog, { item: previewItem, stationSlug: station.slug, onClose: () => setPreviewItem(null) })] }));
791
1044
  }