@flowselections/pick-schermen-kwekers 1.0.89 → 1.0.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-lib/components/PickPublicPage.d.ts.map +1 -1
- package/dist-lib/components/PickPublicPage.js +20 -2
- package/dist-lib/components/PickstationsPage.d.ts.map +1 -1
- package/dist-lib/components/PickstationsPage.js +10 -3
- package/dist-lib/components/pick/PickScherm.d.ts.map +1 -1
- package/dist-lib/components/pick/PickScherm.js +5 -0
- package/dist-lib/components/pick/productie/ProductieScherm.d.ts +8 -0
- package/dist-lib/components/pick/productie/ProductieScherm.d.ts.map +1 -0
- package/dist-lib/components/pick/productie/ProductieScherm.js +107 -0
- package/dist-lib/components/pick/productie/StationProductieInfo.d.ts +9 -0
- package/dist-lib/components/pick/productie/StationProductieInfo.d.ts.map +1 -0
- package/dist-lib/components/pick/productie/StationProductieInfo.js +72 -0
- package/dist-lib/integrations/supabase/types.d.ts +2595 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/__tests__/productiebon.test.d.ts +2 -0
- package/dist-lib/lib/__tests__/productiebon.test.d.ts.map +1 -0
- package/dist-lib/lib/__tests__/productiebon.test.js +105 -0
- package/dist-lib/lib/pickTypes.d.ts +2 -0
- package/dist-lib/lib/pickTypes.d.ts.map +1 -1
- package/dist-lib/lib/productiebon.d.ts +131 -0
- package/dist-lib/lib/productiebon.d.ts.map +1 -0
- package/dist-lib/lib/productiebon.js +255 -0
- package/package.json +9 -1
- package/schema/pickschermen.sql +558 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PickPublicPage.d.ts","sourceRoot":"","sources":["../../src/components/PickPublicPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PickPublicPage.d.ts","sourceRoot":"","sources":["../../src/components/PickPublicPage.tsx"],"names":[],"mappings":"AAUA,UAAU,KAAK;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,+DAA+D;AAC/D,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,EAAE,KAAK,+BA8F7C"}
|
|
@@ -6,6 +6,8 @@ import { pickApi } from "../lib/pickApi.js";
|
|
|
6
6
|
import { fetchDeviceStatus, mustPair, readDeviceKey, setActiveDeviceKey } from "../lib/pick159.js";
|
|
7
7
|
import { PickScherm } from "./pick/PickScherm.js";
|
|
8
8
|
import { StationPairing } from "./pick/StationPairing.js";
|
|
9
|
+
import { ProductieScherm } from "./pick/productie/ProductieScherm.js";
|
|
10
|
+
import { productieApi } from "../lib/productiebon.js";
|
|
9
11
|
/** Openbaar pickscherm, bereikbaar via /pickstation/<slug>. */
|
|
10
12
|
export function PickPublicPage({ slug }) {
|
|
11
13
|
const [deviceKey, setDeviceKey] = useState(null);
|
|
@@ -32,6 +34,22 @@ export function PickPublicPage({ slug }) {
|
|
|
32
34
|
queryFn: () => pickApi.stationBySlug(slug),
|
|
33
35
|
enabled: keyRead,
|
|
34
36
|
});
|
|
37
|
+
// Productiestation: volgens de Engine-instelling (Werkwijze → Pickstations → Productiebon).
|
|
38
|
+
// Lukt het lezen niet (bijv. apparaat niet gekoppeld), dan het gewone pickscherm.
|
|
39
|
+
const { data: isProductie, isLoading: productieLoading } = useQuery({
|
|
40
|
+
queryKey: ["pick", "productie-station", slug, deviceKey],
|
|
41
|
+
queryFn: async () => {
|
|
42
|
+
// Alleen het station dat de Engine als productiestation heeft gekozen; nooit afleiden uit de wachtrij.
|
|
43
|
+
try {
|
|
44
|
+
return (await productieApi.station(slug)).productiestation;
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
console.error("[productiebon] stationcontrole", e);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
enabled: keyRead && !!deviceKey,
|
|
52
|
+
});
|
|
35
53
|
const stationName = typeof station?.name === "string" && station.name.trim() ? station.name.trim() : null;
|
|
36
54
|
// Tabbladtitel = schermnaam. Vorige en eigen titel per effect-run vastleggen (geen gedeelde ref),
|
|
37
55
|
// en alleen terugzetten als niemand anders de titel inmiddels heeft gewijzigd.
|
|
@@ -44,7 +62,7 @@ export function PickPublicPage({ slug }) {
|
|
|
44
62
|
document.title = previous;
|
|
45
63
|
};
|
|
46
64
|
}, [stationName]);
|
|
47
|
-
if (isLoading || !keyRead || statusLoading) {
|
|
65
|
+
if (isLoading || !keyRead || statusLoading || (!!deviceKey && productieLoading)) {
|
|
48
66
|
return (_jsx("div", { className: "flex min-h-screen items-center justify-center bg-background", children: _jsx(Loader2, { className: "h-8 w-8 animate-spin text-primary" }) }));
|
|
49
67
|
}
|
|
50
68
|
if (mustPair(deviceStatus ?? null) || (qrCode && !deviceStatus?.gekoppeld)) {
|
|
@@ -58,5 +76,5 @@ export function PickPublicPage({ slug }) {
|
|
|
58
76
|
if (!station) {
|
|
59
77
|
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." }) }));
|
|
60
78
|
}
|
|
61
|
-
return (_jsx("div", { className: "min-h-screen bg-background p-3 sm:p-6", children: _jsx(PickScherm, { station: station }) }));
|
|
79
|
+
return (_jsx("div", { className: "min-h-screen bg-background p-3 sm:p-6", children: isProductie ? _jsx(ProductieScherm, { slug: slug, stationName: stationName ?? "Pickstation" }) : _jsx(PickScherm, { station: station }) }));
|
|
62
80
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PickstationsPage.d.ts","sourceRoot":"","sources":["../../src/components/PickstationsPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PickstationsPage.d.ts","sourceRoot":"","sources":["../../src/components/PickstationsPage.tsx"],"names":[],"mappings":"AAuHA,wBAAgB,gBAAgB,gCAyU/B"}
|
|
@@ -9,6 +9,7 @@ import { useKoppelingVoorBron } from "../hooks/useKoppelingVoorBron.js";
|
|
|
9
9
|
import { ReferentiedataBanner } from "./pick/ReferentiedataBanner.js";
|
|
10
10
|
import { DOCUMENT_AUTO_TEXT } from "../lib/pick156.js";
|
|
11
11
|
import { StationDevices } from "./pick/StationDevices.js";
|
|
12
|
+
import { StationProductieInfo } from "./pick/productie/StationProductieInfo.js";
|
|
12
13
|
import { plainError } from "../lib/labels.js";
|
|
13
14
|
import { PageHeader, KbdHint } from "./fs-ui/PageHeader.js";
|
|
14
15
|
import { UpdatedAt } from "./fs-ui/UpdatedAt.js";
|
|
@@ -107,7 +108,12 @@ export function PickstationsPage() {
|
|
|
107
108
|
setPanelId(null);
|
|
108
109
|
queryClient.invalidateQueries({ queryKey: ["pick", "stations"] });
|
|
109
110
|
},
|
|
110
|
-
onError: (e) =>
|
|
111
|
+
onError: (e) => {
|
|
112
|
+
console.error("[pickstations] verwijderen", e);
|
|
113
|
+
// De database legt uit waarom (standaardstation of nog in gebruik); die tekst ongewijzigd tonen.
|
|
114
|
+
const msg = e instanceof Error ? e.message : "";
|
|
115
|
+
toast.error(/^Dit (station is nog in gebruik|is een standaardstation)/.test(msg) ? msg : plainError(e, "Verwijderen lukt niet, probeer het opnieuw."));
|
|
116
|
+
},
|
|
111
117
|
});
|
|
112
118
|
const copyLink = async (station) => {
|
|
113
119
|
const url = publicUrl(station.slug);
|
|
@@ -173,7 +179,8 @@ export function PickstationsPage() {
|
|
|
173
179
|
{ label: "Link kopiëren", onSelect: () => void copyLink(s) },
|
|
174
180
|
{ label: "Printplek en apparaten", onSelect: () => setPanelId(s.id) },
|
|
175
181
|
{ label: "Wijzigen", onSelect: () => edit(s) },
|
|
176
|
-
|
|
182
|
+
// Standaardstations kun je niet verwijderen (alleen hernoemen of uitzetten).
|
|
183
|
+
...(s.system_code ? [] : [{ label: "Verwijderen", onSelect: () => setConfirmDelete(s), destructive: true, separatorBefore: true }]),
|
|
177
184
|
] }));
|
|
178
185
|
const columns = [
|
|
179
186
|
{ key: "name", header: "Station", width: COLS[0], cell: (s) => _jsx(TwoLine, { top: s.name, bottom: s.description ?? "" }) },
|
|
@@ -192,7 +199,7 @@ export function PickstationsPage() {
|
|
|
192
199
|
{ id: "active", label: "Actief", count: counts.active },
|
|
193
200
|
{ id: "inactive", label: "Uitgezet", count: counts.inactive },
|
|
194
201
|
{ id: "all", label: "Alle", count: counts.all },
|
|
195
|
-
] }), _jsx(Toolbar, { search: _jsx(SearchInput, { ref: searchRef, value: search, onChange: setSearch, placeholder: "Zoek op naam of omschrijving" }), filterCount: chips.length, onOpenFilters: () => setFiltersOpen(true), chips: _jsx(FilterChips, { chips: chips, onClearAll: clearAll }) }), _jsx("div", { className: "w-full min-w-0 overflow-clip rounded-xl border border-border bg-card shadow-sm", children: isLoading ? (_jsx(TableSkeleton, { cols: COLS })) : rows.length === 0 ? (_jsx(EmptyState, { icon: _jsx(Warehouse, { className: "h-6 w-6" }), title: safeStations.length === 0 ? "Nog geen pickstations" : "Geen pickstations gevonden", text: safeStations.length === 0 ? "Maak een pickstation en deel de link met de medewerkers." : "Pas de zoekterm of filters aan.", action: safeStations.length === 0 ? (_jsxs(Button, { size: "sm", onClick: () => setDraft({ ...emptyDraft }), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), " Nieuw pickstation"] })) : chips.length > 0 || search ? (_jsx(Button, { size: "sm", variant: "outline", onClick: () => { clearAll(); setSearch(""); }, children: "Alles wissen" })) : undefined })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "hidden md:block", children: _jsx(DataTable, { label: "Pickstations", columns: columns, rows: pageRows, rowKey: (s) => s.id, focusedKey: focused, onOpen: (s) => { setFocused(s.id); setPanelId(s.id); } }) }), _jsx("ul", { className: "divide-y divide-border md:hidden", children: pageRows.map((s) => (_jsxs("li", { className: "flex items-center gap-3 p-4", onClick: () => setPanelId(s.id), children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "truncate text-sm font-semibold text-foreground", children: s.name }), _jsx("div", { className: "truncate text-xs text-muted-foreground", children: s.description || `/pickstation/${s.slug ?? ""}` }), _jsx("div", { className: "mt-1", children: statusIcon(s) })] }), menu(s)] }, s.id))) })] })) }), !isLoading && rows.length > 0 && (_jsx(Pagination, { page: page, pageSize: pageSize, total: rows.length, onPage: setPage, onPageSize: setPageSize })), _jsxs(MobilePrimaryAction, { label: "Nieuw pickstation", onClick: () => setDraft({ ...emptyDraft }), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), " Nieuw pickstation"] }), _jsxs(FiltersSheet, { open: filtersOpen, onOpenChange: setFiltersOpen, onClearAll: clearAll, children: [_jsx(YesNoField, { id: "f-multi", label: "Meerdere schermen toegestaan", value: multi, onChange: setMulti }), _jsx(YesNoField, { id: "f-country", label: "Land kiezen verplicht", value: country, onChange: setCountry })] }), _jsx(SidePanel, { open: panelStation !== null, onOpenChange: (o) => !o && setPanelId(null), title: panelStation?.name ?? "Pickstation", footer: panelStation && (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => edit(panelStation), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), " Wijzigen"] }), _jsx(Button, { size: "sm", asChild: true, children: _jsxs("a", { href: publicUrl(panelStation.slug), target: "_blank", rel: "noreferrer", children: [_jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), " Pickstation openen"] }) })] })), children: panelStation && (_jsxs(_Fragment, { children: [panelStation.description && _jsx("p", { className: "text-sm text-muted-foreground", children: panelStation.description }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Link voor medewerkers" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("code", { className: "min-w-0 flex-1 truncate rounded bg-muted px-2 py-1.5 text-xs text-muted-foreground", children: publicUrl(panelStation.slug) }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void copyLink(panelStation), children: [_jsx(Copy, { className: "mr-2 h-4 w-4" }), " Kopi\u00EBren"] })] })] }), _jsx(StationPrintplek, { station: panelStation }), _jsx("p", { className: "text-sm text-muted-foreground", children: DOCUMENT_AUTO_TEXT }), _jsx(StationDevices, { stationId: panelStation.id, slug: panelStation.slug ?? null })] })) }), _jsx(ShortcutHelp, { open: helpOpen, onOpenChange: setHelpOpen, shortcuts: SHORTCUTS }), _jsx(AlertDialog, { open: confirmDelete !== null, onOpenChange: (o) => !o && setConfirmDelete(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Pickstation verwijderen?" }), _jsxs(AlertDialogDescription, { children: [confirmDelete?.name, " verdwijnt, en de link werkt daarna niet meer. Dit kun je niet ongedaan maken."] })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsxs(AlertDialogAction, { className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", onClick: (e) => {
|
|
202
|
+
] }), _jsx(Toolbar, { search: _jsx(SearchInput, { ref: searchRef, value: search, onChange: setSearch, placeholder: "Zoek op naam of omschrijving" }), filterCount: chips.length, onOpenFilters: () => setFiltersOpen(true), chips: _jsx(FilterChips, { chips: chips, onClearAll: clearAll }) }), _jsx("div", { className: "w-full min-w-0 overflow-clip rounded-xl border border-border bg-card shadow-sm", children: isLoading ? (_jsx(TableSkeleton, { cols: COLS })) : rows.length === 0 ? (_jsx(EmptyState, { icon: _jsx(Warehouse, { className: "h-6 w-6" }), title: safeStations.length === 0 ? "Nog geen pickstations" : "Geen pickstations gevonden", text: safeStations.length === 0 ? "Maak een pickstation en deel de link met de medewerkers." : "Pas de zoekterm of filters aan.", action: safeStations.length === 0 ? (_jsxs(Button, { size: "sm", onClick: () => setDraft({ ...emptyDraft }), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), " Nieuw pickstation"] })) : chips.length > 0 || search ? (_jsx(Button, { size: "sm", variant: "outline", onClick: () => { clearAll(); setSearch(""); }, children: "Alles wissen" })) : undefined })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "hidden md:block", children: _jsx(DataTable, { label: "Pickstations", columns: columns, rows: pageRows, rowKey: (s) => s.id, focusedKey: focused, onOpen: (s) => { setFocused(s.id); setPanelId(s.id); } }) }), _jsx("ul", { className: "divide-y divide-border md:hidden", children: pageRows.map((s) => (_jsxs("li", { className: "flex items-center gap-3 p-4", onClick: () => setPanelId(s.id), children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "truncate text-sm font-semibold text-foreground", children: s.name }), _jsx("div", { className: "truncate text-xs text-muted-foreground", children: s.description || `/pickstation/${s.slug ?? ""}` }), _jsx("div", { className: "mt-1", children: statusIcon(s) })] }), menu(s)] }, s.id))) })] })) }), !isLoading && rows.length > 0 && (_jsx(Pagination, { page: page, pageSize: pageSize, total: rows.length, onPage: setPage, onPageSize: setPageSize })), _jsxs(MobilePrimaryAction, { label: "Nieuw pickstation", onClick: () => setDraft({ ...emptyDraft }), children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), " Nieuw pickstation"] }), _jsxs(FiltersSheet, { open: filtersOpen, onOpenChange: setFiltersOpen, onClearAll: clearAll, children: [_jsx(YesNoField, { id: "f-multi", label: "Meerdere schermen toegestaan", value: multi, onChange: setMulti }), _jsx(YesNoField, { id: "f-country", label: "Land kiezen verplicht", value: country, onChange: setCountry })] }), _jsx(SidePanel, { open: panelStation !== null, onOpenChange: (o) => !o && setPanelId(null), title: panelStation?.name ?? "Pickstation", footer: panelStation && (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: () => edit(panelStation), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), " Wijzigen"] }), _jsx(Button, { size: "sm", asChild: true, children: _jsxs("a", { href: publicUrl(panelStation.slug), target: "_blank", rel: "noreferrer", children: [_jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), " Pickstation openen"] }) })] })), children: panelStation && (_jsxs(_Fragment, { children: [panelStation.description && _jsx("p", { className: "text-sm text-muted-foreground", children: panelStation.description }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Link voor medewerkers" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("code", { className: "min-w-0 flex-1 truncate rounded bg-muted px-2 py-1.5 text-xs text-muted-foreground", children: publicUrl(panelStation.slug) }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void copyLink(panelStation), children: [_jsx(Copy, { className: "mr-2 h-4 w-4" }), " Kopi\u00EBren"] })] })] }), _jsx(StationPrintplek, { station: panelStation }), _jsx("p", { className: "text-sm text-muted-foreground", children: DOCUMENT_AUTO_TEXT }), _jsx(StationProductieInfo, { stationId: panelStation.id, active: panelStation.active }), _jsx(StationDevices, { stationId: panelStation.id, slug: panelStation.slug ?? null })] })) }), _jsx(ShortcutHelp, { open: helpOpen, onOpenChange: setHelpOpen, shortcuts: SHORTCUTS }), _jsx(AlertDialog, { open: confirmDelete !== null, onOpenChange: (o) => !o && setConfirmDelete(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Pickstation verwijderen?" }), _jsxs(AlertDialogDescription, { children: [confirmDelete?.name, " verdwijnt, en de link werkt daarna niet meer. Dit kun je niet ongedaan maken."] })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsxs(AlertDialogAction, { className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", onClick: (e) => {
|
|
196
203
|
e.preventDefault();
|
|
197
204
|
if (confirmDelete)
|
|
198
205
|
remove.mutate(confirmDelete.id);
|
|
@@ -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":"AAsFA,OAAO,EAaL,KAAK,WAAW,EACjB,MAAM,qBAAqB,CAAC;AAwF7B,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAGD,wCAAwC;AACxC,eAAO,MAAM,cAAc,GAAI,WAAW,MAAM,WAAuC,CAAC;AAsBxF;;;GAGG;AACH,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,KAAK,sCA47DpD"}
|
|
@@ -1,5 +1,6 @@
|
|
|
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 { isProductieBon, PRODUCTIEBON_ELDERS } from "../../lib/productiebon.js";
|
|
3
4
|
import { useQuery } from "@tanstack/react-query";
|
|
4
5
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, toast, } from "@flowselections/core";
|
|
5
6
|
import { ArrowLeft, ChevronLeft, ChevronRight, FileDown, Globe, Loader2, MoreHorizontal, Printer, Search, } from "lucide-react";
|
|
@@ -465,6 +466,10 @@ export function PickScherm({ station, onBack }) {
|
|
|
465
466
|
else if (r?.error && isRowGoneError(r.error)) {
|
|
466
467
|
entry = { checking: false, ready: false, reason: String(r.error), replaced: true, waitUntil: null };
|
|
467
468
|
}
|
|
469
|
+
// Productiebon op een gewoon station: nooit printen hier, wel uitleggen waar hij hoort.
|
|
470
|
+
if (isProductieBon(item.payload)) {
|
|
471
|
+
entry = { checking: false, ready: false, reason: PRODUCTIEBON_ELDERS, waitUntil: null, notPrintable: true, notStarted: PRODUCTIEBON_ELDERS };
|
|
472
|
+
}
|
|
468
473
|
next[item.id] = entry;
|
|
469
474
|
verifyMemo.current.set(item.id, {
|
|
470
475
|
fingerprint: rowFingerprint(item),
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
slug: string;
|
|
3
|
+
stationName: string;
|
|
4
|
+
};
|
|
5
|
+
/** Werkvloerscherm voor een pickstation dat productiebonnen van de Engine krijgt. */
|
|
6
|
+
export declare function ProductieScherm({ slug, stationName }: Props): import("react").JSX.Element;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=ProductieScherm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductieScherm.d.ts","sourceRoot":"","sources":["../../../../src/components/pick/productie/ProductieScherm.tsx"],"names":[],"mappings":"AAuBA,KAAK,KAAK,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnD,qFAAqF;AACrF,wBAAgB,eAAe,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,KAAK,+BAyM3D"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { CheckCircle2, Clock, ImageOff, Loader2, Printer, RotateCcw, ShieldCheck } from "lucide-react";
|
|
5
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, Button, } from "@flowselections/core";
|
|
6
|
+
import { MeldingsPlek } from "../werkvloer/Werkvloer.js";
|
|
7
|
+
import { herprint, newKey, printEnVerwerk, productieApi, uitkomstTekst, } from "../../../lib/productiebon.js";
|
|
8
|
+
const fmtDag = (iso) => {
|
|
9
|
+
if (!iso)
|
|
10
|
+
return "";
|
|
11
|
+
const d = new Date(iso);
|
|
12
|
+
if (Number.isNaN(d.getTime()))
|
|
13
|
+
return "";
|
|
14
|
+
const tz = "Europe/Amsterdam";
|
|
15
|
+
const dag = new Intl.DateTimeFormat("nl-NL", { timeZone: tz, weekday: "short", day: "numeric", month: "short" }).format(d).replace(/\./g, "");
|
|
16
|
+
const tijd = new Intl.DateTimeFormat("nl-NL", { timeZone: tz, hour: "2-digit", minute: "2-digit" }).format(d);
|
|
17
|
+
return `${dag} ${tijd}`;
|
|
18
|
+
};
|
|
19
|
+
const fmtN = (n) => (n === null ? "?" : new Intl.NumberFormat("nl-NL", { maximumFractionDigits: 3 }).format(n));
|
|
20
|
+
/** Werkvloerscherm voor een pickstation dat productiebonnen van de Engine krijgt. */
|
|
21
|
+
export function ProductieScherm({ slug, stationName }) {
|
|
22
|
+
const qc = useQueryClient();
|
|
23
|
+
const [tab, setTab] = useState("todo");
|
|
24
|
+
const [slot, setSlot] = useState({ kind: "none" });
|
|
25
|
+
const [busyId, setBusyId] = useState(null);
|
|
26
|
+
const [ask, setAsk] = useState(null);
|
|
27
|
+
const jobs = useRef(new Map());
|
|
28
|
+
const lijst = useQuery({
|
|
29
|
+
queryKey: ["pick", "productie", slug, tab],
|
|
30
|
+
queryFn: () => productieApi.lijst(slug, tab === "done"),
|
|
31
|
+
refetchInterval: 10000,
|
|
32
|
+
placeholderData: (p) => p,
|
|
33
|
+
});
|
|
34
|
+
const items = Array.isArray(lijst.data?.items) ? lijst.data.items : [];
|
|
35
|
+
const geenPrintplek = lijst.isSuccess && !lijst.data?.printPlekId;
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (lijst.error)
|
|
38
|
+
console.error("[productiebon] lijst", lijst.error);
|
|
39
|
+
}, [lijst.error]);
|
|
40
|
+
const refresh = useCallback(() => void qc.invalidateQueries({ queryKey: ["pick", "productie", slug] }), [qc, slug]);
|
|
41
|
+
const deps = (bon, herprintMode) => ({
|
|
42
|
+
print: (key) => productieApi.print(slug, bon.productieOrderId, key, herprintMode),
|
|
43
|
+
status: (jobId) => productieApi.status(slug, jobId),
|
|
44
|
+
verwerk: () => productieApi.verwerk(slug, bon.productieOrderId),
|
|
45
|
+
newKey,
|
|
46
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
47
|
+
now: () => Date.now(),
|
|
48
|
+
onJob: (j) => jobs.current.set(bon.productieOrderId, j),
|
|
49
|
+
});
|
|
50
|
+
const run = async (bon, soort) => {
|
|
51
|
+
if (busyId)
|
|
52
|
+
return;
|
|
53
|
+
setBusyId(bon.productieOrderId);
|
|
54
|
+
setSlot({ kind: "busy", lines: [`${bon.nummer} · ${bon.samenstelling}`] });
|
|
55
|
+
try {
|
|
56
|
+
if (soort === "herprint") {
|
|
57
|
+
const u = await herprint(deps(bon, true));
|
|
58
|
+
setSlot({ kind: u.kind === "geprint" ? "success" : "error", text: uitkomstTekst(u, bon.aantal, bon.samenstelling) });
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const bekend = jobs.current.get(bon.productieOrderId) ?? bon.laatsteJob;
|
|
62
|
+
const u = await printEnVerwerk({ laatsteJob: bekend }, deps(bon, false));
|
|
63
|
+
const tekst = uitkomstTekst(u, bon.aantal, bon.samenstelling);
|
|
64
|
+
if (u.kind === "verwerkt") {
|
|
65
|
+
setSlot({ kind: "success", text: u.waarschuwingen.length ? `${tekst}. Let op: ${u.waarschuwingen.join(" · ")}` : tekst });
|
|
66
|
+
}
|
|
67
|
+
else if (u.kind === "al_verwerkt")
|
|
68
|
+
setSlot({ kind: "info", text: tekst });
|
|
69
|
+
else
|
|
70
|
+
setSlot({ kind: "error", text: tekst });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
console.error("[productiebon] actie", e);
|
|
75
|
+
setSlot({ kind: "error", text: "Er ging iets mis. Probeer het opnieuw of neem contact op met de beheerder." });
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
setBusyId(null);
|
|
79
|
+
refresh();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const goedkeuren = async (bon) => {
|
|
83
|
+
if (busyId)
|
|
84
|
+
return;
|
|
85
|
+
setBusyId(bon.productieOrderId);
|
|
86
|
+
try {
|
|
87
|
+
const r = await productieApi.goedkeuren(slug, bon.queueId);
|
|
88
|
+
setSlot({ kind: r.ok ? "success" : "error", text: r.melding ?? (r.ok ? "Goedgekeurd." : "Goedkeuren is niet gelukt.") });
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
console.error("[productiebon] goedkeuren", e);
|
|
92
|
+
setSlot({ kind: "error", text: "Goedkeuren is niet gelukt. Probeer het opnieuw of neem contact op met de beheerder." });
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
setBusyId(null);
|
|
96
|
+
refresh();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
return (_jsxs("div", { className: "space-y-4 pb-8 text-lg", children: [_jsx("header", { className: "flex items-center gap-3", children: _jsx("h2", { className: "min-w-0 flex-1 truncate text-2xl font-bold text-foreground", children: stationName }) }), _jsx("div", { className: "grid grid-cols-2 gap-2", children: ["todo", "done"].map((t) => (_jsx(Button, { variant: tab === t ? "default" : "outline", className: "h-14 text-lg", onClick: () => setTab(t), children: t === "todo" ? "Te doen" : "Geprint vandaag" }, t))) }), _jsx(MeldingsPlek, { slot: slot, onDismiss: () => setSlot({ kind: "none" }) }), geenPrintplek && (_jsxs("div", { role: "alert", className: "rounded-lg border-2 border-destructive bg-muted p-4 text-lg", children: ["Dit station heeft geen printer. Koppel een printplek in de", " ", _jsx("a", { className: "font-semibold text-primary underline", href: "/pickstations", children: "stationinstellingen" }), "."] })), lijst.isError && (_jsx("p", { role: "alert", className: "rounded-lg bg-muted p-4 text-lg text-foreground", children: "De productiebonnen konden niet worden geladen. Controleer of dit apparaat gekoppeld is, of neem contact op met de beheerder." })), lijst.isLoading && _jsx(Loader2, { className: "mx-auto h-8 w-8 animate-spin text-primary" }), lijst.isSuccess && items.length === 0 && (_jsx("p", { className: "py-10 text-center text-xl text-muted-foreground", children: tab === "todo" ? "Geen productiebonnen" : "Vandaag is er nog niets geprint." })), _jsx("ul", { className: "space-y-3", children: items.map((bon) => (_jsxs("li", { className: "rounded-lg border bg-card p-4 text-card-foreground", children: [_jsxs("div", { className: "flex gap-4", children: [bon.foto ? (_jsx("img", { src: bon.foto, alt: bon.samenstelling, className: "h-24 w-24 shrink-0 rounded-md object-cover" })) : (_jsx("div", { className: "flex h-24 w-24 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground", children: _jsx(ImageOff, { className: "h-8 w-8", "aria-label": "Geen foto" }) })), _jsxs("div", { className: "min-w-0 flex-1 space-y-1", children: [_jsxs("p", { className: "text-xl font-bold", children: [bon.nummer, " \u00B7 ", fmtN(bon.aantal), " \u00D7 ", bon.samenstelling || "Onbekende samenstelling"] }), _jsxs("p", { className: "text-base text-muted-foreground", children: ["Aangemaakt ", fmtDag(bon.aangemaaktOp)] }), bon.opmerking && (_jsxs("p", { className: "rounded-md border-l-4 border-primary bg-muted px-3 py-2 text-lg font-semibold", children: ["Opmerking: ", bon.opmerking] }))] })] }), bon.onderdelen.length > 0 && (_jsxs("table", { className: "mt-3 w-full text-lg", children: [_jsx("thead", { children: _jsxs("tr", { className: "text-left text-base text-muted-foreground", children: [_jsx("th", { className: "py-1 pl-2 font-medium", children: "Onderdeel" }), _jsx("th", { className: "py-1 text-right font-medium", children: "Per stuk" }), _jsx("th", { className: "py-1 pr-2 text-right font-medium", children: "Totaal" })] }) }), _jsx("tbody", { children: bon.onderdelen.map((o) => (_jsxs("tr", { className: "border-t", children: [_jsx("td", { className: "py-1 pl-2", children: o.naam }), _jsx("td", { className: "py-1 text-right tabular-nums", children: fmtN(o.perStuk) }), _jsx("td", { className: "py-1 pr-2 text-right tabular-nums", children: fmtN(o.totaal) })] }, o.naam))) })] })), _jsx("div", { className: "mt-3", children: bon.wachtOpGoedkeuring ? (_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsxs("span", { className: "flex items-center gap-2 text-lg font-semibold", children: [_jsx(Clock, { className: "h-6 w-6" }), " Wacht op goedkeuring"] }), _jsxs(Button, { className: "h-14 px-6 text-lg", disabled: busyId !== null, onClick: () => void goedkeuren(bon), children: [_jsx(ShieldCheck, { className: "mr-2 h-6 w-6" }), " Goedkeuren"] })] })) : tab === "done" || bon.afgerond ? (_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsxs("span", { className: "flex items-center gap-2 text-lg font-semibold", children: [_jsx(CheckCircle2, { className: "h-6 w-6 text-primary" }), " Verwerkt"] }), _jsxs(Button, { variant: "outline", className: "h-16 px-6 text-lg", disabled: busyId !== null || geenPrintplek, onClick: () => setAsk({ bon, soort: "herprint" }), children: [_jsx(RotateCcw, { className: "mr-2 h-6 w-6" }), " Opnieuw printen"] })] })) : (_jsxs(Button, { className: "h-16 w-full text-xl font-bold", disabled: busyId !== null || geenPrintplek, onClick: () => setAsk({ bon, soort: "verwerk" }), children: [busyId === bon.productieOrderId ? _jsx(Loader2, { className: "mr-3 h-7 w-7 animate-spin" }) : _jsx(Printer, { className: "mr-3 h-7 w-7" }), busyId === bon.productieOrderId ? "Bezig met printen…" : "Bon printen en verwerken"] })) })] }, bon.queueId))) }), _jsx(AlertDialog, { open: ask !== null, onOpenChange: (o) => !o && setAsk(null), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { className: "text-2xl", children: ask?.soort === "herprint" ? "Bon opnieuw printen?" : "Bon printen en verwerken?" }), _jsx(AlertDialogDescription, { className: "text-lg", children: ask?.soort === "herprint"
|
|
100
|
+
? "De bon wordt opnieuw geprint. Er wordt niets geboekt."
|
|
101
|
+
: `Dit boekt ${fmtN(ask?.bon.aantal ?? null)} × ${ask?.bon.samenstelling ?? ""} bij en de onderdelen af.` })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { className: "h-14 text-lg", children: "Annuleren" }), _jsx(AlertDialogAction, { className: "h-14 text-lg", onClick: () => {
|
|
102
|
+
const a = ask;
|
|
103
|
+
setAsk(null);
|
|
104
|
+
if (a)
|
|
105
|
+
void run(a.bon, a.soort);
|
|
106
|
+
}, children: ask?.soort === "herprint" ? "Opnieuw printen" : "Printen en verwerken" })] })] }) })] }));
|
|
107
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Beheerpagina: laat zien of de Engine productiebonnen naar dit station stuurt.
|
|
3
|
+
* Admin/superadmin kan een open bon verwerken zonder bon.
|
|
4
|
+
*/
|
|
5
|
+
export declare function StationProductieInfo({ stationId, active }: {
|
|
6
|
+
stationId: string;
|
|
7
|
+
active?: boolean;
|
|
8
|
+
}): import("react").JSX.Element | null;
|
|
9
|
+
//# sourceMappingURL=StationProductieInfo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StationProductieInfo.d.ts","sourceRoot":"","sources":["../../../../src/components/pick/productie/StationProductieInfo.tsx"],"names":[],"mappings":"AAQA;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,MAAa,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,sCAqFzG"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import { supabase, useAuth, Button } from "@flowselections/core";
|
|
6
|
+
/**
|
|
7
|
+
* Beheerpagina: laat zien of de Engine productiebonnen naar dit station stuurt.
|
|
8
|
+
* Admin/superadmin kan een open bon verwerken zonder bon.
|
|
9
|
+
*/
|
|
10
|
+
export function StationProductieInfo({ stationId, active = true }) {
|
|
11
|
+
const { roles } = useAuth();
|
|
12
|
+
const isAdmin = Array.isArray(roles) && (roles.includes("admin") || roles.includes("superadmin"));
|
|
13
|
+
const qc = useQueryClient();
|
|
14
|
+
const [busy, setBusy] = useState(null);
|
|
15
|
+
// Welke documenten krijgt dit station volgens de Engine (en eigen wachtrij)? Via pick_station_gebruik.
|
|
16
|
+
const gebruik = useQuery({
|
|
17
|
+
queryKey: ["pick", "station-gebruik", stationId],
|
|
18
|
+
queryFn: async () => {
|
|
19
|
+
const { data, error } = await supabase.rpc("pick_station_gebruik", { p_station_ids: [stationId] });
|
|
20
|
+
if (error)
|
|
21
|
+
throw error;
|
|
22
|
+
const g = data?.[stationId];
|
|
23
|
+
return Array.isArray(g?.documenten) ? g.documenten.filter((d) => typeof d === "string") : [];
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
if (gebruik.isError)
|
|
27
|
+
console.error("[productiebon] stationgebruik", gebruik.error);
|
|
28
|
+
const documenten = Array.isArray(gebruik.data) ? gebruik.data : [];
|
|
29
|
+
const krijgtProductie = documenten.includes("productiebonnen");
|
|
30
|
+
const q = useQuery({
|
|
31
|
+
queryKey: ["pick", "station-productie", stationId],
|
|
32
|
+
queryFn: async () => {
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
|
+
const { data, error } = await supabase
|
|
35
|
+
.from("pick_queue")
|
|
36
|
+
.select("id, document_number, payload, completed_at")
|
|
37
|
+
.eq("station_id", stationId)
|
|
38
|
+
.not("payload->>productie_order_id", "is", null)
|
|
39
|
+
.is("cancelled_at", null)
|
|
40
|
+
.order("created_at", { ascending: false })
|
|
41
|
+
.limit(25);
|
|
42
|
+
if (error)
|
|
43
|
+
throw error;
|
|
44
|
+
return Array.isArray(data) ? data : [];
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
const rows = Array.isArray(q.data) ? q.data : [];
|
|
48
|
+
if (q.isError)
|
|
49
|
+
console.error("[productiebon] beheer", q.error);
|
|
50
|
+
if (documenten.length === 0 && rows.length === 0)
|
|
51
|
+
return null;
|
|
52
|
+
const open = rows.filter((r) => !r.completed_at);
|
|
53
|
+
const verwerk = async (r) => {
|
|
54
|
+
const id = typeof r.payload?.["productie_order_id"] === "string" ? r.payload["productie_order_id"] : null;
|
|
55
|
+
if (!id || busy)
|
|
56
|
+
return;
|
|
57
|
+
if (!window.confirm(`Productieorder ${r.document_number ?? ""} verwerken zonder bon? Dit boekt voorraad bij en de onderdelen af.`))
|
|
58
|
+
return;
|
|
59
|
+
setBusy(r.id);
|
|
60
|
+
const { data, error } = await supabase.rpc("productie_order_verwerken", { p_id: id });
|
|
61
|
+
setBusy(null);
|
|
62
|
+
if (error) {
|
|
63
|
+
console.error("[productiebon] verwerken zonder bon", error);
|
|
64
|
+
toast.error("Verwerken is niet gelukt. Probeer het opnieuw of neem contact op met de beheerder.");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const st = data?.status;
|
|
68
|
+
toast.success(st === "al_verwerkt" ? "Deze productieorder was al verwerkt." : `Verwerkt: ${r.document_number ?? ""}`);
|
|
69
|
+
void qc.invalidateQueries({ queryKey: ["pick", "station-productie", stationId] });
|
|
70
|
+
};
|
|
71
|
+
return (_jsxs("div", { className: "space-y-2 rounded-md border p-3", children: [documenten.length > 0 && (_jsxs("p", { className: "text-sm font-medium text-foreground", children: ["Dit station krijgt: ", documenten.join(", ")] })), !active && documenten.length > 0 && (_jsxs("p", { role: "alert", className: "text-sm font-medium text-destructive", children: ["Dit station staat uit, maar krijgt nog ", documenten.join(", ")] })), !krijgtProductie && rows.some((r) => !r.completed_at) && (_jsx("p", { className: "text-sm text-muted-foreground", children: "Er staan nog productiebonnen in de wachtrij, maar dit is niet het productiestation." })), isAdmin && open.length > 0 && (_jsx("ul", { className: "space-y-1", children: open.map((r) => (_jsxs("li", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsx("span", { className: "truncate", children: r.document_number ?? "Productiebon" }), _jsx(Button, { size: "sm", variant: "outline", disabled: busy !== null, onClick: () => void verwerk(r), children: "Verwerken zonder bon" })] }, r.id))) }))] }));
|
|
72
|
+
}
|