@flowselections/floriday-voorraad 1.0.145 → 1.0.147
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/voorraad/ArtikelWizard.d.ts +3 -1
- package/dist-lib/components/voorraad/ArtikelWizard.d.ts.map +1 -1
- package/dist-lib/components/voorraad/ArtikelWizard.js +56 -52
- package/dist-lib/components/voorraad/EditArtikelPage.d.ts.map +1 -1
- package/dist-lib/components/voorraad/EditArtikelPage.js +3 -4
- package/dist-lib/components/voorraad/ProductCustomFieldsCard.d.ts.map +1 -1
- package/dist-lib/components/voorraad/ProductCustomFieldsCard.js +29 -13
- package/dist-lib/components/voorraad/ProductSalePriceField.d.ts.map +1 -1
- package/dist-lib/components/voorraad/ProductSalePriceField.js +9 -8
- package/dist-lib/components/voorraad/StepSamenstelling.d.ts.map +1 -1
- package/dist-lib/components/voorraad/StepSamenstelling.js +35 -10
- package/dist-lib/components/voorraad/TrayStickerCard.d.ts +5 -1
- package/dist-lib/components/voorraad/TrayStickerCard.d.ts.map +1 -1
- package/dist-lib/components/voorraad/TrayStickerCard.js +64 -66
- package/dist-lib/components/voorraad/ValidationOverviewTable.d.ts +0 -5
- package/dist-lib/components/voorraad/ValidationOverviewTable.d.ts.map +1 -1
- package/dist-lib/components/voorraad/ValidationOverviewTable.js +10 -4
- package/dist-lib/components/voorraad/VoorraadPage.d.ts +1 -13
- package/dist-lib/components/voorraad/VoorraadPage.d.ts.map +1 -1
- package/dist-lib/components/voorraad/VoorraadPage.js +102 -140
- package/dist-lib/components/voorraad/VoorraadSturingCard.d.ts +6 -0
- package/dist-lib/components/voorraad/VoorraadSturingCard.d.ts.map +1 -0
- package/dist-lib/components/voorraad/VoorraadSturingCard.js +47 -0
- package/dist-lib/components/voorraad/artikel/ArtikelPagina.d.ts +5 -0
- package/dist-lib/components/voorraad/artikel/ArtikelPagina.d.ts.map +1 -0
- package/dist-lib/components/voorraad/artikel/ArtikelPagina.js +263 -0
- package/dist-lib/components/voorraad/artikel/TwijfelKiezen.d.ts +7 -0
- package/dist-lib/components/voorraad/artikel/TwijfelKiezen.d.ts.map +1 -0
- package/dist-lib/components/voorraad/artikel/TwijfelKiezen.js +54 -0
- package/dist-lib/components/voorraad/artikel/opslaan-context.d.ts +21 -0
- package/dist-lib/components/voorraad/artikel/opslaan-context.d.ts.map +1 -0
- package/dist-lib/components/voorraad/artikel/opslaan-context.js +15 -0
- package/dist-lib/components/voorraad/lijst/ImporteerKnop.d.ts +5 -0
- package/dist-lib/components/voorraad/lijst/ImporteerKnop.d.ts.map +1 -0
- package/dist-lib/components/voorraad/lijst/ImporteerKnop.js +20 -0
- package/dist-lib/components/voorraad/lijst/LijstExportKnop.d.ts +6 -0
- package/dist-lib/components/voorraad/lijst/LijstExportKnop.d.ts.map +1 -0
- package/dist-lib/components/voorraad/lijst/LijstExportKnop.js +43 -0
- package/dist-lib/components/voorraad/lijst/SnelActieDialogen.d.ts +16 -0
- package/dist-lib/components/voorraad/lijst/SnelActieDialogen.d.ts.map +1 -0
- package/dist-lib/components/voorraad/lijst/SnelActieDialogen.js +95 -0
- package/dist-lib/components/voorraad/lijst/VoorraadLijstTabel.d.ts +21 -0
- package/dist-lib/components/voorraad/lijst/VoorraadLijstTabel.d.ts.map +1 -0
- package/dist-lib/components/voorraad/lijst/VoorraadLijstTabel.js +38 -0
- package/dist-lib/hooks/useVoorraadLijst.d.ts +21 -0
- package/dist-lib/hooks/useVoorraadLijst.d.ts.map +1 -0
- package/dist-lib/hooks/useVoorraadLijst.js +71 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +2 -0
- package/dist-lib/integrations/supabase/types.d.ts +465 -1
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/artikel-pagina.d.ts +57 -0
- package/dist-lib/lib/artikel-pagina.d.ts.map +1 -0
- package/dist-lib/lib/artikel-pagina.js +93 -0
- package/dist-lib/lib/count-stock.d.ts +17 -0
- package/dist-lib/lib/count-stock.d.ts.map +1 -0
- package/dist-lib/lib/count-stock.js +26 -0
- package/dist-lib/lib/floriday-import-validator.d.ts +9 -0
- package/dist-lib/lib/floriday-import-validator.d.ts.map +1 -1
- package/dist-lib/lib/floriday-import-validator.js +13 -0
- package/dist-lib/lib/pending-photos.d.ts +20 -0
- package/dist-lib/lib/pending-photos.d.ts.map +1 -0
- package/dist-lib/lib/pending-photos.js +90 -0
- package/dist-lib/lib/voorraad-custom-fields.d.ts +16 -1
- package/dist-lib/lib/voorraad-custom-fields.d.ts.map +1 -1
- package/dist-lib/lib/voorraad-custom-fields.js +12 -6
- package/dist-lib/lib/voorraad-lijst.d.ts +91 -0
- package/dist-lib/lib/voorraad-lijst.d.ts.map +1 -0
- package/dist-lib/lib/voorraad-lijst.js +82 -0
- package/package.json +1 -1
- package/sql/AGENTS.md +4 -0
- package/sql/floriday-voorraad-schema.sql +723 -10
|
@@ -1,100 +1,98 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
|
-
import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle,
|
|
4
|
-
import { Loader2, Trash2, Upload } from "lucide-react";
|
|
3
|
+
import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@flowselections/core";
|
|
4
|
+
import { Loader2, Trash2, Undo2, Upload } from "lucide-react";
|
|
5
5
|
import { supabase } from "../../integrations/supabase/client.js";
|
|
6
|
+
import { useOpslagDeelnemer } from "./artikel/opslaan-context.js";
|
|
6
7
|
const BUCKET = "product-tray-stickers";
|
|
7
8
|
// Bucket is privé (publieke buckets zijn geblokkeerd): lange signed URL voor de logistiek-engine.
|
|
8
9
|
const SIGNED_URL_SECONDS = 60 * 60 * 24 * 365 * 10;
|
|
9
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Traysticker-afbeelding per artikel — los van de Floriday-productfoto.
|
|
12
|
+
* Kiezen of verwijderen wordt pas bewaard met de knop "Opslaan" van de pagina,
|
|
13
|
+
* met controle tegen de stand bij openen.
|
|
14
|
+
*/
|
|
10
15
|
export function TrayStickerCard({ productId }) {
|
|
11
16
|
const [row, setRow] = useState(null);
|
|
12
17
|
const [loading, setLoading] = useState(true);
|
|
13
|
-
const [
|
|
18
|
+
const [wachtend, setWachtend] = useState(null);
|
|
14
19
|
const inputRef = useRef(null);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
setLoading(true);
|
|
18
|
-
supabase
|
|
20
|
+
const laad = async () => {
|
|
21
|
+
const { data } = await supabase
|
|
19
22
|
.from("product_tray_stickers")
|
|
20
23
|
.select("*")
|
|
21
24
|
.eq("product_id", productId)
|
|
22
|
-
.maybeSingle()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
.maybeSingle();
|
|
26
|
+
setRow(data ?? null);
|
|
27
|
+
};
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
let cancelled = false;
|
|
30
|
+
setLoading(true);
|
|
31
|
+
void laad().finally(() => {
|
|
26
32
|
if (!cancelled)
|
|
27
33
|
setLoading(false);
|
|
28
34
|
});
|
|
29
35
|
return () => {
|
|
30
36
|
cancelled = true;
|
|
31
37
|
};
|
|
38
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
32
39
|
}, [productId]);
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
useEffect(() => () => {
|
|
41
|
+
if (wachtend?.soort === "nieuw")
|
|
42
|
+
URL.revokeObjectURL(wachtend.voorbeeld);
|
|
43
|
+
}, [wachtend]);
|
|
44
|
+
const opslaan = async () => {
|
|
45
|
+
if (!wachtend)
|
|
46
|
+
return { ok: true };
|
|
47
|
+
let pad = null;
|
|
35
48
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
let next = null;
|
|
50
|
+
if (wachtend.soort === "nieuw") {
|
|
51
|
+
const file = wachtend.file;
|
|
52
|
+
const ext = file.name.includes(".") ? file.name.slice(file.name.lastIndexOf(".")) : "";
|
|
53
|
+
pad = `${productId}/${crypto.randomUUID()}${ext}`;
|
|
54
|
+
const up = await supabase.storage.from(BUCKET).upload(pad, file, { contentType: file.type || undefined });
|
|
55
|
+
if (up.error)
|
|
56
|
+
throw up.error;
|
|
57
|
+
const signed = await supabase.storage.from(BUCKET).createSignedUrl(pad, SIGNED_URL_SECONDS);
|
|
58
|
+
if (signed.error || !signed.data)
|
|
59
|
+
throw signed.error ?? new Error("Geen URL");
|
|
60
|
+
next = { product_id: productId, bestand_url: signed.data.signedUrl, bestandspad: pad, bestandsnaam: file.name, bestandsgrootte: file.size };
|
|
61
|
+
}
|
|
62
|
+
const { data, error } = await supabase.rpc("voorraad_traysticker_opslaan", {
|
|
63
|
+
p_product_id: productId,
|
|
64
|
+
p_oud_pad: row?.bestandspad ?? null,
|
|
65
|
+
p_nieuw: next,
|
|
40
66
|
});
|
|
41
|
-
if (up.error)
|
|
42
|
-
throw up.error;
|
|
43
|
-
const signed = await supabase.storage.from(BUCKET).createSignedUrl(path, SIGNED_URL_SECONDS);
|
|
44
|
-
if (signed.error || !signed.data)
|
|
45
|
-
throw signed.error ?? new Error("Geen URL");
|
|
46
|
-
const next = {
|
|
47
|
-
product_id: productId,
|
|
48
|
-
bestand_url: signed.data.signedUrl,
|
|
49
|
-
bestandspad: path,
|
|
50
|
-
bestandsnaam: file.name,
|
|
51
|
-
bestandsgrootte: file.size,
|
|
52
|
-
};
|
|
53
|
-
const { error } = await supabase
|
|
54
|
-
.from("product_tray_stickers")
|
|
55
|
-
.upsert(next, { onConflict: "product_id" });
|
|
56
67
|
if (error)
|
|
57
68
|
throw error;
|
|
69
|
+
if (!data.ok) {
|
|
70
|
+
if (pad)
|
|
71
|
+
await supabase.storage.from(BUCKET).remove([pad]);
|
|
72
|
+
setWachtend(null);
|
|
73
|
+
await laad();
|
|
74
|
+
return { ok: false, melding: "Traysticker niet opgeslagen: iemand anders wijzigde hem intussen. De nieuwste stand is geladen." };
|
|
75
|
+
}
|
|
58
76
|
if (row?.bestandspad)
|
|
59
77
|
await supabase.storage.from(BUCKET).remove([row.bestandspad]);
|
|
60
78
|
setRow(next);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
catch (e) {
|
|
64
|
-
toast.error(`Uploaden mislukt: ${e.message}`);
|
|
65
|
-
}
|
|
66
|
-
finally {
|
|
67
|
-
setBusy(false);
|
|
68
|
-
if (inputRef.current)
|
|
69
|
-
inputRef.current.value = "";
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
const remove = async () => {
|
|
73
|
-
if (!row)
|
|
74
|
-
return;
|
|
75
|
-
setBusy(true);
|
|
76
|
-
try {
|
|
77
|
-
const { error } = await supabase
|
|
78
|
-
.from("product_tray_stickers")
|
|
79
|
-
.delete()
|
|
80
|
-
.eq("product_id", productId);
|
|
81
|
-
if (error)
|
|
82
|
-
throw error;
|
|
83
|
-
if (row.bestandspad)
|
|
84
|
-
await supabase.storage.from(BUCKET).remove([row.bestandspad]);
|
|
85
|
-
setRow(null);
|
|
86
|
-
toast.success("Traysticker-afbeelding verwijderd");
|
|
79
|
+
setWachtend(null);
|
|
80
|
+
return { ok: true };
|
|
87
81
|
}
|
|
88
82
|
catch (e) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
setBusy(false);
|
|
83
|
+
if (pad)
|
|
84
|
+
await supabase.storage.from(BUCKET).remove([pad]);
|
|
85
|
+
return { ok: false, melding: `Traysticker: opslaan mislukt (${e.message}).` };
|
|
93
86
|
}
|
|
94
87
|
};
|
|
95
|
-
|
|
88
|
+
useOpslagDeelnemer("traysticker", wachtend !== null, opslaan);
|
|
89
|
+
const toonUrl = wachtend?.soort === "nieuw" ? wachtend.voorbeeld : wachtend?.soort === "weg" ? null : row?.bestand_url;
|
|
90
|
+
const toonNaam = wachtend?.soort === "nieuw" ? wachtend.file.name : row?.bestandsnaam;
|
|
91
|
+
return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Traysticker-afbeelding" }), _jsx(CardDescription, { children: "Afbeelding die op het traysticker-etiket bij de pakbon op het pickstation wordt geprint. Dit staat los van de productfoto uit Floriday." })] }), _jsxs(CardContent, { className: "space-y-4", children: [loading ? (_jsx(Loader2, { className: "h-4 w-4 animate-spin text-muted-foreground" })) : toonUrl ? (_jsxs("div", { className: "flex items-center gap-4", children: [_jsx("img", { src: toonUrl, alt: "Traysticker", className: "h-24 w-24 rounded border object-contain bg-muted" }), _jsx("div", { className: "text-sm text-muted-foreground", children: _jsx("div", { children: toonNaam }) })] })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: wachtend?.soort === "weg" ? "Wordt verwijderd bij Opslaan." : "Nog geen traysticker-afbeelding." })), wachtend && (_jsx("p", { className: "text-xs text-muted-foreground", children: "Niet opgeslagen wijziging; bewaar met \"Opslaan\" bovenaan." })), _jsx("input", { ref: inputRef, type: "file", accept: "image/*", className: "hidden", onChange: (e) => {
|
|
96
92
|
const f = e.target.files?.[0];
|
|
97
93
|
if (f)
|
|
98
|
-
|
|
99
|
-
|
|
94
|
+
setWachtend({ soort: "nieuw", file: f, voorbeeld: URL.createObjectURL(f) });
|
|
95
|
+
if (inputRef.current)
|
|
96
|
+
inputRef.current.value = "";
|
|
97
|
+
} }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => inputRef.current?.click(), disabled: loading, children: [_jsx(Upload, { className: "mr-2 h-4 w-4" }), row || wachtend?.soort === "nieuw" ? "Andere kiezen" : "Afbeelding kiezen"] }), row && wachtend?.soort !== "weg" && (_jsxs(Button, { variant: "outline", onClick: () => setWachtend({ soort: "weg" }), children: [_jsx(Trash2, { className: "mr-2 h-4 w-4" }), "Verwijderen"] })), wachtend && (_jsxs(Button, { variant: "ghost", onClick: () => setWachtend(null), children: [_jsx(Undo2, { className: "mr-2 h-4 w-4" }), "Ongedaan maken"] }))] })] })] }));
|
|
100
98
|
}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
import type { TradeItem } from "../../lib/floriday-client.js";
|
|
2
|
-
/**
|
|
3
|
-
* Overzicht van producten die (nog) niet actief op Floriday staan. Er is geen concept-status
|
|
4
|
-
* meer — het gaat puur om welke Floriday-velden ontbreken en of het product klaar is om te
|
|
5
|
-
* activeren. Producten blijven volwaardig in gebruik binnen ERP, ongeacht Floriday-status.
|
|
6
|
-
*/
|
|
7
2
|
export declare function ValidationOverviewTable({ items, connectionId, onChanged, }: {
|
|
8
3
|
items: TradeItem[];
|
|
9
4
|
connectionId: string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ValidationOverviewTable.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ValidationOverviewTable.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"ValidationOverviewTable.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ValidationOverviewTable.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAoB3D,wBAAgB,uBAAuB,CAAC,EACtC,KAAK,EACL,YAAY,EACZ,SAAS,GACV,EAAE;IACD,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC,+BAwKA"}
|
|
@@ -3,7 +3,7 @@ import { useMemo, useState } from "react";
|
|
|
3
3
|
import { useNavigate } from "@tanstack/react-router";
|
|
4
4
|
import { Badge, Button } from "@flowselections/core";
|
|
5
5
|
import { Link2, Loader2, Pencil, Send } from "lucide-react";
|
|
6
|
-
import { validateDraftForPublish } from "../../lib/floriday-import-validator.js";
|
|
6
|
+
import { conceptMetArtikel, validateDraftForPublish } from "../../lib/floriday-import-validator.js";
|
|
7
7
|
import { updateDraftMeta } from "../../hooks/useLocalTradeItems.js";
|
|
8
8
|
import { floriday } from "../../lib/floriday-client.js";
|
|
9
9
|
import { relinkProducts } from "../../lib/floriday-relink.js";
|
|
@@ -13,6 +13,12 @@ import { toast } from "@flowselections/core";
|
|
|
13
13
|
* meer — het gaat puur om welke Floriday-velden ontbreken en of het product klaar is om te
|
|
14
14
|
* activeren. Producten blijven volwaardig in gebruik binnen ERP, ongeacht Floriday-status.
|
|
15
15
|
*/
|
|
16
|
+
/** Eigen naam en artikelnummer van het artikel (niet het Floriday-concept). */
|
|
17
|
+
function artikelVan(it) {
|
|
18
|
+
const raw = (it.raw ?? {});
|
|
19
|
+
const s = (v) => (typeof v === "string" && v.trim() ? v : null);
|
|
20
|
+
return { name: s(raw.product) ?? s(it.name), articleNumber: s(raw.article_number) ?? s(it.articleNumber) };
|
|
21
|
+
}
|
|
16
22
|
export function ValidationOverviewTable({ items, connectionId, onChanged, }) {
|
|
17
23
|
const navigate = useNavigate();
|
|
18
24
|
const [relinking, setRelinking] = useState(false);
|
|
@@ -53,14 +59,14 @@ export function ValidationOverviewTable({ items, connectionId, onChanged, }) {
|
|
|
53
59
|
item.raw?.floriday_payload ??
|
|
54
60
|
item.raw?.data ??
|
|
55
61
|
{};
|
|
56
|
-
const v = validateDraftForPublish(payload);
|
|
62
|
+
const v = validateDraftForPublish(conceptMetArtikel(payload, artikelVan(item)));
|
|
57
63
|
if (!v.ok) {
|
|
58
64
|
toast.error("Nog niet compleet: " + v.missing.join(", "));
|
|
59
65
|
return;
|
|
60
66
|
}
|
|
61
67
|
try {
|
|
62
68
|
const { buildAddTradeItemPayload } = await import("../../lib/floriday-payload.js");
|
|
63
|
-
const built = buildAddTradeItemPayload(payload);
|
|
69
|
+
const built = buildAddTradeItemPayload(conceptMetArtikel(payload, artikelVan(item)));
|
|
64
70
|
const res = await floriday.createTradeItem(connectionId, built);
|
|
65
71
|
await updateDraftMeta(productId, {
|
|
66
72
|
floridayLinkStatus: "published",
|
|
@@ -85,7 +91,7 @@ export function ValidationOverviewTable({ items, connectionId, onChanged, }) {
|
|
|
85
91
|
it.raw?.floriday_payload ??
|
|
86
92
|
it.raw?.data ??
|
|
87
93
|
{};
|
|
88
|
-
const v = validateDraftForPublish(payload);
|
|
94
|
+
const v = validateDraftForPublish(conceptMetArtikel(payload, artikelVan(it)));
|
|
89
95
|
return (_jsxs("tr", { className: "border-t align-top", children: [_jsx("td", { className: "p-3 font-mono text-xs", children: it.articleNumber || "—" }), _jsx("td", { className: "p-3", children: it.name }), _jsx("td", { className: "p-3", children: _jsx(Badge, { className: "bg-slate-500/15 text-slate-700 border-slate-500/30", children: "Niet actief op Floriday" }) }), _jsx("td", { className: "p-3 text-xs text-muted-foreground max-w-[260px]", children: reasonFor(it) }), _jsx("td", { className: "p-3", children: v.missing.length === 0 ? (_jsx("span", { className: "text-xs text-emerald-700", children: "Klaar om te activeren" })) : (_jsxs("span", { className: "text-xs text-muted-foreground", title: v.missing.join(", "), children: ["Nog nodig: ", v.missing.slice(0, 3).join(", ").toLowerCase(), v.missing.length > 3 ? ` en nog ${v.missing.length - 3} andere` : "", "."] })) }), _jsx("td", { className: "p-3", children: _jsxs("div", { className: "flex justify-end gap-1", children: [_jsxs(Button, { size: "sm", variant: "outline", onClick: () => {
|
|
90
96
|
const target = it.productId ?? it.localId ?? it.id;
|
|
91
97
|
navigate({ to: "/voorraad/$id", params: { id: target } });
|
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* "Op voorraad" = artikelen met liggende voorraad (fysiek > 0).
|
|
4
|
-
* "Vrij beschikbaar" = artikelen waarvan liggend min gereserveerd > 0.
|
|
5
|
-
*/
|
|
6
|
-
export declare function countStock(items: Array<Pick<TradeItem, "quantity" | "availableQuantity" | "minQuantity">>): {
|
|
7
|
-
total: number;
|
|
8
|
-
inStock: number;
|
|
9
|
-
outOfStock: number;
|
|
10
|
-
belowMin: number;
|
|
11
|
-
fullyReserved: number;
|
|
12
|
-
freeAvailable: number;
|
|
13
|
-
};
|
|
1
|
+
export { countStock } from "../../lib/count-stock.js";
|
|
14
2
|
export declare function VoorraadPage(): import("react").JSX.Element;
|
|
15
3
|
//# sourceMappingURL=VoorraadPage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VoorraadPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"VoorraadPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadPage.tsx"],"names":[],"mappings":"AAwCA,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AA2CnD,wBAAgB,YAAY,gCA6M3B"}
|
|
@@ -1,95 +1,109 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { TwijfelKiezen } from "./artikel/TwijfelKiezen.js";
|
|
3
|
+
import { Fragment, useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { Link, useNavigate } from "@tanstack/react-router";
|
|
5
|
+
import { Button, Card, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuTrigger, Input, } from "@flowselections/core";
|
|
6
|
+
import { AlertTriangle, Columns3, Plus, Search } from "lucide-react";
|
|
7
|
+
import { supabase } from "../../integrations/supabase/client.js";
|
|
4
8
|
import { pageRequestGuard } from "../../lib/request-guard.js";
|
|
5
|
-
const EMPTY_TRIPS = [];
|
|
6
|
-
import { Link } from "@tanstack/react-router";
|
|
7
|
-
import { Button, Card, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@flowselections/core";
|
|
8
|
-
import { AlertTriangle, CheckCircle2, FileText, PackageX, Plus, Search, } from "lucide-react";
|
|
9
9
|
import { useFloridayConnection } from "../../hooks/useFloridayConnection.js";
|
|
10
10
|
import { useEnsureVoorraadDefaults } from "../../hooks/useEnsureVoorraadDefaults.js";
|
|
11
|
+
import { useVoorraadLijst } from "../../hooks/useVoorraadLijst.js";
|
|
12
|
+
import { aantal, FILTER_LABEL, meldingen, volgendeSortering, zichtbareFilters, } from "../../lib/voorraad-lijst.js";
|
|
11
13
|
import { FloridayConnectionNotice } from "./FloridayConnectionNotice.js";
|
|
12
|
-
import { useVoorraadData } from "../../hooks/useVoorraadData.js";
|
|
13
|
-
import { StockSyncStatus } from "./StockSyncStatus.js";
|
|
14
14
|
import { StockIntegrityAlert } from "./StockIntegrityAlert.js";
|
|
15
|
-
import { SupplyReadScopeAlert } from "./SupplyReadScopeAlert.js";
|
|
16
|
-
import { ExcelExportButton } from "./ExcelExportButton.js";
|
|
17
|
-
import { StockBulkButtons } from "./StockBulkButtons.js";
|
|
18
15
|
import { TelmomentButton } from "./TelmomentButton.js";
|
|
19
|
-
import { VoorraadTable } from "./VoorraadTable.js";
|
|
20
16
|
import { VoorraadPagination } from "./VoorraadPagination.js";
|
|
21
|
-
import { StockHistoryChart } from "./StockHistoryChart.js";
|
|
22
17
|
import { InventorySettingsDrawer } from "./InventorySettingsDrawer.js";
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
* "Op voorraad" = artikelen met liggende voorraad (fysiek > 0).
|
|
49
|
-
* "Vrij beschikbaar" = artikelen waarvan liggend min gereserveerd > 0.
|
|
50
|
-
*/
|
|
51
|
-
export function countStock(items) {
|
|
52
|
-
let inStock = 0;
|
|
53
|
-
let outOfStock = 0;
|
|
54
|
-
let belowMin = 0;
|
|
55
|
-
let fullyReserved = 0;
|
|
56
|
-
let freeAvailable = 0;
|
|
57
|
-
for (const i of items) {
|
|
58
|
-
const physical = Number(i.quantity ?? 0);
|
|
59
|
-
const avail = availableOf(i);
|
|
60
|
-
if (physical > 0)
|
|
61
|
-
inStock++;
|
|
18
|
+
import { OPTIONELE_KOLOMMEN, VoorraadLijstTabel } from "./lijst/VoorraadLijstTabel.js";
|
|
19
|
+
import { AanpassenDialog, AfboekenDialog } from "./lijst/SnelActieDialogen.js";
|
|
20
|
+
import { LijstExportKnop } from "./lijst/LijstExportKnop.js";
|
|
21
|
+
import { ImporteerKnop } from "./lijst/ImporteerKnop.js";
|
|
22
|
+
// Bestaande export blijft (tests en andere modules).
|
|
23
|
+
export { countStock } from "../../lib/count-stock.js";
|
|
24
|
+
const EMPTY_TRIPS = [];
|
|
25
|
+
const KOLOMMEN_KEY = "voorraad.lijst.kolommen.v2";
|
|
26
|
+
const STANDAARD_KOLOMMEN = ["inkomend", "prijs"];
|
|
27
|
+
function useKolommen() {
|
|
28
|
+
const [set, setSet] = useState(new Set(STANDAARD_KOLOMMEN));
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
try {
|
|
31
|
+
const raw = window.localStorage.getItem(KOLOMMEN_KEY);
|
|
32
|
+
if (raw)
|
|
33
|
+
setSet(new Set(JSON.parse(raw)));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* standaard houden */
|
|
37
|
+
}
|
|
38
|
+
}, []);
|
|
39
|
+
const toggle = (k) => setSet((cur) => {
|
|
40
|
+
const next = new Set(cur);
|
|
41
|
+
if (next.has(k))
|
|
42
|
+
next.delete(k);
|
|
62
43
|
else
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
44
|
+
next.add(k);
|
|
45
|
+
try {
|
|
46
|
+
window.localStorage.setItem(KOLOMMEN_KEY, JSON.stringify([...next]));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
/* niet opslaan is geen probleem */
|
|
50
|
+
}
|
|
51
|
+
return next;
|
|
52
|
+
});
|
|
53
|
+
return { kolommen: set, toggle };
|
|
54
|
+
}
|
|
55
|
+
/** Verbinding van de voorraadcontext (nodig voor afboeken op de juiste voorraadregel). */
|
|
56
|
+
function useStockContextId() {
|
|
57
|
+
const [id, setId] = useState(null);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
void supabase.rpc("get_stock_context").then(({ data }) => {
|
|
60
|
+
const row = Array.isArray(data) ? data[0] : data;
|
|
61
|
+
setId(row?.connection_id ?? null);
|
|
62
|
+
});
|
|
63
|
+
}, []);
|
|
64
|
+
return id;
|
|
72
65
|
}
|
|
73
66
|
export function VoorraadPage() {
|
|
74
67
|
useEnsureVoorraadDefaults();
|
|
68
|
+
const navigate = useNavigate();
|
|
75
69
|
const conn = useFloridayConnection();
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
const [
|
|
80
|
-
const [
|
|
81
|
-
const [
|
|
82
|
-
const [
|
|
83
|
-
const [page, setPage] = useState(1);
|
|
84
|
-
const [pageSize, setPageSize] = useState(25);
|
|
70
|
+
const stockConnId = useStockContextId();
|
|
71
|
+
const [zoekInvoer, setZoekInvoer] = useState("");
|
|
72
|
+
const [zoek, setZoek] = useState("");
|
|
73
|
+
const [filter, setFilter] = useState("alles");
|
|
74
|
+
const [sorteer, setSorteer] = useState({ sort: "naam", oplopend: true });
|
|
75
|
+
const [pagina, setPagina] = useState(1);
|
|
76
|
+
const [grootte, setGrootte] = useState(25);
|
|
85
77
|
const [selected, setSelected] = useState(new Set());
|
|
86
|
-
const [drawerIds, setDrawerIds] = useState([]);
|
|
87
78
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const {
|
|
91
|
-
const [noPriceOnly, setNoPriceOnly] = useState(false);
|
|
79
|
+
const [aanpassen, setAanpassen] = useState(null);
|
|
80
|
+
const [afboeken, setAfboeken] = useState(null);
|
|
81
|
+
const { kolommen, toggle: toggleKolom } = useKolommen();
|
|
92
82
|
const guardTrips = useSyncExternalStore(pageRequestGuard.subscribe, () => pageRequestGuard.trips(), () => EMPTY_TRIPS);
|
|
83
|
+
// Zoeken met korte vertraging; nieuwe zoekopdracht of filter begint op pagina 1.
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const t = setTimeout(() => setZoek(zoekInvoer), 300);
|
|
86
|
+
return () => clearTimeout(t);
|
|
87
|
+
}, [zoekInvoer]);
|
|
88
|
+
useEffect(() => setPagina(1), [zoek, filter, sorteer, grootte]);
|
|
89
|
+
// Sneltoets N = nieuw artikel (niet tijdens typen).
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const h = (e) => {
|
|
92
|
+
const t = e.target;
|
|
93
|
+
if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable))
|
|
94
|
+
return;
|
|
95
|
+
if (e.key === "n" && !e.metaKey && !e.ctrlKey && !e.altKey)
|
|
96
|
+
navigate({ to: "/voorraad/nieuw" });
|
|
97
|
+
};
|
|
98
|
+
window.addEventListener("keydown", h);
|
|
99
|
+
return () => window.removeEventListener("keydown", h);
|
|
100
|
+
}, [navigate]);
|
|
101
|
+
const vraag = { zoek, filter, sort: sorteer.sort, oplopend: sorteer.oplopend, pagina, grootte };
|
|
102
|
+
const { data, loading, error, reload } = useVoorraadLijst(vraag);
|
|
103
|
+
const heeftKoppeling = data?.heeft_koppeling ?? false;
|
|
104
|
+
const filters = useMemo(() => zichtbareFilters(heeftKoppeling), [heeftKoppeling]);
|
|
105
|
+
const melding = data ? meldingen(data) : [];
|
|
106
|
+
const bijgewerkt = data?.bijgewerkt ? new Date(data.bijgewerkt).toLocaleTimeString("nl-NL", { hour: "2-digit", minute: "2-digit" }) : null;
|
|
93
107
|
const toggleSelect = (id) => setSelected((cur) => {
|
|
94
108
|
const next = new Set(cur);
|
|
95
109
|
if (next.has(id))
|
|
@@ -98,69 +112,17 @@ export function VoorraadPage() {
|
|
|
98
112
|
next.add(id);
|
|
99
113
|
return next;
|
|
100
114
|
});
|
|
101
|
-
const toggleAll = (ids) => setSelected((cur) =>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return false;
|
|
115
|
-
const avail = availableOf(i);
|
|
116
|
-
const physicalQty = Number(i.quantity ?? 0);
|
|
117
|
-
if (stockFilter === "in_stock" && physicalQty <= 0)
|
|
118
|
-
return false;
|
|
119
|
-
if (stockFilter === "out_of_stock" && physicalQty > 0)
|
|
120
|
-
return false;
|
|
121
|
-
if (stockFilter === "below_min" &&
|
|
122
|
-
!((i.minQuantity ?? 0) > 0 && avail < i.minQuantity))
|
|
123
|
-
return false;
|
|
124
|
-
if (floridayFilter === "linked" && !isLinkedToFloriday(i))
|
|
125
|
-
return false;
|
|
126
|
-
if (floridayFilter === "unlinked" && isLinkedToFloriday(i))
|
|
127
|
-
return false;
|
|
128
|
-
if (typeFilter !== "all" && (i.itemType ?? "artikel") !== typeFilter)
|
|
129
|
-
return false;
|
|
130
|
-
if (!q)
|
|
131
|
-
return true;
|
|
132
|
-
return (i.name.toLowerCase().includes(q) ||
|
|
133
|
-
(i.articleNumber ?? "").toLowerCase().includes(q));
|
|
134
|
-
});
|
|
135
|
-
}, [items, search, stockFilter, floridayFilter, typeFilter, noPriceOnly, noPriceIds]);
|
|
136
|
-
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
|
137
|
-
useEffect(() => {
|
|
138
|
-
if (page > totalPages)
|
|
139
|
-
setPage(1);
|
|
140
|
-
}, [page, totalPages]);
|
|
141
|
-
const paged = useMemo(() => filtered.slice((page - 1) * pageSize, page * pageSize), [filtered, page, pageSize]);
|
|
142
|
-
const toggle = (id) => setStockFilter((cur) => (cur === id && id !== "all" ? "all" : id));
|
|
143
|
-
const TABS = [
|
|
144
|
-
{ id: "all", label: "Alle", count: counts.total },
|
|
145
|
-
{ id: "in_stock", label: "Op voorraad", count: counts.inStock },
|
|
146
|
-
{ id: "out_of_stock", label: "Niet op voorraad", count: counts.outOfStock },
|
|
147
|
-
{ id: "below_min", label: "Onder minimum", count: counts.belowMin },
|
|
148
|
-
];
|
|
149
|
-
return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsxs("header", { className: "flex flex-wrap items-start justify-between gap-4", children: [_jsxs("div", { className: "min-w-[12rem] flex-1", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Voorraad" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer je artikelen en voorraadniveaus." })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { to: "/voorraad/validatie", children: "Validatie" }) }), _jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { to: "/voorraad/publicaties", children: "Publicaties" }) }), _jsx(Button, { asChild: true, size: "sm", children: _jsxs(Link, { to: "/voorraad/nieuw", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Artikel toevoegen"] }) })] })] }), guardTrips.length > 0 && (_jsxs("div", { role: "alert", className: "rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive", children: ["Deze pagina vroeg te vaak gegevens op en is daarmee gestopt. Open de pagina opnieuw.", _jsxs("details", { className: "text-xs opacity-80", children: [_jsx("summary", { className: "cursor-pointer", children: "Details voor beheer" }), guardTrips.map((t) => (_jsxs("div", { children: [t.table, ": ", t.count, " aanvragen binnen 10 s om ", new Date(t.at).toLocaleTimeString("nl-NL")] }, t.table)))] })] })), _jsx(StockIntegrityAlert, {}), _jsx(SupplyReadScopeAlert, {}), _jsx(StockSyncStatus, {}), noPriceIds.size > 0 && (_jsxs("div", { className: "flex flex-wrap items-start gap-2 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm text-amber-800", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsxs("div", { className: "flex-1 min-w-[12rem]", children: [_jsxs("span", { className: "font-medium", children: [noPriceIds.size, " artikel", noPriceIds.size === 1 ? " staat" : "en staan", " niet in Floriday zonder geldige prijs:", " ", [
|
|
150
|
-
noPriceBreakdown.empty > 0 ? `${noPriceBreakdown.empty} zonder verkoopprijs` : null,
|
|
151
|
-
noPriceBreakdown.zero > 0 ? `${noPriceBreakdown.zero} met prijs € 0,00` : null,
|
|
152
|
-
noPriceBreakdown.unit > 0 ? `${noPriceBreakdown.unit} met een andere eenheid dan stuks` : null,
|
|
153
|
-
]
|
|
154
|
-
.filter(Boolean)
|
|
155
|
-
.join(", "), "."] }), " ", "Kies \"Toon deze artikelen\", open een artikel en vul bij Artikel regulier de verkoopprijs per stuk (excl. btw) in. De voorraadsturing van elk artikel heeft ook een link \"Prijs invullen\"."] }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => setNoPriceOnly((v) => !v), children: noPriceOnly ? "Alle artikelen tonen" : "Toon deze artikelen" })] })), _jsx(FloridayConnectionNotice, { status: conn.status, environment: conn.selectedEnvironment, error: connError }), error && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 text-destructive p-3 text-sm", children: ["Voorraad laden mislukt: ", error] })), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4", children: [_jsx(KpiCard, { icon: _jsx(FileText, { className: "h-4 w-4" }), tone: "primary", label: "Artikelen", count: counts.total, active: stockFilter === "all", onClick: () => toggle("all") }), _jsx(KpiCard, { icon: _jsx(CheckCircle2, { className: "h-4 w-4" }), tone: "emerald", label: "Op voorraad", count: counts.inStock, active: stockFilter === "in_stock", onClick: () => toggle("in_stock"), hint: "Artikelen waarvan nu iets op de kwekerij staat." }), _jsx(KpiCard, { icon: _jsx(CheckCircle2, { className: "h-4 w-4" }), tone: "primary", label: "Vrij beschikbaar", count: counts.freeAvailable, active: false, hint: "Artikelen waarvan na aftrek van wat voor orders gereserveerd is nog iets over is." }), _jsx(KpiCard, { icon: _jsx(PackageX, { className: "h-4 w-4" }), tone: "rose", label: "Niet op voorraad", count: counts.outOfStock, active: stockFilter === "out_of_stock", onClick: () => toggle("out_of_stock") }), _jsx(KpiCard, { icon: _jsx(AlertTriangle, { className: "h-4 w-4" }), tone: "amber", label: "Onder minimum", count: counts.belowMin, active: stockFilter === "below_min", onClick: () => toggle("below_min") })] }), _jsxs(Card, { className: "overflow-hidden p-0", children: [_jsx("div", { className: "flex flex-wrap items-center gap-1 border-b px-4", children: TABS.map((t) => {
|
|
156
|
-
const active = stockFilter === t.id;
|
|
157
|
-
return (_jsxs("button", { type: "button", onClick: () => setStockFilter(t.id), className: `relative py-3 px-3 text-sm font-medium whitespace-nowrap transition-colors ${active
|
|
158
|
-
? "text-foreground"
|
|
159
|
-
: "text-muted-foreground hover:text-foreground"}`, children: [t.label, _jsx("span", { className: `ml-2 text-xs ${active ? "text-primary" : "text-muted-foreground/70"}`, children: t.count }), active && (_jsx("span", { className: "absolute left-2 right-2 -bottom-px h-0.5 bg-primary rounded-full" }))] }, t.id));
|
|
160
|
-
}) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2 p-4 border-b", children: [_jsxs("div", { className: "relative flex-1 min-w-[220px] max-w-md", children: [_jsx(Search, { className: "w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { placeholder: "Zoek op product of artikelnummer...", className: "pl-9", value: search, onChange: (e) => setSearch(e.target.value) })] }), _jsxs(Select, { value: floridayFilter, onValueChange: (v) => setFloridayFilter(v), children: [_jsx(SelectTrigger, { className: "h-9 w-[190px]", children: _jsx(SelectValue, { placeholder: "Floriday" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "Alle (Floriday)" }), _jsx(SelectItem, { value: "linked", children: "Gekoppeld aan Floriday" }), _jsx(SelectItem, { value: "unlinked", children: "Niet gekoppeld" })] })] }), _jsxs(Select, { value: typeFilter, onValueChange: (v) => setTypeFilter(v), children: [_jsx(SelectTrigger, { className: "h-9 w-[170px]", children: _jsx(SelectValue, { placeholder: "Type" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "Alle types" }), _jsx(SelectItem, { value: "artikel", children: "Artikel" }), _jsx(SelectItem, { value: "productgroep", children: "Productgroep" }), _jsx(SelectItem, { value: "samenstelling", children: "Samenstelling" })] })] }), _jsxs("div", { className: "ml-auto flex flex-wrap items-center gap-2", children: [_jsx(ExcelExportButton, { items: filtered }), _jsx(TelmomentButton, { onSaved: reload }), _jsx(StockBulkButtons, { items: items, activeConnectionId: activeId, onImported: reload })] })] }), selected.size > 0 && (_jsxs("div", { className: "flex flex-wrap items-center gap-3 border-b bg-primary/5 px-4 py-2 text-sm", children: [_jsxs("span", { className: "font-medium", children: [selected.size, " geselecteerd"] }), _jsx(Button, { size: "sm", onClick: () => openSettings([...selected]), children: "Voorraadsturing instellen" }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => setSelected(new Set()), children: "Selectie wissen" })] })), _jsx("div", { className: "overflow-x-auto", children: _jsx(VoorraadTable, { items: paged, loading: loading, settingsByKey: settingsByKey, selected: selected, onToggleSelect: toggleSelect, onToggleAll: toggleAll, onOpenSettings: (id) => openSettings([id]) }) }), _jsx(VoorraadPagination, { total: filtered.length, page: page, pageSize: pageSize, onPageChange: setPage, onPageSizeChange: setPageSize })] }), _jsx(StockHistoryChart, { connectionId: activeId ?? null }), _jsx(InventorySettingsDrawer, { open: drawerOpen, onOpenChange: setDrawerOpen, connectionId: activeId ?? null, productIds: drawerIds, current: drawerIds.length === 1
|
|
161
|
-
? [...settingsByKey.values()].find((s) => s.product_id === drawerIds[0])
|
|
162
|
-
: undefined, onSaved: () => {
|
|
163
|
-
reloadSettings();
|
|
164
|
-
reload();
|
|
165
|
-
} })] }));
|
|
115
|
+
const toggleAll = (ids) => setSelected((cur) => (ids.length > 0 && ids.every((id) => cur.has(id)) ? new Set() : new Set(ids)));
|
|
116
|
+
return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsxs("header", { className: "flex flex-wrap items-start justify-between gap-4", children: [_jsxs("div", { className: "min-w-[12rem] flex-1", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Voorraad" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: bijgewerkt ? `Bijgewerkt ${bijgewerkt}` : "Laden…" })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(ImporteerKnop, { connectionId: conn.activeId ?? null, onImported: reload }), _jsx(LijstExportKnop, { vraag: { zoek, filter, sort: sorteer.sort, oplopend: sorteer.oplopend } }), _jsx(TelmomentButton, { onSaved: reload }), _jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsx(Link, { to: "/voorraad/validatie", children: "Validatie" }) }), _jsx(Button, { asChild: true, size: "sm", title: "Sneltoets N", children: _jsxs(Link, { to: "/voorraad/nieuw", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Nieuw artikel"] }) })] })] }), guardTrips.length > 0 && (_jsx("div", { role: "alert", className: "rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive", children: "Deze pagina vroeg te vaak gegevens op en is daarmee gestopt. Open de pagina opnieuw." })), _jsx(StockIntegrityAlert, {}), _jsx(FloridayConnectionNotice, { status: conn.status, environment: conn.selectedEnvironment, error: conn.error }), error && (_jsxs("div", { role: "alert", className: "rounded-md border border-destructive/40 bg-destructive/10 text-destructive p-3 text-sm", children: ["Voorraad laden mislukt: ", error] })), melding.length > 0 && (_jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-1 rounded-md border border-border bg-muted/40 px-3 py-2 text-sm", children: [_jsx(AlertTriangle, { className: "h-4 w-4 shrink-0 text-muted-foreground" }), melding.map((m) => m.filter ? (_jsx("button", { type: "button", className: "underline-offset-2 hover:underline", onClick: () => setFilter(m.filter), children: m.tekst }, m.tekst)) : (_jsx("span", { children: m.tekst }, m.tekst)))] })), _jsxs(Card, { className: "overflow-hidden p-0", children: [_jsx("div", { role: "tablist", "aria-label": "Filter", className: "flex flex-wrap items-center gap-1 border-b px-2", children: filters.map((f) => {
|
|
117
|
+
const actief = filter === f;
|
|
118
|
+
const n = data?.tellers?.[f];
|
|
119
|
+
return (_jsxs("button", { role: "tab", "aria-selected": actief, type: "button", onClick: () => setFilter(f), className: `relative px-3 py-3 text-sm font-medium whitespace-nowrap transition-colors ${actief ? "text-foreground" : "text-muted-foreground hover:text-foreground"}`, children: [FILTER_LABEL[f], _jsx("span", { className: `ml-2 text-xs tabular-nums ${actief ? "text-primary" : "text-muted-foreground/70"}`, children: n == null ? "…" : aantal(n) }), actief && _jsx("span", { className: "absolute left-2 right-2 -bottom-px h-0.5 rounded-full bg-primary" })] }, f));
|
|
120
|
+
}) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2 border-b p-3", children: [_jsxs("div", { className: "relative flex-1 min-w-[220px] max-w-md", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { placeholder: "Zoek op naam, artikelnummer of barcode\u2026", className: "pl-9", value: zoekInvoer, onChange: (e) => setZoekInvoer(e.target.value), "aria-label": "Zoeken" })] }), _jsx("div", { className: "ml-auto", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", children: [_jsx(Columns3, { className: "mr-2 h-4 w-4" }), "Kolommen"] }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsx(DropdownMenuLabel, { children: "Kolommen kiezen" }), OPTIONELE_KOLOMMEN.map((c) => (_jsx(DropdownMenuCheckboxItem, { checked: kolommen.has(c.key), onCheckedChange: () => toggleKolom(c.key), children: c.label }, c.key)))] })] }) })] }), selected.size > 0 && filter !== "twijfel" && (_jsxs("div", { className: "flex flex-wrap items-center gap-3 border-b bg-primary/5 px-4 py-2 text-sm", children: [_jsxs("span", { className: "font-medium", children: [selected.size, " gekozen"] }), _jsx(Button, { size: "sm", onClick: () => setDrawerOpen(true), children: "Voorraadsturing instellen" }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => setSelected(new Set()), children: "Keuze wissen" })] })), _jsx("div", { className: "overflow-x-auto", children: filter === "twijfel" ? (_jsx(TwijfelTabel, { rijen: data?.twijfel_rijen ?? [], loading: loading, onGekozen: () => void reload() })) : (_jsx(VoorraadLijstTabel, { rijen: data?.rijen ?? [], loading: loading, sort: sorteer.sort, oplopend: sorteer.oplopend, onSort: (k) => setSorteer((cur) => volgendeSortering(cur, k)), kolommen: kolommen, selected: selected, onToggleSelect: toggleSelect, onToggleAll: toggleAll, onAanpassen: setAanpassen, onAfboeken: setAfboeken, heeftKoppeling: heeftKoppeling })) }), _jsx(VoorraadPagination, { total: data?.totaal ?? 0, page: pagina, pageSize: grootte, onPageChange: setPagina, onPageSizeChange: setGrootte })] }), _jsx(AanpassenDialog, { rij: aanpassen, onClose: () => setAanpassen(null), onDone: reload }), _jsx(AfboekenDialog, { rij: afboeken, connectionId: stockConnId, onClose: () => setAfboeken(null), onDone: reload }), _jsx(InventorySettingsDrawer, { open: drawerOpen, onOpenChange: setDrawerOpen, connectionId: conn.activeId ?? null, productIds: [...selected], onSaved: reload })] }));
|
|
121
|
+
}
|
|
122
|
+
function TwijfelTabel({ rijen, loading, onGekozen }) {
|
|
123
|
+
const [open, setOpen] = useState(null);
|
|
124
|
+
if (!loading && rijen.length === 0) {
|
|
125
|
+
return _jsx("p", { className: "px-4 py-10 text-center text-sm text-muted-foreground", children: "Geen twijfelgevallen." });
|
|
126
|
+
}
|
|
127
|
+
return (_jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "border-b bg-muted/40 text-xs text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-3 py-2 text-left font-medium", children: "Floriday-artikel" }), _jsx("th", { className: "px-3 py-2 text-left font-medium", children: "Reden" }), _jsx("th", { className: "px-3 py-2 text-right font-medium", children: "Mogelijke artikelen" }), _jsx("th", { className: "px-3 py-2 text-right font-medium", children: "Actie" })] }) }), _jsx("tbody", { children: rijen.map((t) => (_jsxs(Fragment, { children: [_jsxs("tr", { className: "border-b", children: [_jsx("td", { className: "px-3 py-2 font-medium", children: t.naam ?? "(naamloos)" }), _jsx("td", { className: "px-3 py-2 text-muted-foreground", children: t.reden ?? "Meerdere mogelijke artikelen; niets aangemaakt" }), _jsx("td", { className: "px-3 py-2 text-right tabular-nums", children: t.kandidaten }), _jsx("td", { className: "px-3 py-2 text-right", children: _jsx(Button, { size: "sm", variant: "outline", onClick: () => setOpen(open === t.floriday_id ? null : t.floriday_id), children: "Zelf kiezen" }) })] }), open === t.floriday_id && (_jsx("tr", { className: "border-b", children: _jsx("td", { colSpan: 4, className: "px-3 py-2", children: _jsx(TwijfelKiezen, { floridayId: t.floriday_id, onGekozen: () => { setOpen(null); onGekozen?.(); } }) }) }))] }, t.floriday_id))) })] }));
|
|
166
128
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shellinstelling: welke voorraadsturing krijgen artikelen die automatisch uit
|
|
3
|
+
* Floriday worden aangemaakt. Bestaande artikelen veranderen niet.
|
|
4
|
+
*/
|
|
5
|
+
export declare function VoorraadSturingCard(): import("react").JSX.Element;
|
|
6
|
+
//# sourceMappingURL=VoorraadSturingCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoorraadSturingCard.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/VoorraadSturingCard.tsx"],"names":[],"mappings":"AAiBA;;;GAGG;AACH,wBAAgB,mBAAmB,gCAgElC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Label, toast, } from "@flowselections/core";
|
|
4
|
+
import { Loader2 } from "lucide-react";
|
|
5
|
+
import { supabase } from "../../integrations/supabase/client.js";
|
|
6
|
+
import { dbFoutTekst } from "../../lib/db-error-message.js";
|
|
7
|
+
/**
|
|
8
|
+
* Shellinstelling: welke voorraadsturing krijgen artikelen die automatisch uit
|
|
9
|
+
* Floriday worden aangemaakt. Bestaande artikelen veranderen niet.
|
|
10
|
+
*/
|
|
11
|
+
export function VoorraadSturingCard() {
|
|
12
|
+
const [opgeslagen, setOpgeslagen] = useState(null);
|
|
13
|
+
const [keuze, setKeuze] = useState("lokaal");
|
|
14
|
+
const [bezig, setBezig] = useState(false);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
void supabase
|
|
17
|
+
.rpc("voorraad_instellingen_lezen")
|
|
18
|
+
.then(({ data, error }) => {
|
|
19
|
+
if (error)
|
|
20
|
+
toast.error(dbFoutTekst(error));
|
|
21
|
+
const v = (data?.standaard_sturing ?? "lokaal");
|
|
22
|
+
setOpgeslagen(v);
|
|
23
|
+
setKeuze(v);
|
|
24
|
+
});
|
|
25
|
+
}, []);
|
|
26
|
+
const opslaan = async () => {
|
|
27
|
+
setBezig(true);
|
|
28
|
+
const { data, error } = await supabase.rpc("voorraad_instellingen_opslaan", { p_standaard_sturing: keuze });
|
|
29
|
+
setBezig(false);
|
|
30
|
+
if (error) {
|
|
31
|
+
toast.error(dbFoutTekst(error));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const r = data;
|
|
35
|
+
if (!r.ok) {
|
|
36
|
+
toast.error(r.melding ?? "Niet opgeslagen");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
setOpgeslagen(keuze);
|
|
40
|
+
toast.success("Standaard voorraadsturing opgeslagen");
|
|
41
|
+
};
|
|
42
|
+
const opties = [
|
|
43
|
+
{ waarde: "lokaal", titel: "Lokaal", uitleg: "Je eigen voorraad leidt; Floriday wordt niet bijgewerkt tot je dat per artikel aanzet." },
|
|
44
|
+
{ waarde: "floriday", titel: "Floriday volgt", uitleg: "Je eigen voorraad gaat via de gewone wachtrij naar Floriday." },
|
|
45
|
+
];
|
|
46
|
+
return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: "Standaard voorraadsturing" }), _jsx(CardDescription, { children: "Geldt voor artikelen die automatisch uit Floriday worden aangemaakt. Bestaande artikelen veranderen niet." })] }), _jsx(CardContent, { className: "space-y-3", children: opgeslagen === null ? (_jsx(Loader2, { className: "h-4 w-4 animate-spin text-muted-foreground" })) : (_jsxs(_Fragment, { children: [_jsx("div", { role: "radiogroup", className: "space-y-2", children: opties.map((o) => (_jsxs(Label, { className: "flex cursor-pointer items-start gap-2 rounded-md border p-3 font-normal", children: [_jsx("input", { type: "radio", name: "standaard-sturing", className: "mt-1", checked: keuze === o.waarde, onChange: () => setKeuze(o.waarde) }), _jsxs("span", { children: [_jsx("span", { className: "font-medium", children: o.titel }), _jsx("span", { className: "block text-sm text-muted-foreground", children: o.uitleg })] })] }, o.waarde))) }), _jsxs(Button, { onClick: opslaan, disabled: bezig || keuze === opgeslagen, children: [bezig && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), "Opslaan"] })] })) })] }));
|
|
47
|
+
}
|