@flowselections/floriday-voorraad 1.0.67 → 1.0.69

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.
@@ -2005,7 +2005,7 @@ function StepVbn({ data, update, acceptedVbnCodes }) {
2005
2005
  function InfoTip({ text }) {
2006
2006
  return (_jsx("span", { title: text, className: "inline-flex cursor-help text-muted-foreground", children: _jsx(Info, { className: "h-3.5 w-3.5" }) }));
2007
2007
  }
2008
- function StepArtikel({ data, update, collectionsList, setCollectionsList, articleCodeLocked }) {
2008
+ function StepArtikel({ data, update, collectionsList, setCollectionsList, articleCodeLocked, parentProductId }) {
2009
2009
  const [newCollection, setNewCollection] = useState("");
2010
2010
  const [showNewCollection, setShowNewCollection] = useState(false);
2011
2011
  const addCollection = async (name) => {
@@ -2063,9 +2063,10 @@ function StepArtikel({ data, update, collectionsList, setCollectionsList, articl
2063
2063
  return (_jsxs("button", { type: "button", onClick: () => update("articleType", t.value), className: `flex flex-col items-center gap-2 rounded-lg border p-4 text-center transition-colors ${active
2064
2064
  ? "border-primary bg-primary text-primary-foreground"
2065
2065
  : "border-border hover:border-primary/50"}`, children: [_jsx("div", { className: "flex w-full items-start", children: _jsx("span", { className: `flex h-4 w-4 items-center justify-center rounded-full border ${active ? "border-primary-foreground" : "border-muted-foreground/50"}`, children: active && (_jsx("span", { className: "h-2 w-2 rounded-full bg-primary-foreground" })) }) }), _jsx(Icon, { className: "h-6 w-6" }), _jsx("div", { className: "font-medium", children: t.label }), _jsx("div", { className: `text-xs ${active ? "text-primary-foreground/80" : "text-muted-foreground"}`, children: t.description })] }, t.value));
2066
- }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsxs("label", { className: "flex items-start gap-2 text-sm", children: [_jsx(Checkbox, { checked: data.visibleInExplorer, onCheckedChange: (v) => update("visibleInExplorer", v === true), className: "mt-0.5" }), _jsx("span", { children: "Artikel weergeven bij ontdekken in de Explorer voor kopers" })] }), _jsx("p", { className: "pl-6 text-xs text-muted-foreground", children: "Artikel is altijd zichtbaar via de customer API" })] })] }));
2066
+ }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsxs("label", { className: "flex items-start gap-2 text-sm", children: [_jsx(Checkbox, { checked: data.visibleInExplorer, onCheckedChange: (v) => update("visibleInExplorer", v === true), className: "mt-0.5" }), _jsx("span", { children: "Artikel weergeven bij ontdekken in de Explorer voor kopers" })] }), _jsx("p", { className: "pl-6 text-xs text-muted-foreground", children: "Artikel is altijd zichtbaar via de customer API" })] }), parentProductId && (_jsx(ProductSalePriceField, { productId: parentProductId, onSaved: (p) => update("salePricePerPiece", p) }))] }));
2067
2067
  }
2068
2068
  import { COUNTRIES } from "../../lib/countries";
2069
+ import { ProductSalePriceField } from "./ProductSalePriceField";
2069
2070
  function StepPaspoort({ data, update }) {
2070
2071
  const [countryOpen, setCountryOpen] = useState(false);
2071
2072
  const [countryQuery, setCountryQuery] = useState("");
@@ -2795,7 +2796,7 @@ function StepBeschikbaarheid({ data, update }) {
2795
2796
  // ------------------------------------------------------------
2796
2797
  // Stap "Aanbieden & voorraad" — Direct Sale via CustomerOffer
2797
2798
  // ------------------------------------------------------------
2798
- function StepAanbieden({ data, update, priceGroups }) {
2799
+ function StepAanbieden({ data, update, priceGroups, parentProductId }) {
2799
2800
  const priceStr = Number.isFinite(data.salePricePerPiece)
2800
2801
  ? String(data.salePricePerPiece)
2801
2802
  : "";
@@ -2820,7 +2821,7 @@ function StepAanbieden({ data, update, priceGroups }) {
2820
2821
  : "Alle gekoppelde Floriday-klanten" }), _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Klantselectie beheer je in de stap ", _jsx("em", { children: "Klanten" }), "."] })] })] }), _jsxs("div", { className: "space-y-2 rounded-md border p-4", children: [_jsxs("div", { className: "flex items-baseline justify-between", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-medium", children: "Prijzen per Floriday-prijsgroep" }), _jsx("div", { className: "text-xs text-muted-foreground", children: "Laat leeg om voor die groep g\u00E9\u00E9n afwijkende prijs te publiceren. Basisprijs blijft dan gelden." })] }), _jsxs("div", { className: "text-xs text-muted-foreground", children: [priceGroups.length, " groep", priceGroups.length === 1 ? "" : "en"] })] }), priceGroups.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground py-4", children: "Geen actieve prijsgroepen gevonden voor deze Floriday-connectie." })) : (_jsx("div", { className: "grid gap-3 sm:grid-cols-2 pt-2", children: priceGroups.map((g) => {
