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