@flowselections/pick-schermen-kwekers 1.0.10 → 1.0.11
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/PickPublicPage.js +1 -1
- 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/LanguageTiles.d.ts.map +1 -1
- package/dist-lib/components/pick/LanguageTiles.js +3 -3
- package/dist-lib/components/pick/PickScherm.d.ts.map +1 -1
- package/dist-lib/components/pick/PickScherm.js +71 -10
- package/dist-lib/components/pick/QueueTable.d.ts.map +1 -1
- package/dist-lib/components/pick/QueueTable.js +22 -12
- package/dist-lib/hooks/usePickQueue.d.ts +4 -0
- package/dist-lib/hooks/usePickQueue.d.ts.map +1 -1
- package/dist-lib/hooks/usePickQueue.js +38 -7
- package/dist-lib/hooks/usePrintJob.d.ts.map +1 -1
- package/dist-lib/hooks/usePrintJob.js +13 -2
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +1788 -311
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +1788 -311
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +1788 -311
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +1888 -375
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +3 -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 +11 -4
- package/dist-lib/lib/pickApi.d.ts.map +1 -1
- package/dist-lib/lib/pickApi.js +46 -17
- package/dist-lib/lib/pickUiText.d.ts +29 -0
- package/dist-lib/lib/pickUiText.d.ts.map +1 -0
- package/dist-lib/lib/pickUiText.js +71 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
# pick schermen kwekers
|
|
2
|
-
|
|
3
|
-
blank project start
|
|
4
|
-
|
|
5
|
-
This project was built with [Lovable](https://lovable.dev).
|
|
6
|
-
|
|
7
|
-
## Build with Lovable
|
|
8
|
-
|
|
9
|
-
Continue developing this project in the [Lovable editor](https://lovable.dev/projects/f7462658-1d60-42f1-8e22-dbefe36f44ef).
|
|
10
|
-
|
|
11
|
-
- **Ship faster**: describe what you want to build and Lovable handles the code.
|
|
12
|
-
- **Stay in sync**: every change made in Lovable is committed straight to this repository.
|
|
13
|
-
- **Full ownership**: this code is yours. Push to `main` on GitHub and your changes sync back into Lovable, ready for your next prompt.
|
|
14
|
-
|
|
15
|
-
## Development
|
|
16
|
-
|
|
17
|
-
Prefer working locally? You need Node.js and npm — [install with nvm](https://github.com/nvm-sh/nvm#installing-and-updating).
|
|
18
|
-
|
|
19
|
-
```sh
|
|
20
|
-
git clone <this-repository-url>
|
|
21
|
-
cd <repository-name>
|
|
22
|
-
npm i
|
|
23
|
-
npm run dev
|
|
24
|
-
```
|
|
1
|
+
# pick schermen kwekers
|
|
2
|
+
|
|
3
|
+
blank project start
|
|
4
|
+
|
|
5
|
+
This project was built with [Lovable](https://lovable.dev).
|
|
6
|
+
|
|
7
|
+
## Build with Lovable
|
|
8
|
+
|
|
9
|
+
Continue developing this project in the [Lovable editor](https://lovable.dev/projects/f7462658-1d60-42f1-8e22-dbefe36f44ef).
|
|
10
|
+
|
|
11
|
+
- **Ship faster**: describe what you want to build and Lovable handles the code.
|
|
12
|
+
- **Stay in sync**: every change made in Lovable is committed straight to this repository.
|
|
13
|
+
- **Full ownership**: this code is yours. Push to `main` on GitHub and your changes sync back into Lovable, ready for your next prompt.
|
|
14
|
+
|
|
15
|
+
## Development
|
|
16
|
+
|
|
17
|
+
Prefer working locally? You need Node.js and npm — [install with nvm](https://github.com/nvm-sh/nvm#installing-and-updating).
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
git clone <this-repository-url>
|
|
21
|
+
cd <repository-name>
|
|
22
|
+
npm i
|
|
23
|
+
npm run dev
|
|
24
|
+
```
|
|
@@ -15,5 +15,5 @@ export function PickPublicPage({ slug }) {
|
|
|
15
15
|
if (!station) {
|
|
16
16
|
return (_jsx("div", { className: "flex min-h-screen items-center justify-center bg-background", children: _jsx("p", { className: "text-muted-foreground", children: "Pickstation niet gevonden of niet actief." }) }));
|
|
17
17
|
}
|
|
18
|
-
return (_jsx("div", { className: "min-h-screen bg-background p-6", children: _jsx(PickScherm, { station: station }) }));
|
|
18
|
+
return (_jsx("div", { className: "min-h-screen bg-background p-3 sm:p-6", children: _jsx(PickScherm, { station: station }) }));
|
|
19
19
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TemplateModulePage.d.ts","sourceRoot":"","sources":["../../src/components/TemplateModulePage.tsx"],"names":[],"mappings":"AAsBA,wBAAgB,kBAAkB,gCAmFjC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// src/components/TemplateModulePage.tsx — EXAMPLE PAGE COMPONENT
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Pagina-componenten horen in src/components/, NIET alleen in src/routes/.
|
|
6
|
+
// src/routes/ wordt NIET naar npm gepubliceerd (zie tsconfig.build.json exclude).
|
|
7
|
+
//
|
|
8
|
+
// GEEN <Header /> hier — die zit in _authenticated.tsx via staticData.title
|
|
9
|
+
// ============================================================================
|
|
10
|
+
import { useState, useEffect } from "react";
|
|
11
|
+
import { useAuth, supabase, Button, Card, CardContent, CardHeader, CardTitle, toast, } from "@flowselections/core";
|
|
12
|
+
import { TemplateModuleLogo } from './TemplateModuleLogo';
|
|
13
|
+
export function TemplateModulePage() {
|
|
14
|
+
// Auth is guaranteed — this user is already logged in
|
|
15
|
+
const { user, isAdmin } = useAuth();
|
|
16
|
+
// Example state — replace with your own
|
|
17
|
+
const [items, setItems] = useState([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
loadData();
|
|
21
|
+
}, []);
|
|
22
|
+
const loadData = async () => {
|
|
23
|
+
setLoading(true);
|
|
24
|
+
try {
|
|
25
|
+
// Example query — replace "products" with your table
|
|
26
|
+
const { data, error } = await supabase
|
|
27
|
+
.from("products")
|
|
28
|
+
.select("*")
|
|
29
|
+
.limit(10);
|
|
30
|
+
if (error)
|
|
31
|
+
throw error;
|
|
32
|
+
// ✅ Altijd Array.isArray bewaken op server-function data
|
|
33
|
+
// const safeItems = Array.isArray(data?.items) ? data.items : [];
|
|
34
|
+
setItems(Array.isArray(data) ? data : []);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
toast.error("Fout bij het laden van gegevens");
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
setLoading(false);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
return (_jsxs("main", { className: "p-6 space-y-6", children: [isAdmin && (_jsxs("div", { className: "text-sm text-muted-foreground", children: ["Ingelogd als admin: ", user?.email] })), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Overzicht" }) }), _jsxs(CardContent, { children: [loading ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Laden..." })) : items.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Geen gegevens gevonden." })) : (_jsxs("p", { className: "text-sm", children: [items.length, " items geladen."] })), _jsx(Button, { onClick: loadData, className: "mt-4", disabled: loading, children: "Vernieuwen" })] })] }), _jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(TemplateModuleLogo, { className: "w-6 h-6 text-primary" }), _jsx("span", { children: "SVG-component (voorkeur voor vector-logo's)" })] })] }));
|
|
44
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LanguageTiles.d.ts","sourceRoot":"","sources":["../../../src/components/pick/LanguageTiles.tsx"],"names":[],"mappings":"AAGA,UAAU,KAAK;IACb,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,KAAK,+
|
|
1
|
+
{"version":3,"file":"LanguageTiles.d.ts","sourceRoot":"","sources":["../../../src/components/pick/LanguageTiles.tsx"],"names":[],"mappings":"AAGA,UAAU,KAAK;IACb,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,KAAK,+BAyB1D"}
|
|
@@ -3,9 +3,9 @@ import { Card } from "@flowselections/core";
|
|
|
3
3
|
import { PICK_LANGUAGES } from "../../lib/pickI18n";
|
|
4
4
|
/** Taalkeuze — zelfde tegel-uitstraling en bediening als de oude landselectie. */
|
|
5
5
|
export function LanguageTiles({ onSelect, selected }) {
|
|
6
|
-
return (_jsx("div", { className: "grid grid-cols-2 gap-
|
|
6
|
+
return (_jsx("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 sm:gap-4 lg:grid-cols-4 xl:grid-cols-5", children: PICK_LANGUAGES.map((l) => (_jsxs(Card, { role: "button", tabIndex: 0, onClick: () => onSelect(l.code), onKeyDown: (e) => {
|
|
7
7
|
if (e.key === "Enter" || e.key === " ")
|
|
8
8
|
onSelect(l.code);
|
|
9
|
-
}, className: "cursor-pointer select-none border-2 p-
|
|
10
|
-
(selected === l.code ? "border-primary bg-primary/5" : ""), children: [_jsx("div", { className: "text-
|
|
9
|
+
}, className: "flex min-h-22 cursor-pointer select-none flex-col items-center justify-center border-2 p-4 text-center transition-all hover:border-primary hover:shadow-lg focus:border-primary focus:outline-none active:scale-[0.98] sm:p-6 " +
|
|
10
|
+
(selected === l.code ? "border-primary bg-primary/5" : ""), children: [_jsx("div", { className: "text-4xl leading-none sm:text-5xl", children: l.flag }), _jsx("div", { className: "mt-2 text-base font-semibold text-foreground sm:mt-3 sm:text-lg", children: l.label })] }, l.code))) }));
|
|
11
11
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PickScherm.d.ts","sourceRoot":"","sources":["../../../src/components/pick/PickScherm.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PickScherm.d.ts","sourceRoot":"","sources":["../../../src/components/pick/PickScherm.tsx"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAiB,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAatE,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAID;;;GAGG;AACH,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,KAAK,+BAslBpD"}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { Badge, Button, Card, CardContent, Input, toast, } from "@flowselections/core";
|
|
4
|
-
import { AlertTriangle, ArrowLeft, CheckCircle2, Globe, Loader2, Printer, RefreshCw, Search, WifiOff, } from "lucide-react";
|
|
3
|
+
import { Badge, Button, Card, CardContent, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, toast, } from "@flowselections/core";
|
|
4
|
+
import { AlertTriangle, ArrowLeft, CheckCircle2, Globe, Loader2, Printer, RefreshCw, ScanLine, Search, WifiOff, Zap, } from "lucide-react";
|
|
5
5
|
import { LanguageTiles } from "./LanguageTiles";
|
|
6
6
|
import { QueueTable } from "./QueueTable";
|
|
7
7
|
import { getSessionId, pickApi } from "../../lib/pickApi";
|
|
8
8
|
import { completeLogisticsTaskForItem } from "../../lib/pickPrint";
|
|
9
9
|
import { getPickTranslator } from "../../lib/pickI18n";
|
|
10
|
+
import { friendlyPickError, getPickUiTexts } from "../../lib/pickUiText";
|
|
10
11
|
import { getPrintJobTexts, printJobStatusLabel } from "../../lib/pickPrintJobText";
|
|
11
12
|
import { defaultFilters, usePickHeartbeat, usePickQueue, } from "../../hooks/usePickQueue";
|
|
12
13
|
import { usePickDocumentTypes, usePickStatuses } from "../../hooks/usePickConfig";
|
|
@@ -20,18 +21,22 @@ export function PickScherm({ station, onBack }) {
|
|
|
20
21
|
const sessionId = getSessionId();
|
|
21
22
|
const [lang, setLang] = useState(null);
|
|
22
23
|
const [filters, setFilters] = useState(defaultFilters);
|
|
24
|
+
const [searchInput, setSearchInput] = useState("");
|
|
25
|
+
const [scanInput, setScanInput] = useState("");
|
|
23
26
|
const [busyId, setBusyId] = useState(null);
|
|
24
27
|
const [pending, setPending] = useState(null);
|
|
25
28
|
const [lastPrinted, setLastPrinted] = useState(null);
|
|
26
29
|
const [emptyNotice, setEmptyNotice] = useState(null);
|
|
27
30
|
const resetDone = useRef(null);
|
|
28
31
|
const autoFetched = useRef(false);
|
|
32
|
+
const retryAfterExpiry = useRef(false);
|
|
29
33
|
const { data: statuses } = usePickStatuses();
|
|
30
34
|
const { data: documentTypes } = usePickDocumentTypes();
|
|
31
35
|
const { online } = usePickHeartbeat(station.id, null, sessionId);
|
|
32
36
|
const { tracked, track, clear: clearJob } = usePrintJob();
|
|
33
37
|
const t = useMemo(() => getPickTranslator(lang ?? "nl"), [lang]);
|
|
34
38
|
const jobTexts = useMemo(() => getPrintJobTexts(t.lang), [t]);
|
|
39
|
+
const ui = useMemo(() => getPickUiTexts(t.lang), [t]);
|
|
35
40
|
useEffect(() => {
|
|
36
41
|
if (typeof window === "undefined")
|
|
37
42
|
return;
|
|
@@ -44,8 +49,15 @@ export function PickScherm({ station, onBack }) {
|
|
|
44
49
|
if (typeof window !== "undefined")
|
|
45
50
|
window.localStorage.setItem(LANG_STORAGE_KEY, code);
|
|
46
51
|
}, []);
|
|
52
|
+
/** Zoeken pas na een korte pauze, zodat typen geen query per toets afvuurt. */
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
setFilters((f) => (f.search === searchInput ? f : { ...f, search: searchInput, page: 0 }));
|
|
56
|
+
}, 300);
|
|
57
|
+
return () => clearTimeout(timer);
|
|
58
|
+
}, [searchInput]);
|
|
47
59
|
const effectiveFilters = useMemo(() => ({ ...filters, country: null, sortRules: [] }), [filters]);
|
|
48
|
-
const { items, total, loading, lockedIds, refresh, pendingCount, printedCount } = usePickQueue({
|
|
60
|
+
const { items, total, loading, lockedIds, refresh, pendingCount, printedCount, urgentCount, realtimeOk, } = usePickQueue({
|
|
49
61
|
stationId: station.id,
|
|
50
62
|
filters: effectiveFilters,
|
|
51
63
|
enabled: lang !== null,
|
|
@@ -89,9 +101,14 @@ export function PickScherm({ station, onBack }) {
|
|
|
89
101
|
}
|
|
90
102
|
})();
|
|
91
103
|
}, [tracked, sessionId, refresh, jobTexts]);
|
|
92
|
-
/**
|
|
104
|
+
/**
|
|
105
|
+
* Zet een printfout om in een begrijpelijke melding en meldt of de claim
|
|
106
|
+
* verlopen was — dan kan het scherm zelf opnieuw ophalen.
|
|
107
|
+
*/
|
|
93
108
|
const showPrintError = useCallback((e) => {
|
|
94
|
-
|
|
109
|
+
const { message, detail, expiredClaim } = friendlyPickError(e, t.lang);
|
|
110
|
+
toast.error(message, detail && detail !== message ? { description: detail } : undefined);
|
|
111
|
+
return expiredClaim;
|
|
95
112
|
}, [t]);
|
|
96
113
|
const handlePick = useCallback(async (item) => {
|
|
97
114
|
setBusyId(item.id);
|
|
@@ -162,7 +179,7 @@ export function PickScherm({ station, onBack }) {
|
|
|
162
179
|
return claimed;
|
|
163
180
|
}
|
|
164
181
|
catch (e) {
|
|
165
|
-
const message = e
|
|
182
|
+
const { message } = friendlyPickError(e, t.lang);
|
|
166
183
|
if (silent)
|
|
167
184
|
setEmptyNotice(message);
|
|
168
185
|
else
|
|
@@ -189,7 +206,7 @@ export function PickScherm({ station, onBack }) {
|
|
|
189
206
|
await fetchNext();
|
|
190
207
|
}
|
|
191
208
|
catch (e) {
|
|
192
|
-
showPrintError(e);
|
|
209
|
+
const expired = showPrintError(e);
|
|
193
210
|
try {
|
|
194
211
|
await pickApi.releaseLock(item.id);
|
|
195
212
|
}
|
|
@@ -199,8 +216,47 @@ export function PickScherm({ station, onBack }) {
|
|
|
199
216
|
setPending(null);
|
|
200
217
|
refresh();
|
|
201
218
|
setBusyId(null);
|
|
219
|
+
// Verlopen claim: zelf een verse opdracht ophalen in plaats van blijven hangen.
|
|
220
|
+
if (expired && !retryAfterExpiry.current) {
|
|
221
|
+
retryAfterExpiry.current = true;
|
|
222
|
+
try {
|
|
223
|
+
await fetchNext(true);
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
retryAfterExpiry.current = false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
202
229
|
}
|
|
203
230
|
}, [pending, runPrint, fetchNext, refresh, showPrintError]);
|
|
231
|
+
/** Scanner: barcode, documentnummer of ordernummer selecteert direct de regel. */
|
|
232
|
+
const handleScan = useCallback(async (raw) => {
|
|
233
|
+
const code = raw.trim();
|
|
234
|
+
if (!code)
|
|
235
|
+
return;
|
|
236
|
+
setBusyId("scan");
|
|
237
|
+
try {
|
|
238
|
+
const found = await pickApi.findByCode(code, station.id);
|
|
239
|
+
if (!found) {
|
|
240
|
+
toast.error(ui.scanNotFound);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
setScanInput("");
|
|
244
|
+
if (found.printed_at) {
|
|
245
|
+
toast.error(ui.scanAlreadyPrinted);
|
|
246
|
+
setFilters((f) => ({ ...f, search: code, page: 0 }));
|
|
247
|
+
setSearchInput(code);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
setBusyId(null);
|
|
251
|
+
await handlePick(found);
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
showPrintError(e);
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
setBusyId(null);
|
|
258
|
+
}
|
|
259
|
+
}, [station.id, ui, handlePick, showPrintError]);
|
|
204
260
|
const cancelPending = useCallback(async () => {
|
|
205
261
|
const item = pending;
|
|
206
262
|
setPending(null);
|
|
@@ -234,13 +290,18 @@ export function PickScherm({ station, onBack }) {
|
|
|
234
290
|
}, [pendingCount]);
|
|
235
291
|
// Stap 1 — taalselectie
|
|
236
292
|
if (lang === null) {
|
|
237
|
-
return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "
|
|
293
|
+
return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 sm:flex sm:flex-wrap sm:justify-between", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "truncate text-xl font-bold text-foreground sm:text-2xl", children: station.name }), _jsx("p", { className: "text-sm text-muted-foreground", children: t("chooseLanguageHint") })] }), onBack && (_jsxs(Button, { variant: "outline", className: "shrink-0", onClick: onBack, children: [_jsx(ArrowLeft, { className: "mr-2 h-4 w-4" }), " ", t("back")] }))] }), _jsx(LanguageTiles, { onSelect: chooseLang })] }));
|
|
238
294
|
}
|
|
239
295
|
const totalPages = Math.max(1, Math.ceil(total / filters.pageSize));
|
|
240
296
|
const jobStatus = tracked?.job?.status ?? null;
|
|
241
297
|
const jobFailed = jobStatus === "mislukt" || jobStatus === "geannuleerd";
|
|
242
|
-
return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between
|
|
298
|
+
return (_jsxs("div", { className: "space-y-4 pb-24 md:pb-0", children: [_jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:flex-wrap md:items-center md:justify-between", children: [_jsxs("div", { className: "grid grid-cols-[auto_minmax(0,1fr)] items-center gap-3", children: [_jsxs(Button, { variant: "outline", size: "lg", className: "h-12 shrink-0 px-3 sm:px-4", onClick: () => setLang(null), "aria-label": t("changeLanguage"), children: [_jsx(Globe, { className: "h-5 w-5 sm:mr-2" }), _jsx("span", { className: "hidden sm:inline", children: t("changeLanguage") })] }), _jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "truncate text-lg font-bold text-foreground sm:text-xl", children: station.name }), _jsxs("p", { className: "truncate text-sm text-muted-foreground", children: [pendingCount, " ", t("statusToPrint"), " \u00B7 ", printedCount, " ", t("statusPrinted")] })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [!online && (_jsxs(Badge, { className: "bg-destructive text-destructive-foreground", children: [_jsx(WifiOff, { className: "mr-1 h-3.5 w-3.5" }), " ", t("offline")] })), _jsx(Button, { variant: "outline", size: "icon", className: "h-12 w-12 shrink-0", onClick: refresh, "aria-label": t("refresh"), children: _jsx(RefreshCw, { className: "h-4 w-4" }) }), _jsxs(Button, { size: "lg", className: "hidden h-14 px-8 text-lg md:inline-flex", onClick: () => (pending ? confirmPending() : fetchNext()), disabled: busyId !== null, children: [busyId !== null ? (_jsx(Loader2, { className: "mr-2 h-5 w-5 animate-spin" })) : (_jsx(Printer, { className: "mr-2 h-5 w-5" })), t("printNext"), pending?.document_number ? ` · ${pending.document_number}` : ""] })] })] }), !pending && emptyNotice && (_jsx("p", { className: "text-sm text-muted-foreground", children: emptyNotice })), pending && (_jsx(Card, { className: "border-2 border-primary", children: _jsxs(CardContent, { className: "flex flex-col gap-4 p-4 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between sm:p-5", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-sm font-medium text-muted-foreground", children: t("confirmPrintTitle") }), _jsx("p", { className: "truncate text-xl font-bold text-foreground sm:text-2xl", children: pending.document_number ?? t("document") }), _jsx("p", { className: "truncate text-sm text-muted-foreground", children: [pending.order_number, pending.customer_name].filter(Boolean).join(" · ") })] }), _jsxs("div", { className: "flex flex-col gap-2 sm:flex-row", children: [_jsx(Button, { variant: "outline", size: "lg", className: "h-12 w-full sm:w-auto", onClick: cancelPending, disabled: busyId !== null, children: t("cancel") }), _jsxs(Button, { size: "lg", className: "h-14 w-full px-8 text-lg sm:w-auto", onClick: confirmPending, disabled: busyId !== null, children: [busyId === "confirm" ? (_jsx(Loader2, { className: "mr-2 h-5 w-5 animate-spin" })) : (_jsx(Printer, { className: "mr-2 h-5 w-5" })), t("print")] })] })] }) })), tracked && (_jsx(Card, { className: jobFailed ? "border-2 border-destructive" : "border-2 border-primary/40", children: _jsxs(CardContent, { className: "flex flex-col gap-4 p-4 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between", children: [_jsxs("div", { className: "flex min-w-0 items-start gap-3", children: [jobFailed ? (_jsx(AlertTriangle, { className: "h-6 w-6 shrink-0 text-destructive" })) : jobStatus === "voltooid" ? (_jsx(CheckCircle2, { className: "h-6 w-6 shrink-0 text-primary" })) : (_jsx(Loader2, { className: "h-6 w-6 shrink-0 animate-spin text-muted-foreground" })), _jsxs("div", { className: "min-w-0", children: [_jsxs("p", { className: "truncate text-sm font-medium text-muted-foreground", children: [jobTexts.jobTitle, " ", tracked.documentNumber ? `· ${tracked.documentNumber}` : ""] }), _jsx("p", { className: "text-lg font-semibold text-foreground", children: printJobStatusLabel(jobStatus, t.lang) }), tracked.job?.printer_naam && (_jsxs("p", { className: "truncate text-sm text-muted-foreground", children: [jobTexts.printerLabel, ": ", tracked.job.printer_naam] })), jobFailed && (_jsxs("p", { className: "text-sm text-destructive", children: [tracked.job?.foutmelding ?? jobTexts.unknown, " \u00B7 ", jobTexts.requeued] }))] })] }), _jsxs("div", { className: "flex flex-col gap-2 sm:flex-row", children: [jobFailed && lastPrinted && (_jsxs(Button, { size: "lg", className: "h-12 w-full sm:w-auto", onClick: () => {
|
|
243
299
|
clearJob();
|
|
244
300
|
void handlePick(lastPrinted);
|
|
245
|
-
}, disabled: busyId !== null, children: [_jsx(Printer, { className: "mr-2 h-5 w-5" }), " ", jobTexts.retry] })), _jsx(Button, { variant: "outline", size: "lg", onClick: clearJob, children: jobTexts.close })] })] }) })), _jsx(Card, { children:
|
|
301
|
+
}, disabled: busyId !== null, children: [_jsx(Printer, { className: "mr-2 h-5 w-5" }), " ", jobTexts.retry] })), _jsx(Button, { variant: "outline", size: "lg", className: "h-12 w-full sm:w-auto", onClick: clearJob, children: jobTexts.close })] })] }) })), _jsx(Card, { children: _jsxs(CardContent, { className: "flex flex-wrap items-center gap-3 p-3 sm:p-4", children: [_jsxs("div", { className: "relative w-full min-w-0 flex-1 sm:min-w-64", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { className: "h-12 pl-9 text-base", placeholder: t("search"), value: searchInput, onChange: (e) => setSearchInput(e.target.value) })] }), _jsxs("div", { className: "relative w-full min-w-0 sm:w-56", children: [_jsx(ScanLine, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { className: "h-12 pl-9 text-base", placeholder: ui.scanPlaceholder, value: scanInput, onChange: (e) => setScanInput(e.target.value), onKeyDown: (e) => {
|
|
302
|
+
if (e.key !== "Enter")
|
|
303
|
+
return;
|
|
304
|
+
e.preventDefault();
|
|
305
|
+
void handleScan(scanInput);
|
|
306
|
+
} })] }), _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, 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}` : ""] })] }) })] }));
|
|
246
307
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QueueTable.d.ts","sourceRoot":"","sources":["../../../src/components/pick/QueueTable.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEvF,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAE5E,UAAU,KAAK;IACb,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,aAAa,EAAE,gBAAgB,EAAE,CAAC;IAClC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;IACtC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,CAAC,CAAC,EAAE,cAAc,CAAC;CACpB;AASD,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,SAAS,EACT,QAAQ,EACR,aAAa,EACb,MAAM,EACN,MAAM,EACN,SAAS,EACT,QAAQ,EACR,CAAC,EAAE,UAAU,GACd,EAAE,KAAK,+
|
|
1
|
+
{"version":3,"file":"QueueTable.d.ts","sourceRoot":"","sources":["../../../src/components/pick/QueueTable.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEvF,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAE5E,UAAU,KAAK;IACb,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,aAAa,EAAE,gBAAgB,EAAE,CAAC;IAClC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;IACtC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,CAAC,CAAC,EAAE,cAAc,CAAC;CACpB;AASD,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,SAAS,EACT,QAAQ,EACR,aAAa,EACb,MAAM,EACN,MAAM,EACN,SAAS,EACT,QAAQ,EACR,CAAC,EAAE,UAAU,GACd,EAAE,KAAK,+BA4NP"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Badge, Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@flowselections/core";
|
|
3
3
|
import { AlertTriangle, Lock, Printer, RotateCcw } from "lucide-react";
|
|
4
4
|
import { sinceLabel } from "../../lib/pickTypes";
|
|
@@ -16,15 +16,25 @@ export function QueueTable({ items, lockedIds, statuses, documentTypes, busyId,
|
|
|
16
16
|
const t = translator ?? getPickTranslator("nl");
|
|
17
17
|
const safeItems = Array.isArray(items) ? items : [];
|
|
18
18
|
const docById = new Map((Array.isArray(documentTypes) ? documentTypes : []).map((d) => [d.id, d]));
|
|
19
|
-
return (_jsx("div", { className: "overflow-x-auto rounded-lg border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: t("document") }), _jsx(TableHead, { children: t("order") }), _jsx(TableHead, { children: t("customer") }), _jsx(TableHead, { className: "hidden lg:table-cell", children: t("deliveryAddress") }), _jsx(TableHead, { className: "text-right", children: t("lines") }), _jsx(TableHead, { className: "text-right", children: t("carts") }), _jsx(TableHead, { children: t("status") }), _jsx(TableHead, { children: t("priority") }), _jsx(TableHead, { children: t("announced") }), _jsx(TableHead, { children: t("waiting") }), _jsx(TableHead, { className: "text-right", children: t("action") })] }) }), _jsxs(TableBody, { children: [safeItems.map((item) => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "hidden overflow-x-auto rounded-lg border md:block", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: t("document") }), _jsx(TableHead, { children: t("order") }), _jsx(TableHead, { children: t("customer") }), _jsx(TableHead, { className: "hidden lg:table-cell", children: t("deliveryAddress") }), _jsx(TableHead, { className: "text-right", children: t("lines") }), _jsx(TableHead, { className: "text-right", children: t("carts") }), _jsx(TableHead, { children: t("status") }), _jsx(TableHead, { children: t("priority") }), _jsx(TableHead, { children: t("announced") }), _jsx(TableHead, { children: t("waiting") }), _jsx(TableHead, { className: "text-right", children: t("action") })] }) }), _jsxs(TableBody, { children: [safeItems.map((item) => {
|
|
20
|
+
const printed = !!item.printed_at;
|
|
21
|
+
const locked = !printed && lockedIds.has(item.id);
|
|
22
|
+
const doc = item.document_type_id ? docById.get(item.document_type_id) : undefined;
|
|
23
|
+
const warnings = Array.isArray(item.warnings) ? item.warnings : [];
|
|
24
|
+
return (_jsxs(TableRow, { className: locked || printed ? "opacity-60" : undefined, children: [_jsxs(TableCell, { className: "font-medium", children: [_jsxs("div", { className: "flex items-center gap-2", children: [item.document_number, warnings.length > 0 && (_jsx(AlertTriangle, { className: "h-4 w-4 text-destructive", "aria-label": t("warning") }))] }), _jsx("div", { className: "text-xs text-muted-foreground", children: doc?.name ?? "-" })] }), _jsx(TableCell, { children: item.order_number }), _jsx(TableCell, { children: item.customer_name }), _jsx(TableCell, { className: "hidden max-w-56 truncate lg:table-cell", children: item.delivery_address }), _jsx(TableCell, { className: "text-right", children: item.line_count }), _jsx(TableCell, { className: "text-right", children: item.cart_count }), _jsx(TableCell, { children: _jsx(Badge, { variant: printed ? "secondary" : "default", children: printed ? t("statusPrinted") : t("statusToPrint") }) }), _jsx(TableCell, { children: _jsx("span", { className: "rounded px-2 py-1 text-xs font-semibold " +
|
|
25
|
+
priorityTone(item.priority, item.is_urgent), children: item.is_urgent ? t("urgent") : item.priority }) }), _jsx(TableCell, { className: "whitespace-nowrap text-sm", children: new Date(item.announced_at).toLocaleTimeString(t.locale, {
|
|
26
|
+
hour: "2-digit",
|
|
27
|
+
minute: "2-digit",
|
|
28
|
+
}) }), _jsx(TableCell, { className: "whitespace-nowrap text-sm", children: sinceLabel(item.announced_at) }), _jsx(TableCell, { className: "text-right", children: printed ? (_jsxs(Button, { size: "sm", variant: "ghost", disabled: readOnly || busyId === item.id, onClick: () => (onReprint ?? onPick)(item), children: [_jsx(RotateCcw, { className: "mr-1 h-4 w-4" }), t("print")] })) : locked ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs text-muted-foreground", children: [_jsx(Lock, { className: "h-3.5 w-3.5" }), " ", t("inProgress")] })) : (_jsxs(Button, { size: "sm", variant: "outline", disabled: readOnly || busyId === item.id, onClick: () => onPick(item), children: [_jsx(Printer, { className: "mr-1 h-4 w-4" }), t("print")] })) })] }, item.id));
|
|
29
|
+
}), safeItems.length === 0 && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 11, className: "py-10 text-center text-muted-foreground", children: t("emptyQueue") }) }))] })] }) }), _jsxs("div", { className: "space-y-3 md:hidden", children: [safeItems.map((item) => {
|
|
30
|
+
const printed = !!item.printed_at;
|
|
31
|
+
const locked = !printed && lockedIds.has(item.id);
|
|
32
|
+
const doc = item.document_type_id ? docById.get(item.document_type_id) : undefined;
|
|
33
|
+
const warnings = Array.isArray(item.warnings) ? item.warnings : [];
|
|
34
|
+
return (_jsxs("div", { className: "rounded-lg border bg-card p-4 " + (locked || printed ? "opacity-60" : ""), children: [_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { className: "truncate text-lg font-bold text-foreground", children: item.document_number }), warnings.length > 0 && (_jsx(AlertTriangle, { className: "h-4 w-4 shrink-0 text-destructive", "aria-label": t("warning") }))] }), _jsx("p", { className: "truncate text-xs text-muted-foreground", children: doc?.name ?? "-" })] }), _jsxs("div", { className: "flex shrink-0 flex-col items-end gap-1", children: [_jsx(Badge, { variant: printed ? "secondary" : "default", children: printed ? t("statusPrinted") : t("statusToPrint") }), _jsx("span", { className: "rounded px-2 py-0.5 text-xs font-semibold " +
|
|
35
|
+
priorityTone(item.priority, item.is_urgent), children: item.is_urgent ? t("urgent") : item.priority })] })] }), _jsxs("div", { className: "mt-2 min-w-0 space-y-0.5 text-sm", children: [_jsx("p", { className: "truncate text-foreground", children: item.order_number }), _jsx("p", { className: "truncate text-muted-foreground", children: item.customer_name }), item.delivery_address && (_jsx("p", { className: "truncate text-xs text-muted-foreground", children: item.delivery_address }))] }), _jsxs("div", { className: "mt-2 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [_jsxs("span", { children: [t("lines"), ": ", item.line_count] }), _jsxs("span", { children: [t("carts"), ": ", item.cart_count] }), _jsxs("span", { children: [t("announced"), ":", " ", new Date(item.announced_at).toLocaleTimeString(t.locale, {
|
|
36
|
+
hour: "2-digit",
|
|
37
|
+
minute: "2-digit",
|
|
38
|
+
})] }), _jsxs("span", { children: [t("waiting"), ": ", sinceLabel(item.announced_at)] })] }), _jsx("div", { className: "mt-3", children: printed ? (_jsxs(Button, { size: "lg", variant: "outline", className: "h-12 w-full", disabled: readOnly || busyId === item.id, onClick: () => (onReprint ?? onPick)(item), children: [_jsx(RotateCcw, { className: "mr-2 h-4 w-4" }), t("print")] })) : locked ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs text-muted-foreground", children: [_jsx(Lock, { className: "h-3.5 w-3.5" }), " ", t("inProgress")] })) : (_jsxs(Button, { size: "lg", className: "h-12 w-full", disabled: readOnly || busyId === item.id, onClick: () => onPick(item), children: [_jsx(Printer, { className: "mr-2 h-4 w-4" }), t("print")] })) })] }, item.id));
|
|
39
|
+
}), safeItems.length === 0 && (_jsx("div", { className: "rounded-lg border py-10 text-center text-muted-foreground", children: t("emptyQueue") }))] })] }));
|
|
30
40
|
}
|
|
@@ -4,6 +4,8 @@ export interface QueueFilters {
|
|
|
4
4
|
search: string;
|
|
5
5
|
documentTypeId: string | null;
|
|
6
6
|
urgentOnly: boolean;
|
|
7
|
+
/** Alleen regels die nog geprint moeten worden. */
|
|
8
|
+
onlyPending: boolean;
|
|
7
9
|
page: number;
|
|
8
10
|
pageSize: number;
|
|
9
11
|
sortRules: PickSortRule[];
|
|
@@ -24,9 +26,11 @@ export declare function usePickQueue(args: {
|
|
|
24
26
|
pickDay: string | null;
|
|
25
27
|
pendingCount: number;
|
|
26
28
|
printedCount: number;
|
|
29
|
+
urgentCount: number;
|
|
27
30
|
loading: boolean;
|
|
28
31
|
error: string | null;
|
|
29
32
|
lockedIds: Set<string>;
|
|
33
|
+
realtimeOk: boolean;
|
|
30
34
|
refresh: () => void;
|
|
31
35
|
};
|
|
32
36
|
/** Houdt bij dat dit scherm online is en ruimt verlopen locks op. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePickQueue.d.ts","sourceRoot":"","sources":["../../src/hooks/usePickQueue.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAY,aAAa,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAG9E,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,EAAE,OAAO,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,YAAY,EAAE,CAAC;CAC3B;AAED,eAAO,MAAM,cAAc,EAAE,
|
|
1
|
+
{"version":3,"file":"usePickQueue.d.ts","sourceRoot":"","sources":["../../src/hooks/usePickQueue.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAY,aAAa,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAG9E,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,EAAE,OAAO,CAAC;IACpB,mDAAmD;IACnD,WAAW,EAAE,OAAO,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,YAAY,EAAE,CAAC;CAC3B;AAED,eAAO,MAAM,cAAc,EAAE,YAS5B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;;;;;;;;;;;;EA6IA;AAED,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,SAAS,CAAC,EAAE,MAAM;;EAuBhG;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,0FAOnF"}
|
|
@@ -7,6 +7,7 @@ export const defaultFilters = {
|
|
|
7
7
|
search: "",
|
|
8
8
|
documentTypeId: null,
|
|
9
9
|
urgentOnly: false,
|
|
10
|
+
onlyPending: false,
|
|
10
11
|
page: 0,
|
|
11
12
|
pageSize: 25,
|
|
12
13
|
sortRules: [],
|
|
@@ -56,7 +57,7 @@ export function usePickQueue(args) {
|
|
|
56
57
|
search: filters.search.trim() || undefined,
|
|
57
58
|
documentTypeId: filters.documentTypeId,
|
|
58
59
|
urgentOnly: filters.urgentOnly,
|
|
59
|
-
includeDone:
|
|
60
|
+
includeDone: !filters.onlyPending,
|
|
60
61
|
pickDay,
|
|
61
62
|
sortRules: filters.sortRules,
|
|
62
63
|
page: filters.page,
|
|
@@ -68,27 +69,55 @@ export function usePickQueue(args) {
|
|
|
68
69
|
enabled: enabled && !pickDayQuery.isLoading,
|
|
69
70
|
queryFn: () => pickApi.dayCounts({ stationId, screenId, pickDay }),
|
|
70
71
|
});
|
|
72
|
+
// Locks komen primair via realtime binnen; deze poll is alleen terugval.
|
|
71
73
|
const locksQuery = useQuery({
|
|
72
74
|
queryKey: ["pick", "locks", stationId ?? "-"],
|
|
73
75
|
enabled,
|
|
74
76
|
queryFn: () => pickApi.locks(stationId),
|
|
75
|
-
refetchInterval:
|
|
77
|
+
refetchInterval: 20000,
|
|
76
78
|
});
|
|
77
|
-
const
|
|
79
|
+
const invalidateNow = useCallback(() => {
|
|
78
80
|
queryClient.invalidateQueries({ queryKey: ["pick", "queue"] });
|
|
79
81
|
queryClient.invalidateQueries({ queryKey: ["pick", "locks"] });
|
|
80
82
|
queryClient.invalidateQueries({ queryKey: ["pick", "dayCounts"] });
|
|
81
83
|
queryClient.invalidateQueries({ queryKey: ["pick", "countryCounts"] });
|
|
82
84
|
queryClient.invalidateQueries({ queryKey: ["pick", "dashboard"] });
|
|
83
85
|
}, [queryClient]);
|
|
86
|
+
/**
|
|
87
|
+
* Realtime-meldingen komen bij een Engine-sync in tientallen tegelijk binnen.
|
|
88
|
+
* Samenvoegen tot één herlaadslag houdt het scherm rustig en snel.
|
|
89
|
+
*/
|
|
90
|
+
const debounceTimer = useRef(null);
|
|
91
|
+
const invalidate = useCallback(() => {
|
|
92
|
+
if (debounceTimer.current)
|
|
93
|
+
clearTimeout(debounceTimer.current);
|
|
94
|
+
debounceTimer.current = setTimeout(() => {
|
|
95
|
+
debounceTimer.current = null;
|
|
96
|
+
invalidateNow();
|
|
97
|
+
}, 400);
|
|
98
|
+
}, [invalidateNow]);
|
|
99
|
+
useEffect(() => () => {
|
|
100
|
+
if (debounceTimer.current)
|
|
101
|
+
clearTimeout(debounceTimer.current);
|
|
102
|
+
}, []);
|
|
103
|
+
const [realtimeOk, setRealtimeOk] = useState(true);
|
|
84
104
|
useEffect(() => {
|
|
85
105
|
if (!enabled)
|
|
86
106
|
return;
|
|
107
|
+
// Alleen meldingen van dit station: andere stations mogen dit scherm niet belasten.
|
|
108
|
+
const queueFilter = stationId
|
|
109
|
+
? { event: "*", schema: "public", table: "pick_queue", filter: `station_id=eq.${stationId}` }
|
|
110
|
+
: { event: "*", schema: "public", table: "pick_queue" };
|
|
111
|
+
const lockFilter = stationId
|
|
112
|
+
? { event: "*", schema: "public", table: "pick_queue_locks", filter: `station_id=eq.${stationId}` }
|
|
113
|
+
: { event: "*", schema: "public", table: "pick_queue_locks" };
|
|
87
114
|
const channel = pickDb
|
|
88
115
|
.channel(`pick-queue-${stationId ?? "all"}`)
|
|
89
|
-
.on("postgres_changes",
|
|
90
|
-
.on("postgres_changes",
|
|
91
|
-
.subscribe()
|
|
116
|
+
.on("postgres_changes", queueFilter, invalidate)
|
|
117
|
+
.on("postgres_changes", lockFilter, invalidate)
|
|
118
|
+
.subscribe((status) => {
|
|
119
|
+
setRealtimeOk(status === "SUBSCRIBED");
|
|
120
|
+
});
|
|
92
121
|
return () => {
|
|
93
122
|
pickDb.removeChannel(channel);
|
|
94
123
|
};
|
|
@@ -100,10 +129,12 @@ export function usePickQueue(args) {
|
|
|
100
129
|
pickDay,
|
|
101
130
|
pendingCount: countsQuery.data?.pending ?? 0,
|
|
102
131
|
printedCount: countsQuery.data?.printed ?? 0,
|
|
132
|
+
urgentCount: countsQuery.data?.urgent ?? 0,
|
|
103
133
|
loading: queueQuery.isLoading,
|
|
104
134
|
error: queueQuery.error instanceof Error ? queueQuery.error.message : null,
|
|
105
135
|
lockedIds,
|
|
106
|
-
|
|
136
|
+
realtimeOk,
|
|
137
|
+
refresh: invalidateNow,
|
|
107
138
|
};
|
|
108
139
|
}
|
|
109
140
|
/** Houdt bij dat dit scherm online is en ruimt verlopen locks op. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePrintJob.d.ts","sourceRoot":"","sources":["../../src/hooks/usePrintJob.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,GAAG,EAAE,YAAY,GAAG,IAAI,CAAC;CAC1B;AAED;;;;GAIG;AACH,wBAAgB,WAAW;;kBAKhB;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;;
|
|
1
|
+
{"version":3,"file":"usePrintJob.d.ts","sourceRoot":"","sources":["../../src/hooks/usePrintJob.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,GAAG,EAAE,YAAY,GAAG,IAAI,CAAC;CAC1B;AAED;;;;GAIG;AACH,wBAAgB,WAAW;;kBAKhB;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;;EA2D/E"}
|
|
@@ -21,26 +21,37 @@ export function usePrintJob() {
|
|
|
21
21
|
if (!jobId)
|
|
22
22
|
return;
|
|
23
23
|
let stopped = false;
|
|
24
|
+
let timer = null;
|
|
25
|
+
const stopPolling = () => {
|
|
26
|
+
if (timer !== null) {
|
|
27
|
+
window.clearInterval(timer);
|
|
28
|
+
timer = null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
24
31
|
const read = async () => {
|
|
25
32
|
try {
|
|
26
33
|
const job = await pickApi.printJobStatus(jobId);
|
|
27
34
|
if (stopped || jobIdRef.current !== jobId)
|
|
28
35
|
return;
|
|
29
36
|
setTracked((prev) => (prev && prev.jobId === jobId ? { ...prev, job } : prev));
|
|
37
|
+
// Eindstatus bereikt: niet meer blijven pollen.
|
|
38
|
+
if (job && ["voltooid", "mislukt", "geannuleerd"].includes(job.status))
|
|
39
|
+
stopPolling();
|
|
30
40
|
}
|
|
31
41
|
catch {
|
|
32
42
|
// status tijdelijk niet leesbaar; volgende poging volgt
|
|
33
43
|
}
|
|
34
44
|
};
|
|
35
45
|
void read();
|
|
36
|
-
|
|
46
|
+
// Realtime is de bron; de poll is alleen terugval als er niets binnenkomt.
|
|
47
|
+
timer = window.setInterval(read, 10000);
|
|
37
48
|
const channel = pickDb
|
|
38
49
|
.channel(`print_job_${jobId}`)
|
|
39
50
|
.on("postgres_changes", { event: "*", schema: "public", table: "print_jobs", filter: `id=eq.${jobId}` }, () => void read())
|
|
40
51
|
.subscribe();
|
|
41
52
|
return () => {
|
|
42
53
|
stopped = true;
|
|
43
|
-
|
|
54
|
+
stopPolling();
|
|
44
55
|
pickDb.removeChannel(channel);
|
|
45
56
|
};
|
|
46
57
|
}, [jobId]);
|