2821
2822
  const val = data.priceGroupPrices?.[g.id];
2822
2823
  return (_jsxs("div", { className: "space-y-1", children: [_jsxs(Label, { className: "text-sm", children: [g.name, _jsxs("span", { className: "ml-2 text-xs font-normal text-muted-foreground", children: [g.customerOrganizationIds.length, " klant", g.customerOrganizationIds.length === 1 ? "" : "en"] })] }), _jsx(Input, { type: "number", inputMode: "decimal", min: 0, step: "0.01", placeholder: "\u20AC", value: val ? String(val) : "", onChange: (e) => setGroupPrice(g.id, e.target.value) })] }, g.id));
2823
- }) }))] })] })), _jsx(VatSection, { data: data, update: update })] }));
2824
+ }) }))] })] })), parentProductId && (_jsx(ProductSalePriceField, { productId: parentProductId, onSaved: (p) => update("salePricePerPiece", p) })), _jsx(VatSection, { data: data, update: update })] }));
2824
2825
  }
2825
2826
  // ------------------------------------------------------------
2826
2827
  // BTW-blok (facturatie) — lokaal, niet naar Floriday
@@ -1 +1 @@
1
- {"version":3,"file":"InventorySettingsDrawer.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/InventorySettingsDrawer.tsx"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAiBzE,wBAAgB,uBAAuB,CAAC,EACtC,IAAI,EACJ,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,OAAO,EACP,OAAO,GACR,EAAE;IACD,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACvC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB,+BAiMA"}
1
+ {"version":3,"file":"InventorySettingsDrawer.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/InventorySettingsDrawer.tsx"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAkBzE,wBAAgB,uBAAuB,CAAC,EACtC,IAAI,EACJ,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,OAAO,EACP,OAAO,GACR,EAAE;IACD,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACvC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB,+BAkMA"}
@@ -8,6 +8,7 @@ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogH
8
8
  import { useServerFn } from "@tanstack/react-start";
9
9
  import { toast } from "sonner";
10
10
  import { saveInventorySettings, republishInventory, } from "../../lib/inventory-settings.functions";
11
+ import { MissingPriceNotice } from "./ProductSalePriceField";
11
12
  import { MODE_LABELS, ACTION_REASON_LABELS, publishedLabel, } from "../../lib/stock-modes";
12
13
  const MODE_HELP = {
13
14
  FOLLOW_ERP: "Publiceert beschikbaar min gereserveerd min buffer, afgerond op hele verpakkingen.",
@@ -95,5 +96,5 @@ export function InventorySettingsDrawer({ open, onOpenChange, connectionId, prod
95
96
  const activeMode = (mode || current?.stock_mode || "");
96
97
  return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Voorraadsturing Floriday" }), _jsx(DialogDescription, { children: bulk
97
98
  ? `${productIds.length} artikelen. Alleen de velden die je invult worden aangepast.`
98
- : "Bepaal welk aantal er naar Floriday wordt gepubliceerd." })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { children: "Modus" }), _jsxs(Select, { value: mode, onValueChange: (v) => setMode(v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: bulk ? "Niet wijzigen" : "Kies een modus" }) }), _jsx(SelectContent, { children: Object.keys(MODE_LABELS).map((m) => (_jsx(SelectItem, { value: m, children: MODE_LABELS[m] }, m))) })] }), activeMode && (_jsx("p", { className: "text-xs text-muted-foreground", children: MODE_HELP[activeMode] }))] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { children: "Buffer" }), _jsx(Input, { inputMode: "numeric", value: buffer, onChange: (e) => setBuffer(e.target.value), placeholder: bulk ? "Niet wijzigen" : "0" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Aantal dat je achterhoudt en dus niet aanbiedt." })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { children: "Maximum" }), _jsx(Input, { inputMode: "numeric", value: maximum, onChange: (e) => setMaximum(e.target.value), placeholder: bulk ? "Niet wijzigen" : "Geen", disabled: activeMode !== "" && activeMode !== "MAXIMUM" && !bulk })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { children: "Vast aantal" }), _jsx(Input, { inputMode: "numeric", value: fixed, onChange: (e) => setFixed(e.target.value), placeholder: bulk ? "Niet wijzigen" : "Geen", disabled: activeMode !== "" && activeMode !== "FIXED_REFILL" && !bulk })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { children: "Handmatig aantal" }), _jsx(Input, { inputMode: "numeric", value: manual, onChange: (e) => setManual(e.target.value), placeholder: bulk ? "Niet wijzigen" : "Geen", disabled: activeMode !== "" && activeMode !== "MANUAL" && !bulk })] })] }), !bulk && current && (_jsxs("div", { className: "rounded-md border bg-muted/40 p-3 text-xs text-muted-foreground space-y-1", children: [_jsxs("div", { children: [_jsx("strong", { className: "text-foreground", children: publishedLabel(current.desired_quantity ?? 0, current.packing_size) }), " ", "\u00B7 In Floriday:", " ", _jsx("strong", { className: "text-foreground", children: current.remote_checked_at ? (current.remote_quantity ?? "geen aanbod") : "nog niet gecontroleerd" })] }), current.last_published_at && (_jsxs("div", { children: ["Laatst verstuurd:", " ", new Date(current.last_published_at).toLocaleString("nl-NL")] })), current.sync_status === "ACTION_REQUIRED" && (_jsxs("div", { className: "text-destructive", children: ["Actie nodig: ", ACTION_REASON_LABELS[current.action_reason ?? ""] ?? current.action_reason, ". Er gaat niets naar Floriday; de voorraad zelf is wel opgeslagen."] })), current.last_sync_error && (_jsxs("div", { className: "text-destructive", children: ["Laatste fout: ", current.last_sync_error] }))] }))] }), _jsxs(DialogFooter, { className: "gap-2", children: [_jsx(Button, { variant: "outline", onClick: onRepublish, disabled: busy, children: "Opnieuw versturen" }), _jsx(Button, { onClick: onSave, disabled: busy, children: busy ? "Opslaan..." : "Opslaan" })] })] }) }));
99
+ : "Bepaal welk aantal er naar Floriday wordt gepubliceerd." })] }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { children: "Modus" }), _jsxs(Select, { value: mode, onValueChange: (v) => setMode(v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: bulk ? "Niet wijzigen" : "Kies een modus" }) }), _jsx(SelectContent, { children: Object.keys(MODE_LABELS).map((m) => (_jsx(SelectItem, { value: m, children: MODE_LABELS[m] }, m))) })] }), activeMode && (_jsx("p", { className: "text-xs text-muted-foreground", children: MODE_HELP[activeMode] }))] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { children: "Buffer" }), _jsx(Input, { inputMode: "numeric", value: buffer, onChange: (e) => setBuffer(e.target.value), placeholder: bulk ? "Niet wijzigen" : "0" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Aantal dat je achterhoudt en dus niet aanbiedt." })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { children: "Maximum" }), _jsx(Input, { inputMode: "numeric", value: maximum, onChange: (e) => setMaximum(e.target.value), placeholder: bulk ? "Niet wijzigen" : "Geen", disabled: activeMode !== "" && activeMode !== "MAXIMUM" && !bulk })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { children: "Vast aantal" }), _jsx(Input, { inputMode: "numeric", value: fixed, onChange: (e) => setFixed(e.target.value), placeholder: bulk ? "Niet wijzigen" : "Geen", disabled: activeMode !== "" && activeMode !== "FIXED_REFILL" && !bulk })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { children: "Handmatig aantal" }), _jsx(Input, { inputMode: "numeric", value: manual, onChange: (e) => setManual(e.target.value), placeholder: bulk ? "Niet wijzigen" : "Geen", disabled: activeMode !== "" && activeMode !== "MANUAL" && !bulk })] })] }), !bulk && current && (_jsxs("div", { className: "rounded-md border bg-muted/40 p-3 text-xs text-muted-foreground space-y-1", children: [_jsxs("div", { children: [_jsx("strong", { className: "text-foreground", children: publishedLabel(current.desired_quantity ?? 0, current.packing_size) }), " ", "\u00B7 In Floriday:", " ", _jsx("strong", { className: "text-foreground", children: current.remote_checked_at ? (current.remote_quantity ?? "geen aanbod") : "nog niet gecontroleerd" })] }), current.last_published_at && (_jsxs("div", { children: ["Laatst verstuurd:", " ", new Date(current.last_published_at).toLocaleString("nl-NL")] })), !bulk && productIds[0] && _jsx(MissingPriceNotice, { productId: productIds[0] }), current.sync_status === "ACTION_REQUIRED" && (_jsxs("div", { className: "text-destructive", children: ["Actie nodig: ", ACTION_REASON_LABELS[current.action_reason ?? ""] ?? current.action_reason, ". Er gaat niets naar Floriday; de voorraad zelf is wel opgeslagen."] })), current.last_sync_error && (_jsxs("div", { className: "text-destructive", children: ["Laatste fout: ", current.last_sync_error] }))] }))] }), _jsxs(DialogFooter, { className: "gap-2", children: [_jsx(Button, { variant: "outline", onClick: onRepublish, disabled: busy, children: "Opnieuw versturen" }), _jsx(Button, { onClick: onSave, disabled: busy, children: busy ? "Opslaan..." : "Opslaan" })] })] }) }));
99
100
  }
@@ -1 +1 @@
1
- {"version":3,"file":"NieuwArtikelPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/NieuwArtikelPage.tsx"],"names":[],"mappings":"AAgBA,wBAAgB,gBAAgB,gCA2K/B"}
1
+ {"version":3,"file":"NieuwArtikelPage.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/NieuwArtikelPage.tsx"],"names":[],"mappings":"AAgBA,wBAAgB,gBAAgB,gCAiM/B"}
@@ -5,7 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
5
5
  * lijst van wat daarvoor nog ontbreekt. De volledige Floriday-invoer blijft
