@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.
Files changed (34) hide show
  1. package/README.md +24 -24
  2. package/dist-lib/components/PickPublicPage.js +1 -1
  3. package/dist-lib/components/TemplateModulePage.d.ts +2 -0
  4. package/dist-lib/components/TemplateModulePage.d.ts.map +1 -0
  5. package/dist-lib/components/TemplateModulePage.js +44 -0
  6. package/dist-lib/components/pick/LanguageTiles.d.ts.map +1 -1
  7. package/dist-lib/components/pick/LanguageTiles.js +3 -3
  8. package/dist-lib/components/pick/PickScherm.d.ts.map +1 -1
  9. package/dist-lib/components/pick/PickScherm.js +71 -10
  10. package/dist-lib/components/pick/QueueTable.d.ts.map +1 -1
  11. package/dist-lib/components/pick/QueueTable.js +22 -12
  12. package/dist-lib/hooks/usePickQueue.d.ts +4 -0
  13. package/dist-lib/hooks/usePickQueue.d.ts.map +1 -1
  14. package/dist-lib/hooks/usePickQueue.js +38 -7
  15. package/dist-lib/hooks/usePrintJob.d.ts.map +1 -1
  16. package/dist-lib/hooks/usePrintJob.js +13 -2
  17. package/dist-lib/integrations/supabase/auth-middleware.d.ts +1788 -311
  18. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  19. package/dist-lib/integrations/supabase/client.d.ts +1788 -311
  20. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  21. package/dist-lib/integrations/supabase/client.server.d.ts +1788 -311
  22. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  23. package/dist-lib/integrations/supabase/types.d.ts +1888 -375
  24. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  25. package/dist-lib/integrations/supabase/types.js +3 -0
  26. package/dist-lib/lib/layoutMaker/pakbonHtml.js +49 -49
  27. package/dist-lib/lib/layoutRenderer.js +17 -17
  28. package/dist-lib/lib/pickApi.d.ts +11 -4
  29. package/dist-lib/lib/pickApi.d.ts.map +1 -1
  30. package/dist-lib/lib/pickApi.js +46 -17
  31. package/dist-lib/lib/pickUiText.d.ts +29 -0
  32. package/dist-lib/lib/pickUiText.d.ts.map +1 -0
  33. package/dist-lib/lib/pickUiText.js +71 -0
  34. 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,2 @@
1
+ export declare function TemplateModulePage(): import("react").JSX.Element;
2
+ //# sourceMappingURL=TemplateModulePage.d.ts.map
@@ -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,+BAuB1D"}
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-4 sm:grid-cols-3 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) => {
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-6 text-center transition-all hover:border-primary hover:shadow-lg focus:border-primary focus:outline-none active:scale-[0.98] " +
10
- (selected === l.code ? "border-primary bg-primary/5" : ""), children: [_jsx("div", { className: "text-5xl leading-none", children: l.flag }), _jsx("div", { className: "mt-3 text-lg font-semibold text-foreground", children: l.label })] }, l.code))) }));
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":"AAwBA,OAAO,KAAK,EAAiB,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAYtE,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,+BAuapD"}
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
- /** Zet een printfout om in een begrijpelijke melding. */
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
- toast.error(e instanceof Error ? e.message : t("notAvailable"));
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 instanceof Error ? e.message : t("noDocument");
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: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-2xl font-bold text-foreground", children: station.name }), _jsx("p", { className: "text-sm text-muted-foreground", children: t("chooseLanguageHint") })] }), onBack && (_jsxs(Button, { variant: "outline", onClick: onBack, children: [_jsx(ArrowLeft, { className: "mr-2 h-4 w-4" }), " ", t("back")] }))] }), _jsx(LanguageTiles, { onSelect: chooseLang })] }));
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 gap-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsxs(Button, { variant: "outline", size: "lg", onClick: () => setLang(null), children: [_jsx(Globe, { className: "mr-2 h-5 w-5" }), " ", t("changeLanguage")] }), _jsxs("div", { children: [_jsx("h2", { className: "text-xl font-bold text-foreground", children: station.name }), _jsxs("p", { className: "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", onClick: refresh, "aria-label": t("refresh"), children: _jsx(RefreshCw, { className: "h-4 w-4" }) }), _jsxs(Button, { size: "lg", className: "h-14 px-8 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" })), 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-wrap items-center justify-between gap-4 p-5", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium text-muted-foreground", children: t("confirmPrintTitle") }), _jsx("p", { className: "text-2xl font-bold text-foreground", children: pending.document_number ?? t("document") }), _jsx("p", { className: "text-sm text-muted-foreground", children: [pending.order_number, pending.customer_name].filter(Boolean).join(" · ") })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "outline", size: "lg", onClick: cancelPending, disabled: busyId !== null, children: t("cancel") }), _jsxs(Button, { size: "lg", className: "h-14 px-8 text-lg", 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-wrap items-center justify-between gap-4 p-4", children: [_jsxs("div", { className: "flex items-center gap-3", children: [jobFailed ? (_jsx(AlertTriangle, { className: "h-6 w-6 text-destructive" })) : jobStatus === "voltooid" ? (_jsx(CheckCircle2, { className: "h-6 w-6 text-primary" })) : (_jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" })), _jsxs("div", { children: [_jsxs("p", { className: "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: "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 gap-2", children: [jobFailed && lastPrinted && (_jsxs(Button, { size: "lg", onClick: () => {
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: _jsx(CardContent, { className: "flex flex-wrap items-center gap-3 p-4", children: _jsxs("div", { className: "relative min-w-64 flex-1", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { className: "pl-9", placeholder: t("search"), value: filters.search, onChange: (e) => setFilters((f) => ({ ...f, search: e.target.value, page: 0 })) })] }) }) }), 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 items-center justify-between", children: [_jsxs("span", { className: "text-sm text-muted-foreground", children: [t("page"), " ", filters.page + 1, " ", t("of"), " ", totalPages] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "outline", disabled: filters.page === 0, onClick: () => setFilters((f) => ({ ...f, page: Math.max(0, f.page - 1) })), children: t("previous") }), _jsx(Button, { variant: "outline", disabled: filters.page + 1 >= totalPages, onClick: () => setFilters((f) => ({ ...f, page: f.page + 1 })), children: t("next") })] })] })] }));
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,+BA8GP"}
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
- 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") }) }))] })] }) }));
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,YAQ5B,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;;;;;;;;;;EA0GA;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"}
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: true,
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: 5000,
77
+ refetchInterval: 20000,
76
78
  });
77
- const invalidate = useCallback(() => {
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", { event: "*", schema: "public", table: "pick_queue" }, invalidate)
90
- .on("postgres_changes", { event: "*", schema: "public", table: "pick_queue_locks" }, invalidate)
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
- refresh: invalidate,
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;;EAgD/E"}
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
- const timer = window.setInterval(read, 2000);
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
- window.clearInterval(timer);
54
+ stopPolling();
44
55
  pickDb.removeChannel(channel);
45
56
  };
46
57
  }, [jobId]);