@flowselections/floriday-klanten-module 1.0.34 → 1.0.35
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/assets/floriday-logo.png.asset.json +11 -0
- package/dist-lib/components/klanten/CustomerAssortmentTab.d.ts +5 -0
- package/dist-lib/components/klanten/CustomerAssortmentTab.d.ts.map +1 -0
- package/dist-lib/components/klanten/CustomerAssortmentTab.js +74 -0
- package/dist-lib/components/klanten/FloridayPriceImportDialog.d.ts +5 -0
- package/dist-lib/components/klanten/FloridayPriceImportDialog.d.ts.map +1 -0
- package/dist-lib/components/klanten/FloridayPriceImportDialog.js +55 -0
- package/dist-lib/components/klanten/GroupedKlantDetail.d.ts.map +1 -1
- package/dist-lib/components/klanten/GroupedKlantDetail.js +4 -3
- package/dist-lib/components/klanten/KlantDetail.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantDetail.js +10 -2
- package/dist-lib/components/klanten/KlantenOverzicht.d.ts.map +1 -1
- package/dist-lib/components/klanten/KlantenOverzicht.js +24 -3
- package/dist-lib/components/klanten/pricegroups/CustomerPriceGroupSelect.d.ts +4 -0
- package/dist-lib/components/klanten/pricegroups/CustomerPriceGroupSelect.d.ts.map +1 -0
- package/dist-lib/components/klanten/pricegroups/CustomerPriceGroupSelect.js +47 -0
- package/dist-lib/components/klanten/pricegroups/PriceGroupFormPage.d.ts.map +1 -1
- package/dist-lib/components/klanten/pricegroups/PriceGroupFormPage.js +2 -1
- package/dist-lib/components/klanten/pricegroups/PriceGroupItemPrices.d.ts +5 -0
- package/dist-lib/components/klanten/pricegroups/PriceGroupItemPrices.d.ts.map +1 -0
- package/dist-lib/components/klanten/pricegroups/PriceGroupItemPrices.js +83 -0
- package/dist-lib/index.d.ts +8 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +8 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +595 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +595 -0
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +595 -0
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +597 -0
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/accounting/twinfield/mapping.js +13 -13
- package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts +1785 -0
- package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts.map +1 -1
- package/dist-lib/lib/accounting.functions.d.ts +1785 -0
- package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-analytics.functions.d.ts +595 -0
- package/dist-lib/lib/customer-analytics.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-assortment.functions.d.ts +26666 -0
- package/dist-lib/lib/customer-assortment.functions.d.ts.map +1 -0
- package/dist-lib/lib/customer-assortment.functions.js +280 -0
- package/dist-lib/lib/customer-notes.functions.d.ts +1190 -0
- package/dist-lib/lib/customer-notes.functions.d.ts.map +1 -1
- package/dist-lib/lib/customer-revenue.functions.d.ts +595 -0
- package/dist-lib/lib/customer-revenue.functions.d.ts.map +1 -1
- package/dist-lib/lib/customers.functions.d.ts +5355 -0
- package/dist-lib/lib/customers.functions.d.ts.map +1 -1
- package/dist-lib/lib/delivery-locations.functions.d.ts +2975 -0
- package/dist-lib/lib/delivery-locations.functions.d.ts.map +1 -1
- package/dist-lib/lib/errors.d.ts +12 -0
- package/dist-lib/lib/errors.d.ts.map +1 -1
- package/dist-lib/lib/errors.js +24 -0
- package/dist-lib/lib/external-customers.functions.d.ts +2380 -0
- package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-assortment.functions.d.ts +595 -0
- package/dist-lib/lib/floriday-assortment.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-catalog-prices.d.ts +33 -0
- package/dist-lib/lib/floriday-catalog-prices.d.ts.map +1 -0
- package/dist-lib/lib/floriday-catalog-prices.js +98 -0
- package/dist-lib/lib/floriday-discounts.functions.d.ts +1190 -0
- package/dist-lib/lib/floriday-discounts.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday-price-import.functions.d.ts +53265 -0
- package/dist-lib/lib/floriday-price-import.functions.d.ts.map +1 -0
- package/dist-lib/lib/floriday-price-import.functions.js +219 -0
- package/dist-lib/lib/floriday-trade-items.functions.d.ts +595 -0
- package/dist-lib/lib/floriday-trade-items.functions.d.ts.map +1 -1
- package/dist-lib/lib/floriday.server.d.ts +55 -0
- package/dist-lib/lib/floriday.server.d.ts.map +1 -0
- package/dist-lib/lib/floriday.server.js +765 -0
- package/dist-lib/lib/local-discounts.functions.d.ts +4165 -0
- package/dist-lib/lib/local-discounts.functions.d.ts.map +1 -1
- package/dist-lib/lib/logistics-events.functions.d.ts +595 -0
- package/dist-lib/lib/logistics-events.functions.d.ts.map +1 -1
- package/dist-lib/lib/logistics-strategy.functions.d.ts +2380 -0
- package/dist-lib/lib/logistics-strategy.functions.d.ts.map +1 -1
- package/dist-lib/lib/overdue-invoices.functions.d.ts +595 -0
- package/dist-lib/lib/overdue-invoices.functions.d.ts.map +1 -1
- package/dist-lib/lib/price-groups-local.functions.d.ts +137905 -27
- package/dist-lib/lib/price-groups-local.functions.d.ts.map +1 -1
- package/dist-lib/lib/price-groups-local.functions.js +145 -0
- package/dist-lib/lib/price-groups.functions.d.ts +601 -1
- package/dist-lib/lib/price-groups.functions.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-adopt.functions.d.ts +1190 -0
- package/dist-lib/lib/twinfield-adopt.functions.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-ledger.functions.d.ts +595 -0
- package/dist-lib/lib/twinfield-ledger.functions.d.ts.map +1 -1
- package/dist-lib/lib/twinfield-ledger.server.js +21 -21
- package/package.json +1 -1
- package/public/flowselections-assets/Klanten/twinfield-logo.svg +217 -217
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"asset_id": "3e27c03f-e308-40e7-ac35-07bf1161c1e2",
|
|
4
|
+
"project_id": "dfdc14cf-30fe-4e0b-8abc-a3ae973943d8",
|
|
5
|
+
"url": "/__l5e/assets-v1/3e27c03f-e308-40e7-ac35-07bf1161c1e2/floriday-logo.png",
|
|
6
|
+
"r2_key": "a/v1/dfdc14cf-30fe-4e0b-8abc-a3ae973943d8/3e27c03f-e308-40e7-ac35-07bf1161c1e2/floriday-logo.png",
|
|
7
|
+
"original_filename": "floriday-logo.png",
|
|
8
|
+
"size": 6921,
|
|
9
|
+
"content_type": "image/png",
|
|
10
|
+
"created_at": "2026-07-07T13:17:11Z"
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CustomerAssortmentTab.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/CustomerAssortmentTab.tsx"],"names":[],"mappings":"AAsBA,wBAAgB,qBAAqB,CAAC,EACpC,WAAW,EACX,cAAc,GACf,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,+BA8LA"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { useServerFn } from "@tanstack/react-start";
|
|
5
|
+
import { Card, CardContent, Badge, Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@flowselections/core";
|
|
6
|
+
import { Search, ImageOff } from "lucide-react";
|
|
7
|
+
import { listCustomerAssortment } from "../../lib/customer-assortment.functions";
|
|
8
|
+
import { decodeDeep } from "../../lib/decode-html";
|
|
9
|
+
import { friendlyError } from "../../lib/errors";
|
|
10
|
+
import { FloridayPriceImportDialog } from "./FloridayPriceImportDialog";
|
|
11
|
+
function money(value, currency) {
|
|
12
|
+
if (value == null)
|
|
13
|
+
return "—";
|
|
14
|
+
try {
|
|
15
|
+
return new Intl.NumberFormat("nl-NL", { style: "currency", currency }).format(value);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return `${currency} ${value.toFixed(2)}`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function CustomerAssortmentTab({ customerKey, organizationId, }) {
|
|
22
|
+
const list = useServerFn(listCustomerAssortment);
|
|
23
|
+
const [search, setSearch] = useState("");
|
|
24
|
+
const [onlyPriced, setOnlyPriced] = useState(false);
|
|
25
|
+
const { data, isLoading, error } = useQuery({
|
|
26
|
+
queryKey: ["customer-assortment", customerKey, organizationId ?? null],
|
|
27
|
+
queryFn: () => list({ data: { customerKey, organizationId: organizationId ?? null } }).then(decodeDeep),
|
|
28
|
+
enabled: !!customerKey,
|
|
29
|
+
staleTime: 60000,
|
|
30
|
+
});
|
|
31
|
+
const items = data?.items ?? [];
|
|
32
|
+
const filtered = useMemo(() => {
|
|
33
|
+
const q = search.trim().toLowerCase();
|
|
34
|
+
return items.filter((i) => {
|
|
35
|
+
if (onlyPriced && i.price == null)
|
|
36
|
+
return false;
|
|
37
|
+
if (!q)
|
|
38
|
+
return true;
|
|
39
|
+
return (i.name.toLowerCase().includes(q) ||
|
|
40
|
+
(i.supplier_article_code ?? "").toLowerCase().includes(q) ||
|
|
41
|
+
(i.vbn_code ?? "").toLowerCase().includes(q) ||
|
|
42
|
+
(i.botanical_name ?? "").toLowerCase().includes(q));
|
|
43
|
+
});
|
|
44
|
+
}, [items, search, onlyPriced]);
|
|
45
|
+
if (isLoading) {
|
|
46
|
+
return (_jsx(Card, { children: _jsx(CardContent, { className: "py-8 text-sm text-muted-foreground", children: "Assortiment laden\u2026" }) }));
|
|
47
|
+
}
|
|
48
|
+
if (error) {
|
|
49
|
+
return (_jsx(Card, { children: _jsx(CardContent, { className: "py-6 text-sm text-destructive", children: friendlyError(error) }) }));
|
|
50
|
+
}
|
|
51
|
+
const pg = data?.priceGroup ?? null;
|
|
52
|
+
const currency = pg?.currency ?? "EUR";
|
|
53
|
+
if (items.length === 0) {
|
|
54
|
+
return (_jsx(Card, { children: _jsxs(CardContent, { className: "py-10 text-center space-y-1", children: [_jsx("p", { className: "text-sm font-medium", children: "Nog geen assortiment beschikbaar" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Er staan nog geen handelsartikelen uit Floriday klaar voor deze klant." })] }) }));
|
|
55
|
+
}
|
|
56
|
+
return (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground", children: [_jsxs("span", { children: [_jsx("strong", { className: "text-foreground", children: data?.summary.total ?? 0 }), " artikelen"] }), _jsxs("span", { children: [_jsx("strong", { className: "text-foreground", children: data?.summary.priced ?? 0 }), " met prijs"] }), pg ? (_jsxs("span", { children: ["Prijsgroep: ", _jsx("strong", { className: "text-foreground", children: pg.name }), pg.pricing_mode === "calculated" && pg.calc_value
|
|
57
|
+
? ` (${pg.calc_type === "surcharge" ? "toeslag" : "korting"} ${pg.calc_value}${pg.calc_value_type === "percentage" ? "%" : ` ${currency}`})`
|
|
58
|
+
: ""] })) : (_jsx("span", { className: "text-amber-600", children: "Geen prijsgroep ingesteld voor deze klant" })), data?.summary.last_synced_at && (_jsxs("span", { children: ["laatste sync: ", new Date(data.summary.last_synced_at).toLocaleString("nl-NL")] }))] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs("div", { className: "relative flex-1 min-w-[240px] max-w-md", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" }), _jsx(Input, { placeholder: "Zoek op naam, artikelcode of VBN-code\u2026", value: search, onChange: (e) => setSearch(e.target.value), className: "pl-9" })] }), _jsx("button", { type: "button", onClick: () => setOnlyPriced((v) => !v), className: "inline-flex items-center rounded-full border px-3 py-1 text-xs transition-colors " +
|
|
59
|
+
(onlyPriced
|
|
60
|
+
? "bg-primary text-primary-foreground border-primary"
|
|
61
|
+
: "bg-card text-muted-foreground border-border hover:bg-muted"), children: "Alleen met prijs" }), _jsx("div", { className: "ml-auto", children: _jsx(FloridayPriceImportDialog, { customerKey: customerKey, organizationId: organizationId }) })] }), _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-[64px]" }), _jsx(TableHead, { children: "Artikel" }), _jsx(TableHead, { children: "Verpakking" }), _jsx(TableHead, { className: "text-right", children: "Basisprijs" }), _jsx(TableHead, { className: "text-right", children: "Prijs voor klant" })] }) }), _jsx(TableBody, { children: filtered.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "text-center py-6 text-muted-foreground text-sm", children: "Geen resultaten voor deze zoekopdracht." }) })) : filtered.map((i) => {
|
|
62
|
+
const packing = [
|
|
63
|
+
i.pieces_per_package != null ? `${i.pieces_per_package} per fust` : null,
|
|
64
|
+
i.packages_per_layer != null ? `${i.packages_per_layer} per laag` : null,
|
|
65
|
+
i.layers_per_carrier != null ? `${i.layers_per_carrier} lagen` : null,
|
|
66
|
+
].filter(Boolean);
|
|
67
|
+
return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: i.image_url ? (_jsx("img", { src: i.image_url, alt: "", loading: "lazy", className: "h-10 w-10 rounded object-cover bg-muted" })) : (_jsx("div", { className: "h-10 w-10 rounded bg-muted flex items-center justify-center text-muted-foreground", children: _jsx(ImageOff, { className: "h-4 w-4" }) })) }), _jsxs(TableCell, { children: [_jsxs("div", { className: "font-medium flex items-center gap-2", children: [i.name, i.restricted && (_jsx(Badge, { variant: "secondary", title: "Alleen voor deze klant beschikbaar", children: "Exclusief" })), i.floriday_source_label && (_jsx(Badge, { variant: i.floriday_source === "contract" ? "default" : "secondary", children: i.floriday_source_label }))] }), _jsx("div", { className: "text-xs text-muted-foreground", children: [
|
|
68
|
+
i.supplier_article_code,
|
|
69
|
+
i.vbn_code ? `VBN ${i.vbn_code}` : null,
|
|
70
|
+
i.botanical_name,
|
|
71
|
+
i.country_of_origin,
|
|
72
|
+
].filter(Boolean).join(" · ") || "—" })] }), _jsxs(TableCell, { className: "text-sm text-muted-foreground", children: [_jsx("div", { children: i.packaging ?? "—" }), packing.length > 0 && (_jsx("div", { className: "text-xs", children: packing.join(" · ") }))] }), _jsx(TableCell, { className: "text-right whitespace-nowrap tabular-nums text-muted-foreground", children: money(i.base_price, i.currency) }), _jsxs(TableCell, { className: "text-right whitespace-nowrap tabular-nums", children: [_jsx("div", { className: "font-medium", children: money(i.price, i.currency) }), i.base_price_source && (_jsx("div", { className: "text-[10px] text-muted-foreground", children: i.base_price_source }))] })] }, i.key));
|
|
73
|
+
}) })] }) }) })] }));
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FloridayPriceImportDialog.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/FloridayPriceImportDialog.tsx"],"names":[],"mappings":"AAyBA,wBAAgB,yBAAyB,CAAC,EACxC,WAAW,EACX,cAAc,GACf,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,+BA4IA"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { useServerFn } from "@tanstack/react-start";
|
|
5
|
+
import { Badge, Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@flowselections/core";
|
|
6
|
+
import { Download } from "lucide-react";
|
|
7
|
+
import { toast } from "sonner";
|
|
8
|
+
import { applyFloridayPriceImport, previewFloridayPriceImport, } from "../../lib/floriday-price-import.functions";
|
|
9
|
+
import { decodeDeep } from "../../lib/decode-html";
|
|
10
|
+
import { friendlyError } from "../../lib/errors";
|
|
11
|
+
function money(value, currency) {
|
|
12
|
+
if (value == null)
|
|
13
|
+
return "—";
|
|
14
|
+
try {
|
|
15
|
+
return new Intl.NumberFormat("nl-NL", { style: "currency", currency }).format(value);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return `${currency} ${value.toFixed(2)}`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function FloridayPriceImportDialog({ customerKey, organizationId, }) {
|
|
22
|
+
const [open, setOpen] = useState(false);
|
|
23
|
+
const qc = useQueryClient();
|
|
24
|
+
const preview = useServerFn(previewFloridayPriceImport);
|
|
25
|
+
const apply = useServerFn(applyFloridayPriceImport);
|
|
26
|
+
const { data, isLoading, error } = useQuery({
|
|
27
|
+
queryKey: ["floriday-price-import", customerKey, organizationId ?? null],
|
|
28
|
+
queryFn: () => preview({ data: { customerKey, organizationId: organizationId ?? null } }).then(decodeDeep),
|
|
29
|
+
enabled: open,
|
|
30
|
+
});
|
|
31
|
+
const run = useMutation({
|
|
32
|
+
mutationFn: () => apply({ data: { customerKey, organizationId: organizationId ?? null } }),
|
|
33
|
+
onSuccess: (r) => {
|
|
34
|
+
toast.success(r.imported === 0
|
|
35
|
+
? "Alle prijzen stonden al goed — niets gewijzigd."
|
|
36
|
+
: `${r.imported} prijs(zen) overgenomen uit Floriday.`);
|
|
37
|
+
qc.invalidateQueries({ queryKey: ["customer-assortment"] });
|
|
38
|
+
qc.invalidateQueries({ queryKey: ["floriday-price-import"] });
|
|
39
|
+
setOpen(false);
|
|
40
|
+
},
|
|
41
|
+
onError: (e) => toast.error(friendlyError(e)),
|
|
42
|
+
});
|
|
43
|
+
const rows = data?.rows ?? [];
|
|
44
|
+
const currency = data?.priceGroup?.currency ?? "EUR";
|
|
45
|
+
const changes = (data?.summary.new ?? 0) + (data?.summary.update ?? 0);
|
|
46
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setOpen(true), children: [_jsx(Download, { className: "h-4 w-4 mr-2" }), "Prijzen importeren uit Floriday"] }), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-w-3xl", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Prijzen importeren uit Floriday" }) }), isLoading ? (_jsx("p", { className: "py-6 text-sm text-muted-foreground", children: "Prijzen ophalen\u2026" })) : error ? (_jsx("p", { className: "py-6 text-sm text-destructive", children: friendlyError(error) })) : !data?.priceGroup ? (_jsx("p", { className: "py-6 text-sm", children: "Deze klant heeft nog geen prijsgroep. Kies eerst een prijsgroep bij de klant; de ge\u00EFmporteerde prijzen worden daarin opgeslagen." })) : rows.length === 0 ? (_jsx("p", { className: "py-6 text-sm", children: "Er staan nog geen Floriday-prijzen klaar voor deze klant. Zodra de catalogusprijzen uit Floriday zijn binnengehaald, verschijnen ze hier automatisch." })) : (_jsxs("div", { className: "space-y-3", children: [_jsxs("p", { className: "text-sm text-muted-foreground", children: ["Prijsgroep ", _jsx("strong", { className: "text-foreground", children: data.priceGroup.name }), " \u00B7", " ", data.summary.new, " nieuw \u00B7 ", data.summary.update, " bijgewerkt \u00B7", " ", data.summary.unchanged, " ongewijzigd"] }), _jsx("div", { className: "max-h-[45vh] overflow-auto rounded-md border", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Artikel" }), _jsx(TableHead, { children: "Bron" }), _jsx(TableHead, { className: "text-right", children: "Huidig" }), _jsx(TableHead, { className: "text-right", children: "Floriday" }), _jsx(TableHead, { children: "Actie" })] }) }), _jsx(TableBody, { children: rows.map((r) => (_jsxs(TableRow, { children: [_jsxs(TableCell, { children: [_jsx("div", { className: "font-medium", children: r.name }), r.supplier_article_code && (_jsx("div", { className: "text-xs text-muted-foreground", children: r.supplier_article_code }))] }), _jsx(TableCell, { className: "text-xs text-muted-foreground", children: r.source_label }), _jsx(TableCell, { className: "text-right tabular-nums text-muted-foreground", children: money(r.current_price, currency) }), _jsx(TableCell, { className: "text-right tabular-nums font-medium", children: money(r.floriday_price, r.currency) }), _jsx(TableCell, { children: _jsx(Badge, { variant: r.action === "new"
|
|
47
|
+
? "default"
|
|
48
|
+
: r.action === "update"
|
|
49
|
+
? "secondary"
|
|
50
|
+
: "outline", children: r.action === "new"
|
|
51
|
+
? "Nieuw"
|
|
52
|
+
: r.action === "update"
|
|
53
|
+
? "Bijwerken"
|
|
54
|
+
: "Ongewijzigd" }) })] }, r.trade_item_id))) })] }) })] })), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => setOpen(false), children: "Annuleren" }), _jsx(Button, { type: "button", onClick: () => run.mutate(), disabled: run.isPending || !data?.priceGroup || changes === 0, children: run.isPending ? "Bezig…" : `Overnemen (${changes})` })] })] }) })] }));
|
|
55
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GroupedKlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/GroupedKlantDetail.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"GroupedKlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/GroupedKlantDetail.tsx"],"names":[],"mappings":"AAkCA,wBAAgB,kBAAkB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,+BA2TpE"}
|
|
@@ -11,9 +11,10 @@ import { ProfileCard, KpiStrip, BadgeList, EmptyState, adaptGroupedToProfile, }
|
|
|
11
11
|
import { countryNameToFlag } from "../../lib/countries";
|
|
12
12
|
import { DiscountsTab } from "../../components/klanten/DiscountsTab";
|
|
13
13
|
import { PriceGroupBadges } from "../../components/klanten/PriceGroupBadges";
|
|
14
|
+
import { CustomerPriceGroupSelect } from "../../components/klanten/pricegroups/CustomerPriceGroupSelect";
|
|
14
15
|
import { listPriceGroupsForOrganizations } from "../../lib/price-groups.functions";
|
|
15
16
|
import { FloridayDeeplinkButton } from "../../components/klanten/FloridayDeeplinkButton";
|
|
16
|
-
import {
|
|
17
|
+
import { CustomerAssortmentTab } from "../../components/klanten/CustomerAssortmentTab";
|
|
17
18
|
import { decodeDeep } from "../../lib/decode-html";
|
|
18
19
|
import { DeliveryLocationsTab } from "../../components/klanten/DeliveryLocationsTab";
|
|
19
20
|
import { KlantBewerkenDialog } from "../../components/klanten/KlantBewerkenDialog";
|
|
@@ -90,12 +91,12 @@ export function GroupedKlantDetail({ groupKey }) {
|
|
|
90
91
|
provider: providerMap.get(s.provider)?.label ?? s.provider,
|
|
91
92
|
}))
|
|
92
93
|
.sort((a, b) => +new Date(b.created_at) - +new Date(a.created_at));
|
|
93
|
-
return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx(BackLink, {}), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [localCustomerId ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] })) : twinfieldCode ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] })) : null, _jsx(FloridayDeeplinkButton, { organizationId: floridayBuyerId })] }), _jsx(KlantBewerkenDialog, { open: editOpen, onOpenChange: setEditOpen, customerId: localCustomerId ?? null, twinfieldCode: localCustomerId ? null : twinfieldCode, onSaved: () => qc.invalidateQueries() }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[340px_1fr]", children: [_jsx(ProfileCard, { profile: profile, notes: noteData?.note ?? null, onSaveNotes: (n) => noteMut.mutate(n), isSaving: noteMut.isPending || noteLoading }), _jsxs("div", { className: "space-y-4 min-w-0", children: [priceGroups.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "
|
|
94
|
+
return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx(BackLink, {}), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [localCustomerId ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] })) : twinfieldCode ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] })) : null, _jsx(FloridayDeeplinkButton, { organizationId: floridayBuyerId })] }), _jsx(KlantBewerkenDialog, { open: editOpen, onOpenChange: setEditOpen, customerId: localCustomerId ?? null, twinfieldCode: localCustomerId ? null : twinfieldCode, onSaved: () => qc.invalidateQueries() }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[340px_1fr]", children: [_jsx(ProfileCard, { profile: profile, notes: noteData?.note ?? null, onSaveNotes: (n) => noteMut.mutate(n), isSaving: noteMut.isPending || noteLoading }), _jsxs("div", { className: "space-y-4 min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-4", children: [_jsx(CustomerPriceGroupSelect, { customerKey: groupKey }), priceGroups.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Floriday-prijscategorie:" }), _jsx(PriceGroupBadges, { groups: priceGroups, max: priceGroups.length })] }))] }), _jsx(KpiStrip, { counts: {
|
|
94
95
|
contacts: contacts.length,
|
|
95
96
|
locations: locations.length,
|
|
96
97
|
assortment: 0,
|
|
97
98
|
orders: 0,
|
|
98
|
-
}, onSelect: setTab }), _jsxs(Tabs, { value: tab, onValueChange: setTab, children: [_jsxs(TabsList, { className: "flex-wrap", children: [_jsx(TabsTrigger, { value: "overview", children: "Overzicht" }), _jsxs(TabsTrigger, { value: "contacts", children: ["Contacten (", contacts.length, ")"] }), _jsxs(TabsTrigger, { value: "locations", children: ["Locaties (", locations.length, ")"] }), _jsx(TabsTrigger, { value: "assortment", children: "Assortiment (0)" }), _jsx(TabsTrigger, { value: "orders", children: "Bestellingen (0)" }), _jsx(TabsTrigger, { value: "trade", children: "Handel" }), floridayBuyerId && (_jsx(TabsTrigger, { value: "discounts", children: "Kortingen & prijzen" })), _jsx(TabsTrigger, { value: "activity", children: "Activiteit" }), g.is_merged && sources.length > 1 && (_jsxs(TabsTrigger, { value: "sources", children: ["Bronnen (", sources.length, ")"] }))] }), _jsxs(TabsContent, { value: "overview", className: "space-y-4", children: [(segments.length || tradeForms.length || markets.length || productGroups.length) ? (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 grid gap-4 md:grid-cols-2", children: [_jsx(BadgeList, { label: "Segmenten", items: segments }), _jsx(BadgeList, { label: "Handelsvorm", items: tradeForms }), _jsx(BadgeList, { label: "Afzetmarkten", items: markets }), _jsx(BadgeList, { label: "Productgroepen", items: productGroups })] }) })) : null, paymentMethods.length > 0 && (_jsx(Card, { children: _jsx(CardContent, { className: "pt-6", children: _jsx(BadgeList, { label: "Betaalmethoden", items: paymentMethods }) }) })), !segments.length && !tradeForms.length && !markets.length && !productGroups.length && !paymentMethods.length && (_jsx(EmptyState, { message: "Geen extra profielinformatie beschikbaar voor deze klant." }))] }), _jsx(TabsContent, { value: "contacts", children: contacts.length === 0 ? (_jsx(EmptyState, { message: "Geen contactpersonen beschikbaar via de koppeling." })) : (_jsx("div", { className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3", children: contacts.map((p, i) => (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 space-y-2", children: [_jsx("div", { className: "font-semibold", children: p.full_name }), p.job_title && _jsx("div", { className: "text-sm text-muted-foreground", children: p.job_title }), p.phone && (_jsxs("a", { href: `tel:${p.phone}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Phone, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.phone] })), p.email && (_jsxs("a", { href: `mailto:${p.email}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Mail, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.email] })), _jsx(Badge, { variant: "secondary", children: p.source_label })] }) }, i))) })) }), _jsx(TabsContent, { value: "locations", children: _jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "text-sm font-medium flex items-center gap-2", children: [_jsx(MapPin, { className: "h-4 w-4 text-muted-foreground" }), " Afleverlocaties (logistiek)"] }), _jsx(DeliveryLocationsTab, { groupKey: groupKey, customerId: localCustomerId })] }), locations.length > 0 && (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "text-sm font-medium flex items-center gap-2", children: [_jsx(MapPin, { className: "h-4 w-4 text-muted-foreground" }), " Boekhoudlocaties uit de koppeling (read-only)"] }), _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Adres" }), _jsx(TableHead, { children: "GLN" }), _jsx(TableHead, { children: "Postcode" }), _jsx(TableHead, { children: "Bron" })] }) }), _jsx(TableBody, { children: locations.map((l, i) => (_jsxs(TableRow, { children: [_jsxs(TableCell, { children: [_jsx("div", { className: "font-medium", children: l.name || l.address_line || "—" }), l.name && l.address_line && (_jsxs("div", { className: "text-xs text-muted-foreground", children: [l.address_line, ", ", l.city ?? ""] }))] }), _jsx(TableCell, { className: "font-mono text-sm", children: l.gln || "—" }), _jsxs(TableCell, { className: "text-sm", children: [l.postal_code ? _jsx("span", { className: "font-mono mr-1", children: l.postal_code }) : null, l.country ? (_jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "text-base", "aria-hidden": "true", children: countryNameToFlag(l.country) ?? "🌐" }), _jsx("span", { children: l.country })] })) : null, !l.postal_code && !l.country ? "—" : null] }), _jsx(TableCell, { children: _jsx(Badge, { variant: "secondary", children: l.source_label }) })] }, i))) })] }) }) })] }))] }) }), _jsx(TabsContent, { value: "assortment", children:
|
|
99
|
+
}, onSelect: setTab }), _jsxs(Tabs, { value: tab, onValueChange: setTab, children: [_jsxs(TabsList, { className: "flex-wrap", children: [_jsx(TabsTrigger, { value: "overview", children: "Overzicht" }), _jsxs(TabsTrigger, { value: "contacts", children: ["Contacten (", contacts.length, ")"] }), _jsxs(TabsTrigger, { value: "locations", children: ["Locaties (", locations.length, ")"] }), _jsx(TabsTrigger, { value: "assortment", children: "Assortiment (0)" }), _jsx(TabsTrigger, { value: "orders", children: "Bestellingen (0)" }), _jsx(TabsTrigger, { value: "trade", children: "Handel" }), floridayBuyerId && (_jsx(TabsTrigger, { value: "discounts", children: "Kortingen & prijzen" })), _jsx(TabsTrigger, { value: "activity", children: "Activiteit" }), g.is_merged && sources.length > 1 && (_jsxs(TabsTrigger, { value: "sources", children: ["Bronnen (", sources.length, ")"] }))] }), _jsxs(TabsContent, { value: "overview", className: "space-y-4", children: [(segments.length || tradeForms.length || markets.length || productGroups.length) ? (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 grid gap-4 md:grid-cols-2", children: [_jsx(BadgeList, { label: "Segmenten", items: segments }), _jsx(BadgeList, { label: "Handelsvorm", items: tradeForms }), _jsx(BadgeList, { label: "Afzetmarkten", items: markets }), _jsx(BadgeList, { label: "Productgroepen", items: productGroups })] }) })) : null, paymentMethods.length > 0 && (_jsx(Card, { children: _jsx(CardContent, { className: "pt-6", children: _jsx(BadgeList, { label: "Betaalmethoden", items: paymentMethods }) }) })), !segments.length && !tradeForms.length && !markets.length && !productGroups.length && !paymentMethods.length && (_jsx(EmptyState, { message: "Geen extra profielinformatie beschikbaar voor deze klant." }))] }), _jsx(TabsContent, { value: "contacts", children: contacts.length === 0 ? (_jsx(EmptyState, { message: "Geen contactpersonen beschikbaar via de koppeling." })) : (_jsx("div", { className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3", children: contacts.map((p, i) => (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 space-y-2", children: [_jsx("div", { className: "font-semibold", children: p.full_name }), p.job_title && _jsx("div", { className: "text-sm text-muted-foreground", children: p.job_title }), p.phone && (_jsxs("a", { href: `tel:${p.phone}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Phone, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.phone] })), p.email && (_jsxs("a", { href: `mailto:${p.email}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Mail, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.email] })), _jsx(Badge, { variant: "secondary", children: p.source_label })] }) }, i))) })) }), _jsx(TabsContent, { value: "locations", children: _jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "text-sm font-medium flex items-center gap-2", children: [_jsx(MapPin, { className: "h-4 w-4 text-muted-foreground" }), " Afleverlocaties (logistiek)"] }), _jsx(DeliveryLocationsTab, { groupKey: groupKey, customerId: localCustomerId })] }), locations.length > 0 && (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "text-sm font-medium flex items-center gap-2", children: [_jsx(MapPin, { className: "h-4 w-4 text-muted-foreground" }), " Boekhoudlocaties uit de koppeling (read-only)"] }), _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Adres" }), _jsx(TableHead, { children: "GLN" }), _jsx(TableHead, { children: "Postcode" }), _jsx(TableHead, { children: "Bron" })] }) }), _jsx(TableBody, { children: locations.map((l, i) => (_jsxs(TableRow, { children: [_jsxs(TableCell, { children: [_jsx("div", { className: "font-medium", children: l.name || l.address_line || "—" }), l.name && l.address_line && (_jsxs("div", { className: "text-xs text-muted-foreground", children: [l.address_line, ", ", l.city ?? ""] }))] }), _jsx(TableCell, { className: "font-mono text-sm", children: l.gln || "—" }), _jsxs(TableCell, { className: "text-sm", children: [l.postal_code ? _jsx("span", { className: "font-mono mr-1", children: l.postal_code }) : null, l.country ? (_jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx("span", { className: "text-base", "aria-hidden": "true", children: countryNameToFlag(l.country) ?? "🌐" }), _jsx("span", { children: l.country })] })) : null, !l.postal_code && !l.country ? "—" : null] }), _jsx(TableCell, { children: _jsx(Badge, { variant: "secondary", children: l.source_label }) })] }, i))) })] }) }) })] }))] }) }), _jsx(TabsContent, { value: "assortment", children: _jsx(CustomerAssortmentTab, { customerKey: groupKey, organizationId: floridayBuyerId }) }), _jsx(TabsContent, { value: "orders", children: _jsx(EmptyState, { message: "Bestellingen zijn beschikbaar zodra de koppeling dit levert." }) }), _jsx(TabsContent, { value: "trade", children: _jsx(EmptyState, { message: "Handelsinstellingen zijn niet beschikbaar via deze koppeling." }) }), floridayBuyerId && (_jsx(TabsContent, { value: "discounts", children: _jsx(DiscountsTab, { buyerOrganizationId: floridayBuyerId }) })), _jsx(TabsContent, { value: "activity", children: _jsx(Card, { children: _jsx(CardContent, { className: "pt-6 space-y-2", children: activity.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen activiteit voor deze klant." })) : (activity.map((a) => (_jsxs("div", { className: "flex items-center justify-between text-sm border-l-2 border-primary pl-3 py-1", children: [_jsxs("span", { children: ["Bijgewerkt via ", _jsx("strong", { children: a.provider })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: new Date(a.created_at).toLocaleString("nl-NL") })] }, a.id)))) }) }) }), g.is_merged && sources.length > 1 && (_jsx(TabsContent, { value: "sources", children: _jsx(CompareCard, { sources: sources, providerMap: providerMap }) }))] })] })] })] }));
|
|
99
100
|
}
|
|
100
101
|
function collectContacts(sources, providerMap) {
|
|
101
102
|
const out = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantDetail.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"KlantDetail.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantDetail.tsx"],"names":[],"mappings":"AAuCA,wBAAgB,WAAW,CAAC,EAAE,EAAE,EAAE,EAAE;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,+BAsTjD"}
|
|
@@ -12,8 +12,11 @@ import { BackLink } from "../../components/klanten/BackLink";
|
|
|
12
12
|
import { CustomerAnalyticsTab } from "../../components/klanten/CustomerAnalyticsTab";
|
|
13
13
|
import { LocalDiscountsTab } from "../../components/klanten/LocalDiscountsTab";
|
|
14
14
|
import { PriceGroupBadges } from "../../components/klanten/PriceGroupBadges";
|
|
15
|
+
import { CustomerPriceGroupSelect } from "../../components/klanten/pricegroups/CustomerPriceGroupSelect";
|
|
16
|
+
import { resolveCustomerKeyForLocalCustomer } from "../../lib/price-groups-local.functions";
|
|
15
17
|
import { FloridayDeeplinkButton } from "../../components/klanten/FloridayDeeplinkButton";
|
|
16
18
|
import { FloridayAssortmentTab } from "../../components/klanten/FloridayAssortmentTab";
|
|
19
|
+
import { CustomerAssortmentTab } from "../../components/klanten/CustomerAssortmentTab";
|
|
17
20
|
import { SyncStatusChip } from "../../components/klanten/SyncStatusChip";
|
|
18
21
|
import { decodeDeep } from "../../lib/decode-html";
|
|
19
22
|
import { getMissingTwinfieldFields } from "../../lib/twinfield-required-fields";
|
|
@@ -34,6 +37,11 @@ export function KlantDetail({ id }) {
|
|
|
34
37
|
queryKey: ["customer", id],
|
|
35
38
|
queryFn: () => get({ data: { id } }).then(decodeDeep),
|
|
36
39
|
});
|
|
40
|
+
const resolveKey = useServerFn(resolveCustomerKeyForLocalCustomer);
|
|
41
|
+
const { data: keyData } = useQuery({
|
|
42
|
+
queryKey: ["customer-group-key", id],
|
|
43
|
+
queryFn: () => resolveKey({ data: { customerId: id } }),
|
|
44
|
+
});
|
|
37
45
|
const { data: noteData, isLoading: noteLoading } = useQuery({
|
|
38
46
|
queryKey: ["customer-note", "customer_id", id],
|
|
39
47
|
queryFn: () => getNote({ data: { customer_id: id } }),
|
|
@@ -71,10 +79,10 @@ export function KlantDetail({ id }) {
|
|
|
71
79
|
const c = data.customer;
|
|
72
80
|
const missingTwinfield = getMissingTwinfieldFields(c);
|
|
73
81
|
const priceGroups = data.price_groups ?? c.price_groups ?? [];
|
|
74
|
-
return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx(BackLink, {}), missingTwinfield.length > 0 && (_jsxs("div", { className: "rounded-xl border border-warning/40 bg-warning/10 px-4 py-3 text-sm", children: [_jsx("p", { className: "font-medium", children: "Onvolledig voor Twinfield" }), _jsxs("p", { className: "text-muted-foreground mt-0.5", children: ["Deze klant kan pas naar Twinfield worden gestuurd zodra de volgende gegevens zijn aangevuld: ", missingTwinfield.map((f) => f.label).join(", "), "."] })] })), _jsx("div", { className: "flex items-center justify-end", children: _jsxs("div", { className: "flex gap-2", children: [_jsx(SyncStatusChip, { customerId: id }), _jsx(FloridayDeeplinkButton, { organizationId: c.floriday_organization_id }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => toggleActive.mutate(!c.is_active), children: [_jsx(Power, { className: "h-4 w-4 mr-2" }), c.is_active ? "Op inactief zetten" : "Activeren"] }), _jsxs(AlertDialog, { children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", children: [_jsx(Trash2, { className: "h-4 w-4 mr-2" }), " Verwijderen"] }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Klant verwijderen?" }), _jsxs(AlertDialogDescription, { children: ["Weet je zeker dat je ", _jsx("strong", { children: c.company_name }), " wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt."] })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: () => delMut.mutate(), children: "Verwijderen" })] })] })] })] }) }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[340px_1fr]", children: [_jsx(ProfileCard, { profile: adaptLocalToProfile(c, data.contacts), notes: noteData?.note ?? null, onSaveNotes: (n) => noteMut.mutate(n), isSaving: noteMut.isPending || noteLoading }), _jsxs("div", { className: "space-y-4 min-w-0", children: [priceGroups.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "
|
|
82
|
+
return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx(BackLink, {}), missingTwinfield.length > 0 && (_jsxs("div", { className: "rounded-xl border border-warning/40 bg-warning/10 px-4 py-3 text-sm", children: [_jsx("p", { className: "font-medium", children: "Onvolledig voor Twinfield" }), _jsxs("p", { className: "text-muted-foreground mt-0.5", children: ["Deze klant kan pas naar Twinfield worden gestuurd zodra de volgende gegevens zijn aangevuld: ", missingTwinfield.map((f) => f.label).join(", "), "."] })] })), _jsx("div", { className: "flex items-center justify-end", children: _jsxs("div", { className: "flex gap-2", children: [_jsx(SyncStatusChip, { customerId: id }), _jsx(FloridayDeeplinkButton, { organizationId: c.floriday_organization_id }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "h-4 w-4 mr-2" }), " Bewerken"] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => toggleActive.mutate(!c.is_active), children: [_jsx(Power, { className: "h-4 w-4 mr-2" }), c.is_active ? "Op inactief zetten" : "Activeren"] }), _jsxs(AlertDialog, { children: [_jsx(AlertDialogTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", children: [_jsx(Trash2, { className: "h-4 w-4 mr-2" }), " Verwijderen"] }) }), _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Klant verwijderen?" }), _jsxs(AlertDialogDescription, { children: ["Weet je zeker dat je ", _jsx("strong", { children: c.company_name }), " wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt."] })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: () => delMut.mutate(), children: "Verwijderen" })] })] })] })] }) }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[340px_1fr]", children: [_jsx(ProfileCard, { profile: adaptLocalToProfile(c, data.contacts), notes: noteData?.note ?? null, onSaveNotes: (n) => noteMut.mutate(n), isSaving: noteMut.isPending || noteLoading }), _jsxs("div", { className: "space-y-4 min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-4", children: [keyData?.customerKey ? (_jsx(CustomerPriceGroupSelect, { customerKey: keyData.customerKey })) : null, priceGroups.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Floriday-prijscategorie:" }), _jsx(PriceGroupBadges, { groups: priceGroups, max: priceGroups.length })] }))] }), _jsx(KpiStrip, { counts: {
|
|
75
83
|
contacts: data.contacts.length,
|
|
76
84
|
locations: data.delivery_locations.length,
|
|
77
85
|
assortment: data.assortment.length,
|
|
78
86
|
orders: data.orders.length,
|
|
79
|
-
}, onSelect: setTab }), _jsxs(Tabs, { value: tab, onValueChange: setTab, children: [_jsxs(TabsList, { className: "flex flex-wrap h-auto gap-1 mb-4", children: [_jsx(TabsTrigger, { value: "overview", children: "Overzicht" }), _jsx(TabsTrigger, { value: "dashboard", children: "Analyses" }), _jsxs(TabsTrigger, { value: "contacts", children: ["Contacten (", data.contacts.length, ")"] }), _jsxs(TabsTrigger, { value: "locations", children: ["Locaties (", data.delivery_locations.length, ")"] }), _jsxs(TabsTrigger, { value: "assortment", children: ["Assortiment (", data.assortment.length, ")"] }), c.floriday_organization_id && (_jsx(TabsTrigger, { value: "floriday-assortment", children: "Floriday-aanbod" })), _jsxs(TabsTrigger, { value: "orders", children: ["Bestellingen (", data.orders.length, ")"] }), _jsx(TabsTrigger, { value: "trade", children: "Handel" }), _jsx(TabsTrigger, { value: "discounts", children: "Kortingen & prijzen" }), _jsx(TabsTrigger, { value: "activity", children: "Activiteit" })] }), _jsxs(TabsContent, { value: "overview", className: "space-y-4", children: [c.bio && (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Bio" }) }), _jsx(CardContent, { className: "text-sm whitespace-pre-wrap", children: c.bio })] })), (c.segments?.length || c.trade_forms?.length || c.markets?.length || c.product_groups?.length) ? (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Profiel" }) }), _jsxs(CardContent, { className: "grid gap-4 md:grid-cols-2", children: [_jsx(BadgeList, { label: "Segmenten", items: c.segments }), _jsx(BadgeList, { label: "Handelsvorm", items: c.trade_forms }), _jsx(BadgeList, { label: "Afzetmarkten", items: c.markets }), _jsx(BadgeList, { label: "Productgroepen", items: c.product_groups })] })] })) : null, (c.payment_methods?.length ?? 0) > 0 && (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Financi\u00EBle afhandeling" }) }), _jsx(CardContent, { children: _jsx(BadgeList, { label: "Betaalmethoden", items: c.payment_methods }) })] })), !c.bio && !(c.segments?.length || c.trade_forms?.length || c.markets?.length || c.product_groups?.length) && !(c.payment_methods?.length) && (_jsx(EmptyState, { message: "Geen extra profielinformatie beschikbaar voor deze klant." }))] }), _jsx(TabsContent, { value: "contacts", children: data.contacts.length === 0 ? (_jsx(EmptyState, { message: "Geen contactpersonen gesynchroniseerd." })) : (_jsx("div", { className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3", children: data.contacts.map((p) => (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 space-y-2", children: [_jsx("div", { className: "font-semibold", children: p.full_name }), p.job_title && _jsx("div", { className: "text-sm text-muted-foreground", children: p.job_title }), p.phone && (_jsxs("a", { href: `tel:${p.phone}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Phone, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.phone] })), p.email && (_jsxs("a", { href: `mailto:${p.email}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Mail, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.email] })), p.is_active ? _jsx(Badge, { children: "Actief" }) : _jsx(Badge, { variant: "secondary", children: "Inactief" })] }) }, p.id))) })) }), _jsx(TabsContent, { value: "trade", children: data.trade_settings.length === 0 ? (_jsx(EmptyState, { message: "Handelsinstellingen zijn (nog) niet beschikbaar via de Suppliers API voor deze klant." })) : (_jsx(Card, { children: _jsx(CardContent, { className: "pt-6 space-y-2", children: data.trade_settings.map((s) => (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Badge, { variant: s.is_active ? "default" : "secondary", children: s.is_active ? "Actief" : "Niet actief" }), _jsx("span", { className: "text-sm", children: s.setting_label })] }, s.id))) }) })) }), _jsx(TabsContent, { value: "locations", children: _jsx(DeliveryLocationsTab, { customerId: id }) }), _jsx(TabsContent, { value: "dashboard", children: _jsx(CustomerAnalyticsTab, { customerId: id }) }), _jsx(TabsContent, { value: "assortment", children:
|
|
87
|
+
}, onSelect: setTab }), _jsxs(Tabs, { value: tab, onValueChange: setTab, children: [_jsxs(TabsList, { className: "flex flex-wrap h-auto gap-1 mb-4", children: [_jsx(TabsTrigger, { value: "overview", children: "Overzicht" }), _jsx(TabsTrigger, { value: "dashboard", children: "Analyses" }), _jsxs(TabsTrigger, { value: "contacts", children: ["Contacten (", data.contacts.length, ")"] }), _jsxs(TabsTrigger, { value: "locations", children: ["Locaties (", data.delivery_locations.length, ")"] }), _jsxs(TabsTrigger, { value: "assortment", children: ["Assortiment (", data.assortment.length, ")"] }), c.floriday_organization_id && (_jsx(TabsTrigger, { value: "floriday-assortment", children: "Floriday-aanbod" })), _jsxs(TabsTrigger, { value: "orders", children: ["Bestellingen (", data.orders.length, ")"] }), _jsx(TabsTrigger, { value: "trade", children: "Handel" }), _jsx(TabsTrigger, { value: "discounts", children: "Kortingen & prijzen" }), _jsx(TabsTrigger, { value: "activity", children: "Activiteit" })] }), _jsxs(TabsContent, { value: "overview", className: "space-y-4", children: [c.bio && (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Bio" }) }), _jsx(CardContent, { className: "text-sm whitespace-pre-wrap", children: c.bio })] })), (c.segments?.length || c.trade_forms?.length || c.markets?.length || c.product_groups?.length) ? (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Profiel" }) }), _jsxs(CardContent, { className: "grid gap-4 md:grid-cols-2", children: [_jsx(BadgeList, { label: "Segmenten", items: c.segments }), _jsx(BadgeList, { label: "Handelsvorm", items: c.trade_forms }), _jsx(BadgeList, { label: "Afzetmarkten", items: c.markets }), _jsx(BadgeList, { label: "Productgroepen", items: c.product_groups })] })] })) : null, (c.payment_methods?.length ?? 0) > 0 && (_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Financi\u00EBle afhandeling" }) }), _jsx(CardContent, { children: _jsx(BadgeList, { label: "Betaalmethoden", items: c.payment_methods }) })] })), !c.bio && !(c.segments?.length || c.trade_forms?.length || c.markets?.length || c.product_groups?.length) && !(c.payment_methods?.length) && (_jsx(EmptyState, { message: "Geen extra profielinformatie beschikbaar voor deze klant." }))] }), _jsx(TabsContent, { value: "contacts", children: data.contacts.length === 0 ? (_jsx(EmptyState, { message: "Geen contactpersonen gesynchroniseerd." })) : (_jsx("div", { className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3", children: data.contacts.map((p) => (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6 space-y-2", children: [_jsx("div", { className: "font-semibold", children: p.full_name }), p.job_title && _jsx("div", { className: "text-sm text-muted-foreground", children: p.job_title }), p.phone && (_jsxs("a", { href: `tel:${p.phone}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Phone, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.phone] })), p.email && (_jsxs("a", { href: `mailto:${p.email}`, className: "text-sm flex items-center gap-2 hover:underline", children: [_jsx(Mail, { className: "h-3.5 w-3.5 text-muted-foreground" }), " ", p.email] })), p.is_active ? _jsx(Badge, { children: "Actief" }) : _jsx(Badge, { variant: "secondary", children: "Inactief" })] }) }, p.id))) })) }), _jsx(TabsContent, { value: "trade", children: data.trade_settings.length === 0 ? (_jsx(EmptyState, { message: "Handelsinstellingen zijn (nog) niet beschikbaar via de Suppliers API voor deze klant." })) : (_jsx(Card, { children: _jsx(CardContent, { className: "pt-6 space-y-2", children: data.trade_settings.map((s) => (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Badge, { variant: s.is_active ? "default" : "secondary", children: s.is_active ? "Actief" : "Niet actief" }), _jsx("span", { className: "text-sm", children: s.setting_label })] }, s.id))) }) })) }), _jsx(TabsContent, { value: "locations", children: _jsx(DeliveryLocationsTab, { customerId: id }) }), _jsx(TabsContent, { value: "dashboard", children: _jsx(CustomerAnalyticsTab, { customerId: id }) }), _jsx(TabsContent, { value: "assortment", children: keyData?.customerKey ? (_jsx(CustomerAssortmentTab, { customerKey: keyData.customerKey, organizationId: c.floriday_organization_id ?? null })) : (_jsx(EmptyState, { message: "Assortiment is beschikbaar zodra deze klant gekoppeld is." })) }), c.floriday_organization_id && (_jsx(TabsContent, { value: "floriday-assortment", children: _jsx(FloridayAssortmentTab, { organizationId: c.floriday_organization_id }) })), _jsx(TabsContent, { value: "orders", children: _jsx(Card, { children: _jsx(CardContent, { className: "p-0", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: "Ordernummer" }), _jsx(TableHead, { children: "Datum" }), _jsx(TableHead, { children: "Status" }), _jsx(TableHead, { children: "Totaal" })] }) }), _jsx(TableBody, { children: data.orders.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 4, className: "text-center py-6 text-muted-foreground", children: "Geen bestellingen" }) })) : data.orders.map((o) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: o.order_number }), _jsx(TableCell, { children: o.order_date }), _jsx(TableCell, { children: _jsx(Badge, { children: o.status }) }), _jsx(TableCell, { children: o.total })] }, o.id))) })] }) }) }) }), _jsx(TabsContent, { value: "activity", children: _jsx(Card, { children: _jsx(CardContent, { className: "pt-6 space-y-2", children: data.audit.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen activiteit voor deze klant." })) : (data.audit.map((a) => _jsx(ActivityItem, { entry: a }, a.id))) }) }) }), _jsx(TabsContent, { value: "discounts", children: _jsx(LocalDiscountsTab, { customerId: id }) })] })] })] }), _jsx(KlantBewerkenDialog, { open: editOpen, onOpenChange: setEditOpen, customerId: id, onSaved: () => qc.invalidateQueries({ queryKey: ["customer", id] }) })] }));
|
|
80
88
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"KlantenOverzicht.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenOverzicht.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"KlantenOverzicht.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/KlantenOverzicht.tsx"],"names":[],"mappings":"AA0DA,wBAAgB,gBAAgB,gCAgyB/B"}
|
|
@@ -13,6 +13,7 @@ import { listOverdueBalances } from "../../lib/overdue-invoices.functions";
|
|
|
13
13
|
import { listRevenueActivity } from "../../lib/customer-revenue.functions";
|
|
14
14
|
import { supabase } from "../../integrations/supabase/client";
|
|
15
15
|
import { decodeDeep } from "../../lib/decode-html";
|
|
16
|
+
import { friendlyError, isBackendUnavailable, retryOnTransient, retryBackoff } from "../../lib/errors";
|
|
16
17
|
import { CustomerAvatar } from "../CustomerAvatar";
|
|
17
18
|
import { PriceGroupBadges } from "./PriceGroupBadges";
|
|
18
19
|
import { KlantBewerkenDialog } from "./KlantBewerkenDialog";
|
|
@@ -65,29 +66,40 @@ export function KlantenOverzicht() {
|
|
|
65
66
|
queryKey: ["customer-classes"],
|
|
66
67
|
queryFn: () => classesFn({ data: undefined }).then(decodeDeep),
|
|
67
68
|
staleTime: 5 * 60000,
|
|
69
|
+
retry: retryOnTransient,
|
|
70
|
+
retryDelay: retryBackoff,
|
|
68
71
|
});
|
|
69
|
-
const { data: localData, isLoading: loadingLocal } = useQuery({
|
|
72
|
+
const { data: localData, isLoading: loadingLocal, error: localError } = useQuery({
|
|
70
73
|
queryKey: ["customers-local"],
|
|
71
74
|
queryFn: () => list({
|
|
72
75
|
data: { search: "", customer_class: "", country: "", is_active: "all", source: "local" },
|
|
73
76
|
}).then(decodeDeep),
|
|
74
77
|
staleTime: 60000,
|
|
78
|
+
retry: retryOnTransient,
|
|
79
|
+
retryDelay: retryBackoff,
|
|
75
80
|
});
|
|
76
|
-
const { data: externalData, isLoading: loadingExternal } = useQuery({
|
|
81
|
+
const { data: externalData, isLoading: loadingExternal, error: externalError } = useQuery({
|
|
77
82
|
queryKey: ["customers-external"],
|
|
78
83
|
queryFn: () => listExternal({ data: { status: "all" } }).then(decodeDeep),
|
|
79
84
|
staleTime: 60000,
|
|
85
|
+
retry: retryOnTransient,
|
|
86
|
+
retryDelay: retryBackoff,
|
|
80
87
|
});
|
|
81
88
|
const { data: overdueData } = useQuery({
|
|
82
89
|
queryKey: ["overdue-balances"],
|
|
83
90
|
queryFn: () => overdueFn().then(decodeDeep),
|
|
84
91
|
staleTime: 60000,
|
|
92
|
+
retry: retryOnTransient,
|
|
93
|
+
retryDelay: retryBackoff,
|
|
85
94
|
});
|
|
86
95
|
const { data: revenueData, isLoading: loadingRevenue } = useQuery({
|
|
87
96
|
queryKey: ["customer-revenue-12m"],
|
|
88
97
|
queryFn: () => revenueFn().then(decodeDeep),
|
|
89
98
|
staleTime: 60000,
|
|
99
|
+
retry: retryOnTransient,
|
|
100
|
+
retryDelay: retryBackoff,
|
|
90
101
|
});
|
|
102
|
+
const loadError = (localError ?? externalError);
|
|
91
103
|
// Realtime: refetch de klantenlijst zodra de Twinfield/Floriday cache verandert.
|
|
92
104
|
// De DB-trigger heeft de materialized view al ververst, dus een simpele
|
|
93
105
|
// invalidatie levert direct verse data. Debounced om bulk-syncs op te vangen.
|
|
@@ -365,7 +377,16 @@ export function KlantenOverzicht() {
|
|
|
365
377
|
});
|
|
366
378
|
const toggleSort = (col) => setSort((s) => (s.col === col ? { col, dir: s.dir === "asc" ? "desc" : "asc" } : { col, dir: "asc" }));
|
|
367
379
|
const lastUpdated = externalData?.last_fetched_at ?? null;
|
|
368
|
-
return (_jsx(TooltipPrimitive.Provider, { delayDuration: 200, children: _jsxs("main", { className: "p-6 space-y-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Klanten" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer en doorzoek al je klanten." })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => goTo({ to: "/klanten/import" }), children: [_jsx(Upload, { className: "h-4 w-4 mr-2" }), " Importeren"] }), _jsxs(Button, { onClick: () => goTo({ to: "/klanten/nieuw" }), children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Nieuwe klant"] })] })] }), _jsxs("div", { className: "
|
|
380
|
+
return (_jsx(TooltipPrimitive.Provider, { delayDuration: 200, children: _jsxs("main", { className: "p-6 space-y-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Klanten" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Beheer en doorzoek al je klanten." })] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => goTo({ to: "/klanten/import" }), children: [_jsx(Upload, { className: "h-4 w-4 mr-2" }), " Importeren"] }), _jsxs(Button, { onClick: () => goTo({ to: "/klanten/nieuw" }), children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), " Nieuwe klant"] })] })] }), loadError && (_jsxs("div", { className: "flex flex-wrap items-center gap-3 rounded-lg border border-destructive/30 bg-destructive/5 px-4 py-3", children: [_jsx(AlertCircle, { className: "h-5 w-5 text-destructive shrink-0" }), _jsxs("div", { className: "text-sm", children: [_jsx("div", { className: "font-medium", children: isBackendUnavailable(loadError)
|
|
381
|
+
? "De database is tijdelijk niet bereikbaar"
|
|
382
|
+
: "De klanten konden niet geladen worden" }), _jsx("div", { className: "text-muted-foreground", children: isBackendUnavailable(loadError)
|
|
383
|
+
? "Je bent niet uitgelogd. Probeer het over een paar minuten opnieuw."
|
|
384
|
+
: friendlyError(loadError) })] }), _jsx(Button, { size: "sm", variant: "outline", className: "ml-auto", onClick: () => {
|
|
385
|
+
queryClient.invalidateQueries({ queryKey: ["customers-local"] });
|
|
386
|
+
queryClient.invalidateQueries({ queryKey: ["customers-external"] });
|
|
387
|
+
queryClient.invalidateQueries({ queryKey: ["overdue-balances"] });
|
|
388
|
+
queryClient.invalidateQueries({ queryKey: ["customer-revenue-12m"] });
|
|
389
|
+
}, children: "Opnieuw proberen" })] })), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3", children: [_jsx(StatCard, { icon: _jsx(Users, { className: "h-5 w-5" }), tint: "muted", label: "Alle klanten", value: totals.all, tooltip: "Totaal aantal klanten (lokaal + gekoppelde bronnen).", active: view === "all", onClick: () => { setView("all"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(UserCheck, { className: "h-5 w-5" }), tint: "success", label: "Actieve klanten", value: totals.active, tooltip: revenueData
|
|
369
390
|
? `Klanten met minimaal één bestelling of factuur sinds ${formatDateNl(revenueData.since)}.\nTotale omzet in die periode: ${formatEuro(revenueData.totals.amount)} · ${revenueData.totals.transactions} transacties`
|
|
370
391
|
: "Klanten met minimaal één bestelling in de afgelopen 12 maanden.", active: view === "active", onClick: () => { setView("active"); resetPage(); } }), _jsx(StatCard, { icon: _jsx(UserX, { className: "h-5 w-5" }), tint: "warning", label: "Inactieve klanten", value: totals.inactive, tooltip: revenueData
|
|
371
392
|
? `Klanten zonder enige bestelling of factuur sinds ${formatDateNl(revenueData.since)}.`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CustomerPriceGroupSelect.d.ts","sourceRoot":"","sources":["../../../../src/components/klanten/pricegroups/CustomerPriceGroupSelect.tsx"],"names":[],"mappings":"AAcA,wBAAgB,wBAAwB,CAAC,EAAE,WAAW,EAAE,EAAE;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,+BA2DhF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { useServerFn } from "@tanstack/react-start";
|
|
5
|
+
import { toast } from "@flowselections/core";
|
|
6
|
+
import { SearchableSelect } from "../SearchableSelect";
|
|
7
|
+
import { listCustomerPriceGroups, getPriceGroupsForCustomer, setPriceGroupForCustomer, } from "../../../lib/price-groups-local.functions";
|
|
8
|
+
import { friendlyError } from "../../../lib/errors";
|
|
9
|
+
const NONE = "__none__";
|
|
10
|
+
export function CustomerPriceGroupSelect({ customerKey }) {
|
|
11
|
+
const qc = useQueryClient();
|
|
12
|
+
const listGroups = useServerFn(listCustomerPriceGroups);
|
|
13
|
+
const getCurrent = useServerFn(getPriceGroupsForCustomer);
|
|
14
|
+
const setGroup = useServerFn(setPriceGroupForCustomer);
|
|
15
|
+
const [pending, setPending] = useState(null);
|
|
16
|
+
const { data: groups } = useQuery({
|
|
17
|
+
queryKey: ["price-groups-local"],
|
|
18
|
+
queryFn: () => listGroups(),
|
|
19
|
+
});
|
|
20
|
+
const { data: current } = useQuery({
|
|
21
|
+
queryKey: ["customer-price-group-for-customer", customerKey],
|
|
22
|
+
queryFn: () => getCurrent({ data: { customerKey } }),
|
|
23
|
+
enabled: !!customerKey,
|
|
24
|
+
});
|
|
25
|
+
const options = useMemo(() => [
|
|
26
|
+
{ value: NONE, label: "Geen prijsgroep" },
|
|
27
|
+
...(groups ?? [])
|
|
28
|
+
.filter((g) => g.is_active !== false)
|
|
29
|
+
.map((g) => ({ value: g.id, label: g.name })),
|
|
30
|
+
], [groups]);
|
|
31
|
+
const mutation = useMutation({
|
|
32
|
+
mutationFn: (priceGroupId) => setGroup({ data: { customerKey, priceGroupId } }),
|
|
33
|
+
onSuccess: () => {
|
|
34
|
+
toast.success("Prijsgroep bijgewerkt");
|
|
35
|
+
qc.invalidateQueries({ queryKey: ["customer-price-group-for-customer", customerKey] });
|
|
36
|
+
qc.invalidateQueries({ queryKey: ["price-groups-local"] });
|
|
37
|
+
qc.invalidateQueries({ queryKey: ["customer-price-group"] });
|
|
38
|
+
},
|
|
39
|
+
onError: (err) => toast.error(friendlyError(err)),
|
|
40
|
+
onSettled: () => setPending(null),
|
|
41
|
+
});
|
|
42
|
+
const value = pending ?? current?.priceGroupIds?.[0] ?? NONE;
|
|
43
|
+
return (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: "Prijsgroep:" }), _jsx("div", { className: "w-[240px]", children: _jsx(SearchableSelect, { value: value, onValueChange: (v) => {
|
|
44
|
+
setPending(v);
|
|
45
|
+
mutation.mutate(v === NONE ? null : v);
|
|
46
|
+
}, options: options, placeholder: "Geen prijsgroep" }) })] }));
|
|
47
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PriceGroupFormPage.d.ts","sourceRoot":"","sources":["../../../../src/components/klanten/pricegroups/PriceGroupFormPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PriceGroupFormPage.d.ts","sourceRoot":"","sources":["../../../../src/components/klanten/pricegroups/PriceGroupFormPage.tsx"],"names":[],"mappings":"AAqLA,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,+BA2sB9E"}
|
|
@@ -10,6 +10,7 @@ import { createCustomerPriceGroup, updateCustomerPriceGroup, getCustomerPriceGro
|
|
|
10
10
|
import { computePrice } from "../../../lib/pricing";
|
|
11
11
|
import { listTradeItemsForPreview } from "../../../lib/floriday-trade-items.functions";
|
|
12
12
|
import { ProductPickerDialog } from "./ProductPickerDialog";
|
|
13
|
+
import { PriceGroupItemPrices } from "./PriceGroupItemPrices";
|
|
13
14
|
import { CustomerPickerDialog } from "./CustomerPickerDialog";
|
|
14
15
|
import { UserPlus, Users2 } from "lucide-react";
|
|
15
16
|
import { listExternalCustomers } from "../../../lib/external-customers.functions";
|
|
@@ -256,7 +257,7 @@ export function PriceGroupFormPage({ editingId }) {
|
|
|
256
257
|
] }))] }), _jsxs("div", { className: "pt-2", children: [_jsx("div", { className: "text-sm text-muted-foreground mb-2", children: "Diensten" }), _jsx(Separator, { className: "mb-3" }), _jsxs("label", { className: "flex items-center gap-2 cursor-pointer select-none", children: [_jsx(Checkbox, { checked: form.include_stickers, onCheckedChange: (v) => patch({ include_stickers: v === true }) }), _jsx("span", { className: "text-sm font-medium", children: "Inclusief stickeren" })] })] }), _jsxs("div", { className: "pt-2", children: [_jsx("div", { className: "text-sm text-muted-foreground mb-2", children: "Afname" }), _jsx(Separator, { className: "mb-3" }), _jsxs("div", { className: "max-w-sm", children: [_jsx(Label, { children: "Afname-eenheid" }), _jsxs(Select, { value: form.unit, onValueChange: (v) => patch({ unit: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "- Selecteer een eenheid -" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "per_fust", children: "Per fust" }), _jsx(SelectItem, { value: "per_laag", children: "Per laag" }), _jsx(SelectItem, { value: "per_ladingdrager", children: "Per ladingdrager" })] })] })] }), _jsxs("div", { className: "text-xs text-muted-foreground mt-2 leading-relaxed", children: ["Afname-eenheid per aangeboden artikel.", _jsx("br", {}), "Deze instelling geldt voor alle klanten in deze prijsgroep, per aangeboden artikel, niet per bestelling."] })] }), _jsx("div", { className: "pt-2 flex justify-end", children: _jsxs(Button, { onClick: () => nextStep(1), children: ["Volgende stap ", _jsx(ArrowRight, { className: "h-4 w-4 ml-1" })] }) })] }) }), _jsx(StepSection, { index: 2, title: STEP_TITLES[2], subtitle: summaries[2], open: openStep === 2, completed: form.pricing_mode === "calculated" && maxReached >= 2 && openStep !== 2, disabled: maxReached < 2 || form.pricing_mode !== "calculated", onToggle: () => goToStep(2), children: _jsx("div", { className: "space-y-4", children: form.pricing_mode !== "calculated" ? (_jsx("div", { className: "text-sm text-muted-foreground p-4 border rounded-md bg-muted/30", children: "Deze prijsgroep gebruikt handmatige prijzen. Prijsberekening is niet van toepassing." })) : (_jsxs(_Fragment, { children: [_jsxs("div", { children: [_jsx(Label, { children: "Korting/opslag per stuk" }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-3 gap-3 mt-1.5", children: [_jsxs(Select, { value: form.calc_type ?? "discount", onValueChange: (v) => patch({ calc_type: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "discount", children: "Korting" }), _jsx(SelectItem, { value: "surcharge", children: "Opslag" })] })] }), _jsxs(Select, { value: form.calc_value_type ?? "fixed", onValueChange: (v) => patch({ calc_value_type: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "percentage", children: "Percentage" }), _jsx(SelectItem, { value: "fixed", children: "Vast bedrag" })] })] }), _jsxs("div", { className: "relative", children: [form.calc_value_type !== "percentage" && (_jsx("span", { className: "absolute left-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground pointer-events-none", children: "\u20AC" })), _jsx(Input, { type: "number", step: form.calc_value_type === "percentage" ? "1" : "0.001", value: form.calc_value ?? 0, onChange: (e) => patch({ calc_value: Number(e.target.value) }), className: form.calc_value_type === "percentage" ? "pr-8" : "pl-7" }), form.calc_value_type === "percentage" && (_jsx("span", { className: "absolute right-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground pointer-events-none", children: "%" }))] })] })] }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-4", children: [_jsxs("div", { children: [_jsxs(Label, { className: "flex items-center gap-1.5", children: ["Afhandelingskosten (optioneel)", _jsx(Info, { className: "h-3.5 w-3.5 text-muted-foreground", "aria-label": "Extra kosten die bovenop de prijs komen. Kies hoe deze verdeeld worden." })] }), _jsxs("div", { className: "relative mt-1.5", children: [_jsx("span", { className: "absolute left-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground pointer-events-none", children: "\u20AC" }), _jsx(Input, { type: "number", step: "0.01", value: form.handling_fee ?? 0, onChange: (e) => patch({ handling_fee: Number(e.target.value) }), className: "pl-7" })] })] }), _jsxs("div", { children: [_jsx(Label, { children: "Verdeeld over het aantal planten of bloemen op een" }), _jsxs(Select, { value: form.distribute_over ?? "ladingdrager", onValueChange: (v) => patch({ distribute_over: v }), children: [_jsx(SelectTrigger, { className: "mt-1.5", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "ladingdrager", children: "Ladingdrager" }), _jsx(SelectItem, { value: "laag", children: "Laag" }), _jsx(SelectItem, { value: "fust", children: "Fust" })] })] })] })] }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-4", children: [_jsxs("div", { children: [_jsx(Label, { children: "Prijs afronden" }), _jsxs(Select, { value: form.rounding, onValueChange: (v) => patch({ rounding: v }), children: [_jsx(SelectTrigger, { className: "mt-1.5", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "none", children: "Niet afronden" }), _jsx(SelectItem, { value: "up", children: "Omhoog" }), _jsx(SelectItem, { value: "down", children: "Omlaag" }), _jsx(SelectItem, { value: "nearest", children: "Dichtstbijzijnde" })] })] })] }), form.rounding !== "none" && (_jsxs("div", { children: [_jsx(Label, { children: "Op" }), _jsxs(Select, { value: String(form.rounding_step ?? 0.05), onValueChange: (v) => patch({ rounding_step: Number(v) }), children: [_jsx(SelectTrigger, { className: "mt-1.5", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "0.01", children: "\u20AC 0,01" }), _jsx(SelectItem, { value: "0.05", children: "\u20AC 0,05" }), _jsx(SelectItem, { value: "0.1", children: "\u20AC 0,10" })] })] })] }))] }), _jsx("div", { className: "pt-2 flex justify-end", children: _jsxs(Button, { onClick: () => nextStep(2), children: ["Volgende stap ", _jsx(ArrowRight, { className: "h-4 w-4 ml-1" })] }) })] })) }) }), _jsxs(StepSection, { index: 3, title: STEP_TITLES[3], subtitle: summaries[3], open: openStep === 3, completed: form.group_type === "customer_specific" && maxReached >= 3 && openStep !== 3 && selectedCustomerKeys.length > 0, disabled: maxReached < 3 || form.group_type !== "customer_specific", onToggle: () => goToStep(3), children: [form.group_type !== "customer_specific" ? (_jsx("div", { className: "text-sm text-muted-foreground p-4 border rounded-md bg-muted/30", children: "Algemene prijsgroepen zijn zichtbaar voor alle klanten \u2014 een doelgroep is niet van toepassing." })) : selectedCustomerKeys.length === 0 ? (_jsxs("div", { className: "rounded-lg border bg-muted/20 p-8", children: [_jsxs("div", { className: "flex flex-col items-center text-center gap-3", children: [_jsxs("div", { className: "relative h-16 w-16 rounded-full bg-muted flex items-center justify-center", children: [_jsx(Users2, { className: "h-8 w-8 text-muted-foreground/60" }), _jsx("span", { className: "absolute -top-1 -right-1 h-6 w-6 rounded-full bg-background border flex items-center justify-center", children: _jsx(UserPlus, { className: "h-3.5 w-3.5 text-muted-foreground" }) })] }), _jsx("div", { children: _jsx("div", { className: "font-medium text-sm", children: "Nog geen klanten toegevoegd" }) })] }), _jsx("div", { className: "flex justify-end mt-4", children: _jsxs(Button, { variant: "outline", onClick: () => setCustomerPickerOpen(true), children: [_jsx(UserPlus, { className: "h-4 w-4 mr-1.5" }), " Organisaties wijzigen"] }) })] })) : (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "text-sm text-muted-foreground", children: [_jsx("strong", { className: "text-foreground", children: selectedCustomerKeys.length }), " ", "klant", selectedCustomerKeys.length === 1 ? "" : "en", " gekoppeld"] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => setCustomerPickerOpen(true), children: [_jsx(UserPlus, { className: "h-4 w-4 mr-1.5" }), " Organisaties wijzigen"] })] }), _jsx("div", { className: "max-h-[320px] overflow-y-auto border rounded-md divide-y", children: selectedCustomerKeys.map((k) => {
|
|
257
258
|
const c = customersByKey.get(k);
|
|
258
259
|
return (_jsxs("div", { className: "flex items-center gap-3 p-2.5", children: [_jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "text-sm font-medium truncate", children: c?.display_name ?? k }), _jsx("div", { className: "text-xs text-muted-foreground truncate", children: c ? (c.city || c.country ? (_jsxs("span", { className: "inline-flex items-center gap-1", children: [c.city && _jsx("span", { children: c.city }), c.city && c.country && _jsx("span", { children: "\u00B7" }), c.country && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-base", "aria-hidden": "true", children: countryNameToFlag(c.country) ?? "🌐" }), _jsx("span", { children: c.country })] }))] })) : "—") : "Niet in cache" })] }), _jsx("div", { className: "flex gap-1", children: (c?.providers ?? []).slice(0, 3).map((p) => (_jsx(Badge, { variant: "secondary", className: "text-[10px]", children: p }, p))) })] }, k));
|
|
259
|
-
}) })] })), _jsx(CustomerPickerDialog, { open: customerPickerOpen, onOpenChange: setCustomerPickerOpen, value: selectedCustomerKeys, onChange: setSelectedCustomerKeys })] }), error && _jsx("div", { className: "text-sm text-destructive", children: error })] }), form.pricing_mode === "calculated" && (_jsxs("aside", { className: "lg:sticky lg:top-4 space-y-3", children: [_jsx(Card, { children: _jsxs(CardContent, { className: "p-4 space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("div", { className: "text-sm font-medium", children: "Voorbeeld prijscalculatie" }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { variant: "outline", size: "icon", className: "h-7 w-7", onClick: () => cycleItem(-1), disabled: items.length < 2, "aria-label": "Vorige product", children: _jsx(ChevronLeft, { className: "h-4 w-4" }) }), _jsx(Button, { variant: "outline", size: "icon", className: "h-7 w-7", onClick: () => cycleItem(1), disabled: items.length < 2, "aria-label": "Volgend product", children: _jsx(ChevronRight, { className: "h-4 w-4" }) })] })] }), _jsxs("button", { type: "button", onClick: () => setPickerOpen(true), className: "w-full flex items-center gap-3 text-left hover:bg-muted/30 rounded-md p-2 -m-2", children: [_jsx("div", { className: "h-14 w-14 rounded bg-muted flex items-center justify-center overflow-hidden shrink-0", children: selectedItem?.image_url
|
|
260
|
+
}) })] })), _jsx(CustomerPickerDialog, { open: customerPickerOpen, onOpenChange: setCustomerPickerOpen, value: selectedCustomerKeys, onChange: setSelectedCustomerKeys })] }), editingId && (_jsx(PriceGroupItemPrices, { priceGroupId: editingId, currency: form.currency })), error && _jsx("div", { className: "text-sm text-destructive", children: error })] }), form.pricing_mode === "calculated" && (_jsxs("aside", { className: "lg:sticky lg:top-4 space-y-3", children: [_jsx(Card, { children: _jsxs(CardContent, { className: "p-4 space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("div", { className: "text-sm font-medium", children: "Voorbeeld prijscalculatie" }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { variant: "outline", size: "icon", className: "h-7 w-7", onClick: () => cycleItem(-1), disabled: items.length < 2, "aria-label": "Vorige product", children: _jsx(ChevronLeft, { className: "h-4 w-4" }) }), _jsx(Button, { variant: "outline", size: "icon", className: "h-7 w-7", onClick: () => cycleItem(1), disabled: items.length < 2, "aria-label": "Volgend product", children: _jsx(ChevronRight, { className: "h-4 w-4" }) })] })] }), _jsxs("button", { type: "button", onClick: () => setPickerOpen(true), className: "w-full flex items-center gap-3 text-left hover:bg-muted/30 rounded-md p-2 -m-2", children: [_jsx("div", { className: "h-14 w-14 rounded bg-muted flex items-center justify-center overflow-hidden shrink-0", children: selectedItem?.image_url
|
|
260
261
|
? _jsx("img", { src: selectedItem.image_url, alt: "", className: "h-full w-full object-contain" })
|
|
261
262
|
: _jsx(ImageIcon, { className: "h-6 w-6 text-muted-foreground" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-sm font-medium truncate", children: selectedItem?.name ?? "Geen product beschikbaar" }), _jsx("div", { className: "text-xs text-muted-foreground truncate", children: selectedItem
|
|
262
263
|
? [selectedItem.packaging_code,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PriceGroupItemPrices.d.ts","sourceRoot":"","sources":["../../../../src/components/klanten/pricegroups/PriceGroupItemPrices.tsx"],"names":[],"mappings":"AAqBA,wBAAgB,oBAAoB,CAAC,EACnC,YAAY,EACZ,QAAgB,GACjB,EAAE;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,+BA0HA"}
|