6
6
  * beschikbaar via de knop onderaan.
7
7
  */
8
- import { useState } from "react";
8
+ import { useRef, useState } from "react";
9
9
  import { Link, useNavigate } from "@tanstack/react-router";
10
10
  import { Button, Card, Input, Label, toast } from "@flowselections/core";
11
11
  import { ArrowLeft, Check, Loader2, Send } from "lucide-react";
@@ -23,46 +23,60 @@ export function NieuwArtikelPage() {
23
23
  const [price, setPrice] = useState("");
24
24
  const [saving, setSaving] = useState(false);
25
25
  const [createdId, setCreatedId] = useState(null);
26
+ const [saveError, setSaveError] = useState(null);
27
+ // Voorkomt dubbele artikelen bij snel herhaald klikken of opnieuw proberen.
28
+ const savingRef = useRef(false);
29
+ const insertedIdRef = useRef(null);
26
30
  if (full)
27
31
  return _jsx(ArtikelWizard, {});
28
32
  const canNext = step === 0 ? name.trim().length > 0 : step === 1 ? Number(stock) >= 0 && stock !== "" : true;
29
33
  const save = async () => {
34
+ if (savingRef.current)
35
+ return;
36
+ savingRef.current = true;
30
37
  setSaving(true);
38
+ setSaveError(null);
31
39
  try {
32
40
  const priceNum = price.trim() === "" ? null : Number(price.replace(",", "."));
33
- if (priceNum !== null && !(priceNum >= 0))
34
- throw new Error("Vul een geldige prijs in");
35
- const { data, error } = await supabase
36
- .from("products")
37
- .insert({
38
- product: name.trim(),
39
- article_number: code.trim() || null,
40
- unit: "stuks",
41
- sale_price: priceNum,
42
- batch: "",
43
- location: "",
44
- })
45
- .select("id")
46
- .single();
47
- if (error)
48
- throw new Error(error.message);
49
- const id = data.id;
41
+ if (priceNum !== null && !(priceNum > 0))
42
+ throw new Error("Vul een geldige prijs in (groter dan 0)");
43
+ let id = insertedIdRef.current;
44
+ if (!id) {
45
+ const { data, error } = await supabase
46
+ .from("products")
47
+ .insert({
48
+ product: name.trim(),
49
+ article_number: code.trim() || null,
50
+ unit: "stuks",
51
+ sale_price: priceNum,
52
+ batch: "",
53
+ location: "",
54
+ })
55
+ .select("id")
56
+ .single();
57
+ if (error)
58
+ throw new Error(`Artikel niet opgeslagen: ${error.message}`);
59
+ id = data.id;
60
+ insertedIdRef.current = id;
61
+ }
50
62
  const { error: stockErr } = await supabase.rpc("set_product_stock", {
51
63
  p_product_id: id,
52
64
  p_physical_stock: Math.trunc(Number(stock) || 0),
53
- p_minimum_stock: null,
54
- p_incoming_stock: null,
55
65
  p_notes: "Nieuw artikel",
56
66
  });
57
- if (stockErr)
58
- throw new Error(`Artikel aangemaakt, voorraad niet opgeslagen: ${stockErr.message}`);
67
+ if (stockErr) {
68
+ throw new Error(`Artikel is aangemaakt, maar de voorraad is niet opgeslagen: ${stockErr.message}. Klik opnieuw op "Artikel opslaan" om alleen de voorraad nog eens te proberen.`);
69
+ }
59
70
  toast.success("Artikel aangemaakt");
60
71
  setCreatedId(id);
61
72
  }
62
73
  catch (e) {
63
- toast.error(e?.message ?? "Opslaan mislukt");
74
+ const msg = e?.message ?? "Opslaan mislukt";
75
+ setSaveError(msg);
76
+ toast.error(msg);
64
77
  }
65
78
  finally {
79
+ savingRef.current = false;
66
80
  setSaving(false);
67
81
  }
68
82
  };
@@ -71,5 +85,5 @@ export function NieuwArtikelPage() {
71
85
  const hasPrice = Number(price.replace(",", ".")) > 0;
72
86
  return (_jsx("main", { className: "p-6 space-y-4 max-w-2xl", children: _jsxs(Card, { className: "p-5 space-y-3", children: [_jsxs("div", { className: "flex items-center gap-2 text-emerald-700", children: [_jsx(Check, { className: "h-5 w-5" }), _jsxs("p", { className: "font-medium", children: [name, " is aangemaakt."] })] }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Wil je het artikel ook op Floriday zetten? Daarvoor is nog nodig:" }), _jsxs("ul", { className: "list-disc pl-5 text-sm", children: [!hasPrice && _jsx("li", { children: "Prijs per stuk (excl. btw)" }), missing.map((m) => (_jsx("li", { children: m }, m)))] }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsxs(Button, { onClick: () => navigate({ to: "/voorraad/$id", params: { id: createdId } }), children: [_jsx(Send, { className: "mr-2 h-4 w-4" }), "Op Floriday zetten"] }), _jsx(Button, { variant: "outline", asChild: true, children: _jsx(Link, { to: "/voorraad", children: "Terug naar voorraad" }) })] })] }) }));
73
87
  }
74
- return (_jsxs("main", { className: "p-6 space-y-4 max-w-2xl", children: [_jsxs(Link, { to: "/voorraad", className: "inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Terug naar voorraad"] }), _jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold", children: "Artikel toevoegen" }), _jsxs("p", { className: "text-sm text-muted-foreground", children: ["Stap ", step + 1, " van ", STEPS.length, ": ", STEPS[step]] })] }), _jsxs(Card, { className: "p-5 space-y-4", children: [step === 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-name", children: "Naam" }), _jsx(Input, { id: "na-name", value: name, onChange: (e) => setName(e.target.value), autoFocus: true })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-code", children: "Artikelcode (optioneel)" }), _jsx(Input, { id: "na-code", value: code, onChange: (e) => setCode(e.target.value) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Leeg laten: er wordt automatisch een code gemaakt." })] })] })), step === 1 && (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-stock", children: "Voorraad (stuks)" }), _jsx(Input, { id: "na-stock", type: "number", min: 0, value: stock, onChange: (e) => setStock(e.target.value), autoFocus: true })] })), step === 2 && (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-price", children: "Prijs per stuk (EUR, excl. btw)" }), _jsx(Input, { id: "na-price", inputMode: "decimal", value: price, onChange: (e) => setPrice(e.target.value), placeholder: "Bijv. 1,10", autoFocus: true }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Zonder prijs komt het artikel niet in Floriday (\"Prijs ontbreekt\")." })] })), _jsxs("div", { className: "flex flex-wrap justify-between gap-2", children: [_jsx(Button, { variant: "outline", disabled: step === 0, onClick: () => setStep((s) => s - 1), children: "Vorige" }), step < STEPS.length - 1 ? (_jsx(Button, { disabled: !canNext, onClick: () => setStep((s) => s + 1), children: "Volgende" })) : (_jsxs(Button, { disabled: saving || !name.trim(), onClick: () => void save(), children: [saving ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(Check, { className: "mr-2 h-4 w-4" }), "Artikel opslaan"] }))] })] }), _jsx("button", { type: "button", className: "text-sm text-muted-foreground underline hover:text-foreground", onClick: () => setFull(true), children: "Meteen alle Floriday-gegevens invullen" })] }));
88
+ return (_jsxs("main", { className: "p-6 space-y-4 max-w-2xl", children: [_jsxs(Link, { to: "/voorraad", className: "inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Terug naar voorraad"] }), _jsxs("div", { children: [_jsx("h1", { className: "text-xl font-semibold", children: "Artikel toevoegen" }), _jsxs("p", { className: "text-sm text-muted-foreground", children: ["Stap ", step + 1, " van ", STEPS.length, ": ", STEPS[step]] })] }), _jsxs(Card, { className: "p-5 space-y-4", children: [step === 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-name", children: "Naam" }), _jsx(Input, { id: "na-name", value: name, onChange: (e) => setName(e.target.value), autoFocus: true })] }), _jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-code", children: "Artikelcode (optioneel)" }), _jsx(Input, { id: "na-code", value: code, onChange: (e) => setCode(e.target.value) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Leeg laten: er wordt automatisch een code gemaakt." })] })] })), step === 1 && (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-stock", children: "Voorraad (stuks)" }), _jsx(Input, { id: "na-stock", type: "number", min: 0, value: stock, onChange: (e) => setStock(e.target.value), autoFocus: true })] })), step === 2 && (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { htmlFor: "na-price", children: "Prijs per stuk (EUR, excl. btw)" }), _jsx(Input, { id: "na-price", inputMode: "decimal", value: price, onChange: (e) => setPrice(e.target.value), placeholder: "Bijv. 1,10", autoFocus: true }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Zonder prijs komt het artikel niet in Floriday (\"Prijs ontbreekt\")." })] })), saveError && (_jsx("div", { role: "alert", className: "rounded-md border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive", children: saveError })), _jsxs("div", { className: "flex flex-wrap justify-between gap-2", children: [_jsx(Button, { variant: "outline", disabled: step === 0 || saving, onClick: () => setStep((s) => s - 1), children: "Vorige" }), step < STEPS.length - 1 ? (_jsx(Button, { disabled: !canNext, onClick: () => setStep((s) => s + 1), children: "Volgende" })) : (_jsxs(Button, { disabled: saving || !name.trim(), onClick: () => void save(), children: [saving ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(Check, { className: "mr-2 h-4 w-4" }), saving ? "Bezig met opslaan…" : "Artikel opslaan"] }))] })] }), _jsx("button", { type: "button", className: "text-sm text-muted-foreground underline hover:text-foreground", onClick: () => setFull(true), children: "Meteen alle Floriday-gegevens invullen" })] }));
75
89
  }
