@flowselections/pick-schermen-kwekers 1.0.16 → 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.
- package/dist-lib/components/pick/PickScherm.d.ts +1 -1
- package/dist-lib/components/pick/PickScherm.d.ts.map +1 -1
- package/dist-lib/components/pick/PickScherm.js +513 -60
- package/dist-lib/components/pick/PreviewDialog.d.ts.map +1 -1
- package/dist-lib/components/pick/PreviewDialog.js +11 -2
- package/dist-lib/components/pick/QueueTable.d.ts +4 -1
- package/dist-lib/components/pick/QueueTable.d.ts.map +1 -1
- package/dist-lib/components/pick/QueueTable.js +77 -11
- package/dist-lib/components/pick/ShippingNoteScan.d.ts +15 -0
- package/dist-lib/components/pick/ShippingNoteScan.d.ts.map +1 -0
- package/dist-lib/components/pick/ShippingNoteScan.js +89 -0
- package/dist-lib/hooks/usePickQueue.d.ts +7 -0
- package/dist-lib/hooks/usePickQueue.d.ts.map +1 -1
- package/dist-lib/hooks/usePickQueue.js +20 -2
- package/dist-lib/hooks/usePrintJob.d.ts +23 -0
- package/dist-lib/hooks/usePrintJob.d.ts.map +1 -1
- package/dist-lib/hooks/usePrintJob.js +83 -1
- package/dist-lib/index.d.ts +2 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +2 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +3762 -1229
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +3762 -1229
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +3762 -1229
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +3893 -1334
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +1 -0
- package/dist-lib/lib/pickApi.d.ts +61 -3
- package/dist-lib/lib/pickApi.d.ts.map +1 -1
- package/dist-lib/lib/pickApi.js +169 -2
- package/dist-lib/lib/pickPreview.d.ts +9 -4
- package/dist-lib/lib/pickPreview.d.ts.map +1 -1
- package/dist-lib/lib/pickPreview.js +43 -61
- package/dist-lib/lib/pickTypes.d.ts +87 -0
- package/dist-lib/lib/pickTypes.d.ts.map +1 -1
- package/dist-lib/lib/pickTypes.js +226 -0
- package/dist-lib/lib/pickUiText.d.ts +7 -0
- package/dist-lib/lib/pickUiText.d.ts.map +1 -1
- package/dist-lib/lib/pickUiText.js +14 -0
- package/dist-lib/lib/shippingNote.d.ts +41 -0
- package/dist-lib/lib/shippingNote.d.ts.map +1 -0
- package/dist-lib/lib/shippingNote.js +77 -0
- 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,
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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,
|
|
234
|
-
/**
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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
|
|
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("
|
|
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
|
}
|