@@ -0,0 +1,9 @@
1
+ export declare function ProductSalePriceField({ productId, onSaved, }: {
2
+ productId: string;
3
+ onSaved?: (price: number) => void;
4
+ }): import("react").JSX.Element | null;
5
+ /** Melding met directe link naar het prijsveld; toont niets als de prijs in orde is. */
6
+ export declare function MissingPriceNotice({ productId }: {
7
+ productId: string;
8
+ }): import("react").JSX.Element | null;
9
+ //# sourceMappingURL=ProductSalePriceField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProductSalePriceField.d.ts","sourceRoot":"","sources":["../../../src/components/voorraad/ProductSalePriceField.tsx"],"names":[],"mappings":"AAkBA,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,OAAO,GACR,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,sCAkGA;AAED,wFAAwF;AACxF,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,sCAmCtE"}
@@ -0,0 +1,111 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Verkoopprijs per stuk (excl. btw) van een bestaand artikel.
4
+ *
5
+ * Leest en schrijft rechtstreeks `products.sale_price`: dat is de prijs die de
6
+ * Floriday-voorraadsync gebruikt. Bij opslaan zet de database de laatste
7
+ * wachtrijregel "Prijs ontbreekt" automatisch opnieuw klaar.
8
+ */
9
+ import { useEffect, useState } from "react";
10
+ import { Link } from "@tanstack/react-router";
11
+ import { Button, Input, Label, toast } from "@flowselections/core";
12
+ import { AlertTriangle, Loader2 } from "lucide-react";
13
+ import { supabase } from "../../integrations/supabase/client";
14
+ function isPieceUnit(unit) {
15
+ const u = String(unit ?? "").trim().toLowerCase();
16
+ return u === "" || u === "stuks" || u === "stuk";
17
+ }
18
+ export function ProductSalePriceField({ productId, onSaved, }) {
19
+ const [loaded, setLoaded] = useState(false);
20
+ const [unit, setUnit] = useState(null);
21
+ const [saved, setSaved] = useState(null);
22
+ const [value, setValue] = useState("");
23
+ const [saving, setSaving] = useState(false);
24
+ const [error, setError] = useState(null);
25
+ const [exists, setExists] = useState(true);
26
+ useEffect(() => {
27
+ let cancelled = false;
28
+ (async () => {
29
+ const { data } = await supabase
30
+ .from("products")
31
+ .select("sale_price, unit")
32
+ .eq("id", productId)
33
+ .maybeSingle();
34
+ if (cancelled)
35
+ return;
36
+ if (!data) {
37
+ setExists(false);
38
+ return;
39
+ }
40
+ const p = data?.sale_price != null ? Number(data.sale_price) : null;
41
+ setSaved(p);
42
+ setUnit(data?.unit ?? null);
43
+ setValue(p && p > 0 ? String(p).replace(".", ",") : "");
44
+ setLoaded(true);
45
+ })();
46
+ return () => {
47
+ cancelled = true;
48
+ };
49
+ }, [productId]);
50
+ const parsed = Number(value.trim().replace(",", "."));
51
+ const valid = value.trim() !== "" && Number.isFinite(parsed) && parsed > 0 &&
52
+ Math.round(parsed * 1e5) === parsed * 1e5;
53
+ const dirty = valid && parsed !== saved;
54
+ const save = async () => {
55
+ if (!valid || saving)
56
+ return;
57
+ setSaving(true);
58
+ setError(null);
59
+ try {
60
+ const { data, error: err } = await supabase
61
+ .from("products")
62
+ .update({ sale_price: parsed })
63
+ .eq("id", productId)
64
+ .select("id");
65
+ if (err)
66
+ throw new Error(err.message);
67
+ if (!data || data.length === 0)
68
+ throw new Error("geen rechten om dit artikel te wijzigen");
69
+ setSaved(parsed);
70
+ onSaved?.(parsed);
71
+ toast.success("Prijs opgeslagen. Het artikel gaat opnieuw naar Floriday.");
72
+ }
73
+ catch (e) {
74
+ const msg = `Prijs niet opgeslagen: ${e?.message ?? e}`;
75
+ setError(msg);
76
+ toast.error(msg);
77
+ }
78
+ finally {
79
+ setSaving(false);
80
+ }
81
+ };
82
+ if (!exists)
83
+ return null;
84
+ return (_jsxs("div", { id: "verkoopprijs", className: "space-y-2 rounded-md border p-4", children: [_jsx(Label, { htmlFor: `price-${productId}`, children: "Verkoopprijs per stuk (excl. btw)" }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(Input, { id: `price-${productId}`, inputMode: "decimal", className: "w-40", placeholder: "Bijv. 1,40", disabled: !loaded || saving, value: value, onChange: (e) => setValue(e.target.value) }), _jsxs(Button, { size: "sm", disabled: !dirty || saving, onClick: () => void save(), children: [saving && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), saving ? "Bezig met opslaan…" : "Prijs opslaan"] })] }), value.trim() !== "" && !valid && (_jsx("p", { className: "text-xs text-destructive", children: "Vul een prijs groter dan 0 in, met maximaal 5 decimalen." })), loaded && !isPieceUnit(unit) && (_jsxs("p", { className: "text-xs text-amber-700", children: ["Eenheid onduidelijk (", unit, "): de prijs telt voor Floriday alleen bij eenheid \"stuks\"."] })), loaded && !(saved && saved > 0) && (_jsx("p", { className: "text-xs text-muted-foreground", children: "Zonder prijs komt dit artikel niet in Floriday (\"Prijs ontbreekt\")." })), error && _jsx("p", { role: "alert", className: "text-xs text-destructive", children: error })] }));
85
+ }
86
+ /** Melding met directe link naar het prijsveld; toont niets als de prijs in orde is. */
87
+ export function MissingPriceNotice({ productId }) {
88
+ const [state, setState] = useState(null);
89
+ useEffect(() => {
90
+ let cancelled = false;
91
+ (async () => {
92
+ const { data } = await supabase
93
+ .from("products")
94
+ .select("sale_price, unit")
95
+ .eq("id", productId)
96
+ .maybeSingle();
97
+ if (cancelled || !data)
98
+ return;
99
+ if (!isPieceUnit(data.unit))
100
+ setState("unit");
101
+ else
102
+ setState(Number(data.sale_price) > 0 ? "ok" : "missing");
103
+ })();
104
+ return () => {
105
+ cancelled = true;
106
+ };
107
+ }, [productId]);
108
+ if (state !== "missing" && state !== "unit")
109
+ return null;
110
+ return (_jsxs("div", { className: "flex flex-wrap items-center 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: "h-4 w-4 shrink-0" }), _jsx("span", { className: "flex-1", children: state === "missing" ? "Prijs ontbreekt: dit artikel gaat niet naar Floriday." : "Eenheid onduidelijk: de prijs telt alleen per stuk." }), _jsx(Link, { to: "/voorraad/$id", params: { id: productId }, hash: "verkoopprijs", className: "font-medium underline", children: "Prijs invullen" })] }));
111
+ }
@@ -145,7 +145,7 @@ export function VoorraadPage() {
145
145
  { id: "out_of_stock", label: "Niet op voorraad", count: counts.outOfStock },
146
146
  { id: "below_min", label: "Onder minimum", count: counts.belowMin },
147
147
  ];
148
- 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, disabled: !activeId, size: "sm", children: _jsxs(Link, { to: "/voorraad/nieuw", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Artikel toevoegen"] }) })] })] }), _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 ? "" : "en", " staan niet in Floriday omdat de prijs ontbreekt."] }), " ", "Vul een verkoopprijs per stuk (excl. btw) in bij het artikel."] }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => setNoPriceOnly((v) => !v), children: noPriceOnly ? "Alle artikelen tonen" : "Toon deze artikelen" })] })), _jsx(StockPushMonitor, { connectionId: activeId ?? null }), connError && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 text-destructive p-3 text-sm", children: ["Floriday-connectie laden mislukt: ", 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-4 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") }), _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) => {
148
+ 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, disabled: !activeId, size: "sm", children: _jsxs(Link, { to: "/voorraad/nieuw", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "Artikel toevoegen"] }) })] })] }), _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 ? "" : "en", " staan niet in Floriday omdat de prijs ontbreekt."] }), " ", "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(StockPushMonitor, { connectionId: activeId ?? null }), connError && (_jsxs("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 text-destructive p-3 text-sm", children: ["Floriday-connectie laden mislukt: ", 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-4 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") }), _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) => {
149
149
  const active = stockFilter === t.id;
150
150
  return (_jsxs("button", { type: "button", onClick: () => setStockFilter(t.id), className: `relative py-3 px-3 text-sm font-medium whitespace-nowrap transition-colors ${active
151
151
  ? "text-foreground"
@@ -29598,6 +29598,12 @@ export type Database = {
29598
29598
  };
29599
29599
  Returns: Json;
29600
29600
  };
29601
+ invoice_resync_concept_from_work_order: {
29602
+ Args: {
29603
+ _werkbon_id: string;
29604
+ };
29605
+ Returns: undefined;
29606
+ };
29601
29607
  invoice_save_branding_settings: {
29602
29608
  Args: {
29603
29609
  _payload: Json;
@@ -32003,6 +32009,13 @@ export type Database = {
32003
32009
  };
32004
32010
  Returns: undefined;
32005
32011
  };
32012
+ logistics_slip_note_hold_kenmerk: {
32013
+ Args: {
32014
+ _id: string;
32015
+ _reason: string;
32016
+ };
32017
+ Returns: undefined;
32018
+ };
32006
32019
  logistics_slip_note_keep_waiting: {
32007
32020
  Args: {
32008
32021
  _class?: string;