@flowselections/floriday-klanten-module 1.0.15 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist-lib/assets/floriday-logo.png.asset.json +11 -0
  2. package/dist-lib/components/klanten/BackLink.d.ts +12 -0
  3. package/dist-lib/components/klanten/BackLink.d.ts.map +1 -0
  4. package/dist-lib/components/klanten/BackLink.js +12 -0
  5. package/dist-lib/components/klanten/FloridayAssortmentTab.d.ts +4 -0
  6. package/dist-lib/components/klanten/FloridayAssortmentTab.d.ts.map +1 -0
  7. package/dist-lib/components/klanten/FloridayAssortmentTab.js +80 -0
  8. package/dist-lib/components/klanten/FloridayDeeplinkButton.d.ts +8 -0
  9. package/dist-lib/components/klanten/FloridayDeeplinkButton.d.ts.map +1 -0
  10. package/dist-lib/components/klanten/FloridayDeeplinkButton.js +9 -0
  11. package/dist-lib/components/klanten/GroupedKlantDetail.d.ts.map +1 -1
  12. package/dist-lib/components/klanten/GroupedKlantDetail.js +19 -5
  13. package/dist-lib/components/klanten/ImportKlanten.d.ts.map +1 -1
  14. package/dist-lib/components/klanten/ImportKlanten.js +3 -2
  15. package/dist-lib/components/klanten/KlantDetail.d.ts.map +1 -1
  16. package/dist-lib/components/klanten/KlantDetail.js +9 -3
  17. package/dist-lib/components/klanten/KlantenOverzicht.d.ts.map +1 -1
  18. package/dist-lib/components/klanten/KlantenOverzicht.js +6 -3
  19. package/dist-lib/components/klanten/NetwerkPagina.d.ts.map +1 -1
  20. package/dist-lib/components/klanten/NetwerkPagina.js +21 -18
  21. package/dist-lib/components/klanten/NieuweKlant.d.ts.map +1 -1
  22. package/dist-lib/components/klanten/NieuweKlant.js +23 -8
  23. package/dist-lib/components/klanten/PriceGroupBadges.d.ts +7 -0
  24. package/dist-lib/components/klanten/PriceGroupBadges.d.ts.map +1 -0
  25. package/dist-lib/components/klanten/PriceGroupBadges.js +13 -0
  26. package/dist-lib/components/klanten/SyncStatusChip.d.ts +4 -0
  27. package/dist-lib/components/klanten/SyncStatusChip.d.ts.map +1 -0
  28. package/dist-lib/components/klanten/SyncStatusChip.js +49 -0
  29. package/dist-lib/components/klanten/pricegroups/CustomerPickerDialog.d.ts +8 -0
  30. package/dist-lib/components/klanten/pricegroups/CustomerPickerDialog.d.ts.map +1 -0
  31. package/dist-lib/components/klanten/pricegroups/CustomerPickerDialog.js +75 -0
  32. package/dist-lib/components/klanten/pricegroups/PriceGroupFormPage.d.ts +4 -0
  33. package/dist-lib/components/klanten/pricegroups/PriceGroupFormPage.d.ts.map +1 -0
  34. package/dist-lib/components/klanten/pricegroups/PriceGroupFormPage.js +269 -0
  35. package/dist-lib/components/klanten/pricegroups/PriceGroupsPage.d.ts +2 -0
  36. package/dist-lib/components/klanten/pricegroups/PriceGroupsPage.d.ts.map +1 -0
  37. package/dist-lib/components/klanten/pricegroups/PriceGroupsPage.js +133 -0
  38. package/dist-lib/components/klanten/pricegroups/ProductPickerDialog.d.ts +8 -0
  39. package/dist-lib/components/klanten/pricegroups/ProductPickerDialog.d.ts.map +1 -0
  40. package/dist-lib/components/klanten/pricegroups/ProductPickerDialog.js +18 -0
  41. package/dist-lib/index.d.ts +1 -0
  42. package/dist-lib/index.d.ts.map +1 -1
  43. package/dist-lib/index.js +6 -1
  44. package/dist-lib/integrations/supabase/auth-middleware.d.ts +762 -3
  45. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  46. package/dist-lib/integrations/supabase/client.d.ts +762 -3
  47. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  48. package/dist-lib/integrations/supabase/client.js +4 -3
  49. package/dist-lib/integrations/supabase/client.server.d.ts +762 -3
  50. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  51. package/dist-lib/integrations/supabase/types.d.ts +789 -8
  52. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  53. package/dist-lib/lib/accounting/twinfield/client.server.d.ts +21 -0
  54. package/dist-lib/lib/accounting/twinfield/client.server.d.ts.map +1 -0
  55. package/dist-lib/lib/accounting/twinfield/client.server.js +171 -0
  56. package/dist-lib/lib/accounting/twinfield/mapping.d.ts +46 -0
  57. package/dist-lib/lib/accounting/twinfield/mapping.d.ts.map +1 -0
  58. package/dist-lib/lib/accounting/twinfield/mapping.js +109 -0
  59. package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts +36318 -0
  60. package/dist-lib/lib/accounting/twinfield/worker.functions.d.ts.map +1 -0
  61. package/dist-lib/lib/accounting/twinfield/worker.functions.js +48 -0
  62. package/dist-lib/lib/accounting/twinfield/worker.server.d.ts +12 -0
  63. package/dist-lib/lib/accounting/twinfield/worker.server.d.ts.map +1 -0
  64. package/dist-lib/lib/accounting/twinfield/worker.server.js +124 -0
  65. package/dist-lib/lib/accounting.functions.d.ts +2286 -9
  66. package/dist-lib/lib/accounting.functions.d.ts.map +1 -1
  67. package/dist-lib/lib/customer-analytics.functions.d.ts +762 -3
  68. package/dist-lib/lib/customer-analytics.functions.d.ts.map +1 -1
  69. package/dist-lib/lib/customer-schemas.d.ts +2 -0
  70. package/dist-lib/lib/customer-schemas.d.ts.map +1 -1
  71. package/dist-lib/lib/customer-schemas.js +1 -0
  72. package/dist-lib/lib/customers.functions.d.ts +18241 -99
  73. package/dist-lib/lib/customers.functions.d.ts.map +1 -1
  74. package/dist-lib/lib/customers.functions.js +92 -4
  75. package/dist-lib/lib/delivery-locations.functions.d.ts +3048 -12
  76. package/dist-lib/lib/delivery-locations.functions.d.ts.map +1 -1
  77. package/dist-lib/lib/external-customers.functions.d.ts +3048 -12
  78. package/dist-lib/lib/external-customers.functions.d.ts.map +1 -1
  79. package/dist-lib/lib/floriday-assortment.functions.d.ts +12127 -0
  80. package/dist-lib/lib/floriday-assortment.functions.d.ts.map +1 -0
  81. package/dist-lib/lib/floriday-assortment.functions.js +213 -0
  82. package/dist-lib/lib/floriday-discounts.functions.d.ts +1800 -282
  83. package/dist-lib/lib/floriday-discounts.functions.d.ts.map +1 -1
  84. package/dist-lib/lib/floriday-trade-items.functions.d.ts +12111 -0
  85. package/dist-lib/lib/floriday-trade-items.functions.d.ts.map +1 -0
  86. package/dist-lib/lib/floriday-trade-items.functions.js +107 -0
  87. package/dist-lib/lib/local-discounts.functions.d.ts +5336 -23
  88. package/dist-lib/lib/local-discounts.functions.d.ts.map +1 -1
  89. package/dist-lib/lib/overdue-invoices.functions.d.ts +762 -3
  90. package/dist-lib/lib/overdue-invoices.functions.d.ts.map +1 -1
  91. package/dist-lib/lib/price-groups-local.functions.d.ts +84841 -0
  92. package/dist-lib/lib/price-groups-local.functions.d.ts.map +1 -0
  93. package/dist-lib/lib/price-groups-local.functions.js +148 -0
  94. package/dist-lib/lib/price-groups.functions.d.ts +12120 -0
  95. package/dist-lib/lib/price-groups.functions.d.ts.map +1 -0
  96. package/dist-lib/lib/price-groups.functions.js +115 -0
  97. package/dist-lib/lib/pricing.d.ts +24 -0
  98. package/dist-lib/lib/pricing.d.ts.map +1 -0
  99. package/dist-lib/lib/pricing.js +60 -0
  100. package/package.json +6 -3
@@ -5,8 +5,9 @@ import { useForm } from "react-hook-form";
5
5
  import { zodResolver } from "@hookform/resolvers/zod";
6
6
  import { useMutation, useQuery } from "@tanstack/react-query";
7
7
  import { useServerFn } from "@tanstack/react-start";
8
- import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, Textarea, toast, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@flowselections/core";
9
- import { ArrowLeft } from "lucide-react";
8
+ import { Button, Card, CardContent, Input, Label, Textarea, toast, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@flowselections/core";
9
+ import { UserRound, MapPin, Landmark, ReceiptText, ChevronDown, Check, AlertCircle, Users, } from "lucide-react";
10
+ import { BackLink } from "../../components/klanten/BackLink";
10
11
  import { createCustomer, listCustomerClasses } from "../../lib/customers.functions";
11
12
  import { customerCreateSchema } from "../../lib/customer-schemas";
12
13
  export function NieuweKlant() {
@@ -15,12 +16,18 @@ export function NieuweKlant() {
15
16
  const classesFn = useServerFn(listCustomerClasses);
16
17
  const [dupes, setDupes] = useState([]);
17
18
  const [pendingData, setPendingData] = useState(null);
19
+ const [open, setOpen] = useState({
20
+ algemeen: true, adres: true, bank: true, facturering: true,
21
+ });
22
+ const toggle = (k) => setOpen((s) => ({ ...s, [k]: !s[k] }));
18
23
  const { data: classData } = useQuery({
19
24
  queryKey: ["customer-classes"],
20
25
  queryFn: () => classesFn({ data: undefined }),
21
26
  });
22
27
  const form = useForm({
23
28
  resolver: zodResolver(customerCreateSchema),
29
+ mode: "onBlur",
30
+ reValidateMode: "onChange",
24
31
  defaultValues: {
25
32
  company_name: "", vat_number: "", contact_person: "", email: "", phone: "",
26
33
  street: "", house_number: "", postal_code: "", city: "", country: "Nederland",
@@ -50,17 +57,25 @@ export function NieuweKlant() {
50
57
  });
51
58
  const onSubmit = (values) => {
52
59
  setPendingData(values);
53
- mut.mutate(values);
60
+ mut.mutate({ ...values, push_to_twinfield: true });
54
61
  };
55
62
  const forceCreate = () => {
56
63
  if (!pendingData)
57
64
  return;
58
65
  setDupes([]);
59
- mut.mutate({ ...pendingData, force: true });
66
+ mut.mutate({ ...pendingData, force: true, push_to_twinfield: true });
60
67
  };
61
- const fieldErr = (k) => form.formState.errors[k]?.message;
62
- return (_jsxs("main", { className: "p-6 max-w-5xl mx-auto space-y-4", children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate({ to: "/klanten" }), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-2" }), " Terug"] }), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Algemeen" }) }), _jsxs(CardContent, { className: "grid grid-cols-2 gap-4", children: [_jsx(Field, { label: "Nummer", error: fieldErr("customer_number"), children: _jsx(Input, { ...form.register("customer_number") }) }), _jsx(Field, { label: "Bedrijfsnaam *", error: fieldErr("company_name"), children: _jsx(Input, { ...form.register("company_name") }) }), _jsx(Field, { label: "Taal", error: fieldErr("language"), children: _jsx(Input, { placeholder: "bv. nl, en", ...form.register("language") }) }), _jsx(Field, { label: "Website", error: fieldErr("website"), children: _jsx(Input, { ...form.register("website") }) }), _jsx(Field, { label: "BTW-nummer", error: fieldErr("vat_number"), children: _jsx(Input, { ...form.register("vat_number") }) }), _jsx(Field, { label: "KvK-nummer", error: fieldErr("coc_number"), children: _jsx(Input, { ...form.register("coc_number") }) }), _jsx(Field, { label: "Contactpersoon", error: fieldErr("contact_person"), children: _jsx(Input, { ...form.register("contact_person") }) }), _jsx(Field, { label: "T.a.v.", error: fieldErr("attention_to"), children: _jsx(Input, { ...form.register("attention_to") }) }), _jsx(Field, { label: "E-mail *", error: fieldErr("email"), children: _jsx(Input, { type: "email", ...form.register("email") }) }), _jsx(Field, { label: "E-mail voor facturering", error: fieldErr("billing_email"), children: _jsx(Input, { type: "email", ...form.register("billing_email") }) }), _jsx(Field, { label: "Telefoon", error: fieldErr("phone"), children: _jsx(Input, { ...form.register("phone") }) }), _jsx(Field, { label: "Faxnummer", error: fieldErr("fax"), children: _jsx(Input, { ...form.register("fax") }) }), _jsx(Field, { label: "Klantklasse", children: _jsxs(Select, { onValueChange: (v) => form.setValue("customer_class", v), defaultValue: "", children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies klasse..." }) }), _jsx(SelectContent, { children: (classData?.classes ?? []).map((c) => _jsx(SelectItem, { value: c, children: c }, c)) })] }) })] })] }), _jsxs(Card, { className: "mt-4", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Adres" }) }), _jsxs(CardContent, { className: "grid grid-cols-2 gap-4", children: [_jsx(Field, { label: "Adrestype", error: fieldErr("address_type"), children: _jsx(Input, { placeholder: "bv. Bezoek, Post", ...form.register("address_type") }) }), _jsx(Field, { label: "ICP landcode", error: fieldErr("icp_country_code"), children: _jsx(Input, { placeholder: "bv. NL", ...form.register("icp_country_code") }) }), _jsx(Field, { label: "Adresregel 1", error: fieldErr("address_line1"), children: _jsx(Input, { ...form.register("address_line1") }) }), _jsx(Field, { label: "Adresregel 2", error: fieldErr("address_line2"), children: _jsx(Input, { ...form.register("address_line2") }) }), _jsx(Field, { label: "Straat *", error: fieldErr("street"), children: _jsx(Input, { ...form.register("street") }) }), _jsx(Field, { label: "Huisnummer *", error: fieldErr("house_number"), children: _jsx(Input, { ...form.register("house_number") }) }), _jsx(Field, { label: "Postcode", error: fieldErr("postal_code"), children: _jsx(Input, { ...form.register("postal_code") }) }), _jsx(Field, { label: "Plaats", error: fieldErr("city"), children: _jsx(Input, { ...form.register("city") }) }), _jsx(Field, { label: "Land *", error: fieldErr("country"), children: _jsx(Input, { ...form.register("country") }) })] })] }), _jsxs(Card, { className: "mt-4", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Bankgegevens" }) }), _jsxs(CardContent, { className: "grid grid-cols-2 gap-4", children: [_jsx(Field, { label: "Bank - Naam", children: _jsx(Input, { ...form.register("bank_name") }) }), _jsx(Field, { label: "Bank - Rekeninghouder", children: _jsx(Input, { ...form.register("bank_account_holder") }) }), _jsx(Field, { label: "Bank - Rekeningnummer", children: _jsx(Input, { ...form.register("bank_account_number") }) }), _jsx(Field, { label: "Bank - IBAN", children: _jsx(Input, { ...form.register("bank_iban") }) }), _jsx(Field, { label: "Bank - BIC", children: _jsx(Input, { ...form.register("bank_bic") }) }), _jsx(Field, { label: "Bank - Nationale bankcode", children: _jsx(Input, { ...form.register("bank_national_code") }) }), _jsx(Field, { label: "Bank - Adresregel 1", children: _jsx(Input, { ...form.register("bank_address_line1") }) }), _jsx(Field, { label: "Bank - Adresregel 2", children: _jsx(Input, { ...form.register("bank_address_line2") }) }), _jsx(Field, { label: "Bank - Huisnummer", children: _jsx(Input, { ...form.register("bank_house_number") }) }), _jsx(Field, { label: "Bank - Postcode", children: _jsx(Input, { ...form.register("bank_postal_code") }) }), _jsx(Field, { label: "Bank - Plaats", children: _jsx(Input, { ...form.register("bank_city") }) }), _jsx(Field, { label: "Bank - Staat", children: _jsx(Input, { ...form.register("bank_state") }) }), _jsx(Field, { label: "Bank - Land", children: _jsx(Input, { ...form.register("bank_country") }) })] })] }), _jsxs(Card, { className: "mt-4", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Facturering & betaling" }) }), _jsxs(CardContent, { className: "grid grid-cols-2 gap-4", children: [_jsx(Field, { label: "Kortingspercentage (%)", error: fieldErr("discount_percentage"), children: _jsx(Input, { type: "number", step: "0.01", ...form.register("discount_percentage", { valueAsNumber: true }) }) }), _jsx(Field, { label: "Aantal kortingsdagen", error: fieldErr("discount_days"), children: _jsx(Input, { type: "number", ...form.register("discount_days", { valueAsNumber: true }) }) }), _jsx(Field, { label: "Aantal vervaldagen", error: fieldErr("payment_term_days"), children: _jsx(Input, { type: "number", ...form.register("payment_term_days", { valueAsNumber: true }) }) }), _jsx(Field, { label: "Btw-weergave op factuur", error: fieldErr("vat_display"), children: _jsx(Input, { placeholder: "bv. Inclusief, Exclusief", ...form.register("vat_display") }) }), _jsxs("div", { className: "col-span-2", children: [_jsx(Label, { children: "Notities" }), _jsx(Textarea, { ...form.register("notes") })] })] })] }), _jsxs("div", { className: "flex justify-end gap-2 mt-4", children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => navigate({ to: "/klanten" }), children: "Annuleren" }), _jsx(Button, { type: "submit", disabled: mut.isPending, children: mut.isPending ? "Opslaan..." : "Klant aanmaken" })] })] }), _jsx(AlertDialog, { open: dupes.length > 0, onOpenChange: (o) => !o && setDupes([]), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Mogelijke duplicaten gevonden" }), _jsx(AlertDialogDescription, { children: _jsx("ul", { className: "list-disc pl-5 space-y-1 mt-2", children: dupes.map((d) => _jsxs("li", { children: [_jsx("strong", { children: d.company_name }), " \u2014 ", d.reason] }, d.id + d.reason)) }) })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: forceCreate, children: "Toch aanmaken" })] })] }) })] }));
68
+ const errors = form.formState.errors;
69
+ const errorCount = Object.keys(errors).length;
70
+ const touched = form.formState.touchedFields;
71
+ const values = form.watch();
72
+ const fieldErr = (k) => errors[k]?.message;
73
+ const isValid = (k) => !errors[k] && !!touched[k] && !!values[k];
74
+ return (_jsxs("main", { className: "min-h-screen bg-muted/30 pb-28", children: [_jsxs("div", { className: "max-w-6xl mx-auto px-6 pt-6", children: [_jsx(BackLink, {}), _jsxs("header", { className: "flex items-start gap-4 mb-6", children: [_jsx("div", { className: "h-12 w-12 shrink-0 rounded-full bg-primary/10 text-primary grid place-items-center", children: _jsx(UserRound, { className: "h-6 w-6" }) }), _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Nieuwe klant" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Vul de gegevens in om een nieuwe klant aan te maken." })] })] }), errorCount > 0 && form.formState.isSubmitted && (_jsxs("div", { className: "mb-4 flex items-start gap-3 rounded-xl border border-destructive/30 bg-destructive/5 px-4 py-3 text-sm", children: [_jsx(AlertCircle, { className: "h-5 w-5 shrink-0 text-destructive mt-0.5" }), _jsxs("div", { children: [_jsxs("p", { className: "font-medium text-destructive", children: ["Controleer ", errorCount, " ", errorCount === 1 ? "veld" : "velden"] }), _jsx("p", { className: "text-muted-foreground mt-0.5", children: "Er zijn een aantal velden nog niet correct ingevuld." })] })] })), _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "space-y-4", children: [_jsx(Section, { k: "algemeen", title: "Algemeen", icon: _jsx(Users, { className: "h-5 w-5" }), open: open.algemeen, onToggle: toggle, children: _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4", children: [_jsx(Field, { label: "Nummer", error: fieldErr("customer_number"), success: isValid("customer_number"), children: _jsx(Input, { ...form.register("customer_number") }) }), _jsx(Field, { label: "Bedrijfsnaam", required: true, error: fieldErr("company_name"), success: isValid("company_name"), children: _jsx(Input, { "aria-invalid": !!errors.company_name, ...form.register("company_name") }) }), _jsx(Field, { label: "Taal", error: fieldErr("language"), children: _jsxs(Select, { value: form.watch("language") || "nl", onValueChange: (v) => form.setValue("language", v, { shouldValidate: true }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "nl", children: "nl" }), _jsx(SelectItem, { value: "en", children: "en" }), _jsx(SelectItem, { value: "de", children: "de" }), _jsx(SelectItem, { value: "fr", children: "fr" })] })] }) }), _jsx(Field, { label: "Website", error: fieldErr("website"), success: isValid("website"), children: _jsx(Input, { placeholder: "https://", ...form.register("website") }) }), _jsx(Field, { label: "BTW-nummer", error: fieldErr("vat_number"), success: isValid("vat_number"), children: _jsx(Input, { ...form.register("vat_number") }) }), _jsx(Field, { label: "KvK-nummer", error: fieldErr("coc_number"), success: isValid("coc_number"), children: _jsx(Input, { ...form.register("coc_number") }) }), _jsx(Field, { label: "Contactpersoon", error: fieldErr("contact_person"), success: isValid("contact_person"), children: _jsx(Input, { ...form.register("contact_person") }) }), _jsx(Field, { label: "T.a.v.", error: fieldErr("attention_to"), children: _jsx(Input, { ...form.register("attention_to") }) }), _jsx(Field, { label: "E-mail", required: true, error: fieldErr("email"), success: isValid("email"), children: _jsx(Input, { type: "email", "aria-invalid": !!errors.email, placeholder: "naam@bedrijf.nl", ...form.register("email") }) }), _jsx(Field, { label: "E-mail voor facturering", error: fieldErr("billing_email"), success: isValid("billing_email"), children: _jsx(Input, { type: "email", placeholder: "facturen@bedrijf.nl", ...form.register("billing_email") }) }), _jsx(Field, { label: "Telefoon", error: fieldErr("phone"), success: isValid("phone"), children: _jsx(Input, { ...form.register("phone") }) }), _jsx(Field, { label: "Faxnummer", error: fieldErr("fax"), children: _jsx(Input, { ...form.register("fax") }) }), _jsx(Field, { label: "Klantklasse", children: _jsxs(Select, { onValueChange: (v) => form.setValue("customer_class", v), defaultValue: "", children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Kies klasse..." }) }), _jsx(SelectContent, { children: (classData?.classes ?? []).map((c) => _jsx(SelectItem, { value: c, children: c }, c)) })] }) })] }) }), _jsx(Section, { k: "adres", title: "Adres", icon: _jsx(MapPin, { className: "h-5 w-5" }), open: open.adres, onToggle: toggle, children: _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: [_jsx(Field, { label: "Adres type", error: fieldErr("address_type"), children: _jsx(Input, { placeholder: "bv. Bezoek, Post", ...form.register("address_type") }) }), _jsx(Field, { label: "ICP landcode", error: fieldErr("icp_country_code"), children: _jsx(Input, { placeholder: "bv. NL", ...form.register("icp_country_code") }) }), _jsx(Field, { label: "Adresregel 1", error: fieldErr("address_line1"), children: _jsx(Input, { ...form.register("address_line1") }) }), _jsx(Field, { label: "Adresregel 2", error: fieldErr("address_line2"), children: _jsx(Input, { ...form.register("address_line2") }) }), _jsx(Field, { label: "Straat", required: true, error: fieldErr("street"), success: isValid("street"), children: _jsx(Input, { "aria-invalid": !!errors.street, ...form.register("street") }) }), _jsx(Field, { label: "Huisnummer", required: true, error: fieldErr("house_number"), success: isValid("house_number"), children: _jsx(Input, { "aria-invalid": !!errors.house_number, ...form.register("house_number") }) }), _jsx(Field, { label: "Postcode", error: fieldErr("postal_code"), success: isValid("postal_code"), children: _jsx(Input, { ...form.register("postal_code") }) }), _jsx(Field, { label: "Plaats", error: fieldErr("city"), success: isValid("city"), children: _jsx(Input, { ...form.register("city") }) }), _jsx(Field, { label: "Land", required: true, error: fieldErr("country"), success: isValid("country"), children: _jsxs(Select, { value: form.watch("country") || "Nederland", onValueChange: (v) => form.setValue("country", v, { shouldValidate: true }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "Nederland", children: "Nederland" }), _jsx(SelectItem, { value: "Belgi\u00EB", children: "Belgi\u00EB" }), _jsx(SelectItem, { value: "Duitsland", children: "Duitsland" }), _jsx(SelectItem, { value: "Frankrijk", children: "Frankrijk" }), _jsx(SelectItem, { value: "Verenigd Koninkrijk", children: "Verenigd Koninkrijk" })] })] }) })] }) }), _jsx(Section, { k: "bank", title: "Bankgegevens", icon: _jsx(Landmark, { className: "h-5 w-5" }), open: open.bank, onToggle: toggle, children: _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: [_jsx(Field, { label: "Bank - Naam", children: _jsx(Input, { ...form.register("bank_name") }) }), _jsx(Field, { label: "Bank - Rekeninghouder", children: _jsx(Input, { ...form.register("bank_account_holder") }) }), _jsx(Field, { label: "Bank - Rekeningnummer", children: _jsx(Input, { ...form.register("bank_account_number") }) }), _jsx(Field, { label: "Bank - IBAN", children: _jsx(Input, { placeholder: "NL00 BANK 0000 0000 00", ...form.register("bank_iban") }) }), _jsx(Field, { label: "Bank - BIC", children: _jsx(Input, { ...form.register("bank_bic") }) }), _jsx(Field, { label: "Bank - Nationale bankcode", children: _jsx(Input, { ...form.register("bank_national_code") }) }), _jsx(Field, { label: "Bank - Adresregel 1", children: _jsx(Input, { ...form.register("bank_address_line1") }) }), _jsx(Field, { label: "Bank - Adresregel 2", children: _jsx(Input, { ...form.register("bank_address_line2") }) }), _jsx(Field, { label: "Bank - Huisnummer", children: _jsx(Input, { ...form.register("bank_house_number") }) }), _jsx(Field, { label: "Bank - Postcode", children: _jsx(Input, { ...form.register("bank_postal_code") }) }), _jsx(Field, { label: "Bank - Plaats", children: _jsx(Input, { ...form.register("bank_city") }) }), _jsx(Field, { label: "Bank - Staat", children: _jsx(Input, { ...form.register("bank_state") }) }), _jsx("div", { className: "md:col-span-2", children: _jsx(Field, { label: "Bank - Land", children: _jsx(Input, { ...form.register("bank_country") }) }) })] }) }), _jsx(Section, { k: "facturering", title: "Facturering & betaling", icon: _jsx(ReceiptText, { className: "h-5 w-5" }), open: open.facturering, onToggle: toggle, children: _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: [_jsx(Field, { label: "Kortingspercentage (%)", error: fieldErr("discount_percentage"), children: _jsx(Input, { type: "number", step: "0.01", ...form.register("discount_percentage", { valueAsNumber: true }) }) }), _jsx(Field, { label: "Aantal kortingsdagen", error: fieldErr("discount_days"), children: _jsx(Input, { type: "number", ...form.register("discount_days", { valueAsNumber: true }) }) }), _jsx(Field, { label: "Aantal vervaldagen", error: fieldErr("payment_term_days"), children: _jsx(Input, { type: "number", ...form.register("payment_term_days", { valueAsNumber: true }) }) }), _jsx(Field, { label: "Btw-weergave op factuur", error: fieldErr("vat_display"), children: _jsxs(Select, { value: form.watch("vat_display") || "", onValueChange: (v) => form.setValue("vat_display", v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "bv. Inclusief, Exclusief" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "Inclusief", children: "Inclusief" }), _jsx(SelectItem, { value: "Exclusief", children: "Exclusief" })] })] }) }), _jsxs("div", { className: "md:col-span-2 space-y-1.5", children: [_jsx(Label, { children: "Notities" }), _jsx(Textarea, { rows: 4, ...form.register("notes") })] })] }) })] })] }), _jsx("div", { className: "fixed bottom-0 left-0 right-0 border-t bg-background/90 backdrop-blur z-20", children: _jsxs("div", { className: "max-w-6xl mx-auto px-6 py-3 flex justify-end gap-2", children: [_jsx(Button, { type: "button", variant: "outline", onClick: () => navigate({ to: "/klanten" }), children: "Annuleren" }), _jsxs(Button, { type: "button", onClick: form.handleSubmit(onSubmit), disabled: mut.isPending, children: [_jsx(Check, { className: "h-4 w-4 mr-2" }), mut.isPending ? "Opslaan..." : "Klant aanmaken"] })] }) }), _jsx(AlertDialog, { open: dupes.length > 0, onOpenChange: (o) => !o && setDupes([]), children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Mogelijke duplicaten gevonden" }), _jsx(AlertDialogDescription, { children: _jsx("ul", { className: "list-disc pl-5 space-y-1 mt-2", children: dupes.map((d) => _jsxs("li", { children: [_jsx("strong", { children: d.company_name }), " \u2014 ", d.reason] }, d.id + d.reason)) }) })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogCancel, { children: "Annuleren" }), _jsx(AlertDialogAction, { onClick: forceCreate, children: "Toch aanmaken" })] })] }) })] }));
75
+ }
76
+ function Section({ k, title, icon, open, onToggle, children, }) {
77
+ return (_jsxs(Card, { className: "rounded-2xl border shadow-sm overflow-hidden", children: [_jsxs("button", { type: "button", onClick: () => onToggle(k), className: "w-full flex items-center justify-between px-6 py-4 hover:bg-muted/40 transition-colors", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "text-primary", children: icon }), _jsx("span", { className: "text-base font-semibold", children: title })] }), _jsx(ChevronDown, { className: `h-5 w-5 text-muted-foreground transition-transform ${open ? "" : "-rotate-90"}` })] }), open && (_jsx(CardContent, { className: "px-6 pb-6 pt-0", children: children }))] }));
63
78
  }
64
- function Field({ label, error, children }) {
65
- return (_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { children: label }), children, error && _jsx("p", { className: "text-xs text-destructive", children: error })] }));
79
+ function Field({ label, required, error, success, children, }) {
80
+ return (_jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { className: "text-sm font-medium", children: [label, required && _jsx("span", { className: "text-destructive ml-0.5", children: "*" })] }), _jsxs("div", { className: `relative ${error ? "[&_input]:border-destructive [&_input]:focus-visible:ring-destructive/30" : ""}`, children: [children, success && !error && (_jsx(Check, { className: "pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-emerald-600" }))] }), error && _jsxs("p", { className: "text-xs text-destructive flex items-center gap-1", children: [_jsx(AlertCircle, { className: "h-3 w-3" }), error] })] }));
66
81
  }
@@ -0,0 +1,7 @@
1
+ import type { PriceGroup } from "../../lib/price-groups.functions";
2
+ export declare function PriceGroupBadges({ groups, max, emptyLabel, }: {
3
+ groups: PriceGroup[] | null | undefined;
4
+ max?: number;
5
+ emptyLabel?: string;
6
+ }): import("react").JSX.Element;
7
+ //# sourceMappingURL=PriceGroupBadges.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PriceGroupBadges.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/PriceGroupBadges.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAE/D,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,GAAO,EACP,UAAgB,GACjB,EAAE;IACD,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,+BAoCA"}
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Badge } from "@flowselections/core";
3
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
4
+ export function PriceGroupBadges({ groups, max = 1, emptyLabel = "—", }) {
5
+ const list = groups ?? [];
6
+ if (list.length === 0) {
7
+ return _jsx("span", { className: "text-muted-foreground", children: emptyLabel });
8
+ }
9
+ const visible = list.slice(0, max);
10
+ const extra = list.length - visible.length;
11
+ const tooltip = list.map((g) => g.name).join(", ");
12
+ return (_jsxs(TooltipPrimitive.Root, { children: [_jsx(TooltipPrimitive.Trigger, { asChild: true, children: _jsxs("span", { className: "inline-flex items-center gap-1 flex-wrap", children: [visible.map((g) => (_jsx(Badge, { variant: "secondary", className: "max-w-[160px] truncate", children: g.name }, g.id))), extra > 0 && (_jsxs(Badge, { variant: "outline", className: "text-xs", children: ["+", extra] }))] }) }), _jsx(TooltipPrimitive.Portal, { children: _jsxs(TooltipPrimitive.Content, { side: "top", sideOffset: 6, className: "z-50 max-w-xs rounded-md border border-border bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-md", children: [tooltip, _jsx(TooltipPrimitive.Arrow, { className: "fill-popover" })] }) })] }));
13
+ }
@@ -0,0 +1,4 @@
1
+ export declare function SyncStatusChip({ customerId }: {
2
+ customerId: string;
3
+ }): import("react").JSX.Element | null;
4
+ //# sourceMappingURL=SyncStatusChip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SyncStatusChip.d.ts","sourceRoot":"","sources":["../../../src/components/klanten/SyncStatusChip.tsx"],"names":[],"mappings":"AAUA,wBAAgB,cAAc,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,sCAsEpE"}
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
4
+ import { useServerFn } from "@tanstack/react-start";
5
+ import { Badge } from "@flowselections/core";
6
+ import { CheckCircle2, Clock, AlertTriangle, Loader2 } from "lucide-react";
7
+ import { getTwinfieldSyncStatus, retryTwinfieldPush, } from "../../lib/accounting/twinfield/worker.functions";
8
+ export function SyncStatusChip({ customerId }) {
9
+ const qc = useQueryClient();
10
+ const [retrying, setRetrying] = useState(false);
11
+ const fetchStatus = useServerFn(getTwinfieldSyncStatus);
12
+ const doRetry = useServerFn(retryTwinfieldPush);
13
+ const { data } = useQuery({
14
+ queryKey: ["twinfield-sync-status", customerId],
15
+ queryFn: () => fetchStatus({ data: { customerId } }),
16
+ refetchInterval: 30000,
17
+ staleTime: 15000,
18
+ });
19
+ const retry = useMutation({
20
+ mutationFn: async () => {
21
+ setRetrying(true);
22
+ return doRetry({ data: { customerId } });
23
+ },
24
+ onSettled: () => {
25
+ setRetrying(false);
26
+ qc.invalidateQueries({ queryKey: ["twinfield-sync-status", customerId] });
27
+ },
28
+ });
29
+ if (!data)
30
+ return null;
31
+ const latest = data.latest;
32
+ const customer = data.customer;
33
+ const synced = customer?.twinfield_synced_at;
34
+ const status = latest?.status;
35
+ // Toon niets als er nooit een queue-rij is geweest én er ook geen sync bestaat
36
+ if (!synced && !latest)
37
+ return null;
38
+ if (status === "failed" || status === "dead") {
39
+ return (_jsx("button", { type: "button", onClick: () => !retrying && retry.mutate(), className: "inline-flex items-center gap-1.5 text-xs", title: latest?.last_error ?? "Onbekende fout", children: _jsxs(Badge, { variant: "destructive", className: "gap-1", children: [retrying ? _jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : _jsx(AlertTriangle, { className: "h-3 w-3" }), "Synchronisatie mislukt \u2014 opnieuw proberen"] }) }));
40
+ }
41
+ if (status === "pending" || status === "processing") {
42
+ return (_jsxs(Badge, { variant: "secondary", className: "gap-1", children: [_jsx(Clock, { className: "h-3 w-3" }), "Wacht op synchronisatie"] }));
43
+ }
44
+ if (synced) {
45
+ const when = new Date(synced).toLocaleString("nl-NL", { dateStyle: "short", timeStyle: "short" });
46
+ return (_jsxs(Badge, { variant: "outline", className: "gap-1 text-emerald-700 border-emerald-200 bg-emerald-50", children: [_jsx(CheckCircle2, { className: "h-3 w-3" }), "Gesynchroniseerd \u00B7 ", when] }));
47
+ }
48
+ return null;
49
+ }
@@ -0,0 +1,8 @@
1
+ export declare function CustomerPickerDialog({ priceGroupId, open, onOpenChange, value, onChange, }: {
2
+ priceGroupId?: string;
3
+ open: boolean;
4
+ onOpenChange: (v: boolean) => void;
5
+ value?: string[];
6
+ onChange?: (keys: string[]) => void;
7
+ }): import("react").JSX.Element;
8
+ //# sourceMappingURL=CustomerPickerDialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CustomerPickerDialog.d.ts","sourceRoot":"","sources":["../../../../src/components/klanten/pricegroups/CustomerPickerDialog.tsx"],"names":[],"mappings":"AAaA,wBAAgB,oBAAoB,CAAC,EACnC,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,GAClD,EAAE;IACD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CACrC,+BA+HA"}
@@ -0,0 +1,75 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import { useServerFn } from "@tanstack/react-start";
5
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, Button, Input, Checkbox, Badge, } from "@flowselections/core";
6
+ import { Search } from "lucide-react";
7
+ import { listExternalCustomers } from "../../../lib/external-customers.functions";
8
+ import { setPriceGroupMembers, getCustomerPriceGroup, } from "../../../lib/price-groups-local.functions";
9
+ export function CustomerPickerDialog({ priceGroupId, open, onOpenChange, value, onChange, }) {
10
+ const qc = useQueryClient();
11
+ const listCustomers = useServerFn(listExternalCustomers);
12
+ const getGroup = useServerFn(getCustomerPriceGroup);
13
+ const setMembers = useServerFn(setPriceGroupMembers);
14
+ const [search, setSearch] = useState("");
15
+ const [selected, setSelected] = useState(new Set());
16
+ const isControlled = onChange !== undefined;
17
+ const { data: customersData, isLoading } = useQuery({
18
+ queryKey: ["external-customers-picker"],
19
+ queryFn: () => listCustomers({ data: { search: "", status: "all", limit: 2000 } }),
20
+ enabled: open,
21
+ });
22
+ const { data: group } = useQuery({
23
+ queryKey: ["customer-price-group", priceGroupId],
24
+ queryFn: () => getGroup({ data: { id: priceGroupId } }),
25
+ enabled: open && !!priceGroupId && !isControlled,
26
+ });
27
+ useEffect(() => {
28
+ if (!open)
29
+ return;
30
+ if (isControlled) {
31
+ setSelected(new Set(value ?? []));
32
+ }
33
+ else if (group?.customerKeys) {
34
+ setSelected(new Set(group.customerKeys));
35
+ }
36
+ }, [group, open, isControlled, value]);
37
+ const customers = customersData?.customers ?? [];
38
+ const filtered = useMemo(() => {
39
+ const q = search.trim().toLowerCase();
40
+ if (!q)
41
+ return customers;
42
+ return customers.filter((c) => [c.display_name, c.commercial_name, c.city, c.email, c.vat_number]
43
+ .filter(Boolean).some((v) => String(v).toLowerCase().includes(q)));
44
+ }, [customers, search]);
45
+ const toggle = (key) => {
46
+ setSelected((prev) => {
47
+ const n = new Set(prev);
48
+ if (n.has(key))
49
+ n.delete(key);
50
+ else
51
+ n.add(key);
52
+ return n;
53
+ });
54
+ };
55
+ const saveMut = useMutation({
56
+ mutationFn: () => setMembers({ data: { id: priceGroupId, customerKeys: Array.from(selected) } }),
57
+ onSuccess: () => {
58
+ qc.invalidateQueries({ queryKey: ["customer-price-groups"] });
59
+ qc.invalidateQueries({ queryKey: ["customer-price-group", priceGroupId] });
60
+ onOpenChange(false);
61
+ },
62
+ });
63
+ const handleSave = () => {
64
+ if (isControlled) {
65
+ onChange(Array.from(selected));
66
+ onOpenChange(false);
67
+ return;
68
+ }
69
+ saveMut.mutate();
70
+ };
71
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-[720px]", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Klanten toewijzen" }) }), _jsxs("div", { className: "relative", 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, plaats of BTW-nummer\u2026", value: search, onChange: (e) => setSearch(e.target.value), className: "pl-9" })] }), _jsxs("div", { className: "flex items-center justify-between text-xs text-muted-foreground", children: [_jsxs("span", { children: [filtered.length, " klanten"] }), _jsxs("span", { children: [_jsx("strong", { className: "text-foreground", children: selected.size }), " geselecteerd"] })] }), _jsx("div", { className: "max-h-[380px] overflow-y-auto border rounded-md divide-y", children: isLoading ? (_jsx("div", { className: "p-6 text-center text-sm text-muted-foreground", children: "Laden\u2026" })) : filtered.length === 0 ? (_jsx("div", { className: "p-6 text-center text-sm text-muted-foreground", children: "Geen klanten gevonden." })) : filtered.map((c) => {
72
+ const checked = selected.has(c.group_key);
73
+ return (_jsxs("label", { className: "flex items-center gap-3 p-2.5 hover:bg-muted/50 cursor-pointer", children: [_jsx(Checkbox, { checked: checked, onCheckedChange: () => toggle(c.group_key) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "text-sm font-medium truncate", children: c.display_name }), _jsx("div", { className: "text-xs text-muted-foreground truncate", children: [c.city, c.country].filter(Boolean).join(" · ") || "—" })] }), _jsx("div", { className: "flex gap-1", children: c.providers.slice(0, 3).map((p) => (_jsx(Badge, { variant: "secondary", className: "text-[10px]", children: p }, p))) })] }, c.group_key));
74
+ }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", onClick: () => onOpenChange(false), children: "Annuleren" }), _jsx(Button, { onClick: handleSave, disabled: !isControlled && saveMut.isPending, children: !isControlled && saveMut.isPending ? "Opslaan…" : "Selectie opslaan" })] })] }) }));
75
+ }
@@ -0,0 +1,4 @@
1
+ export declare function PriceGroupFormPage({ editingId }: {
2
+ editingId?: string | null;
3
+ }): import("react").JSX.Element;
4
+ //# sourceMappingURL=PriceGroupFormPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PriceGroupFormPage.d.ts","sourceRoot":"","sources":["../../../../src/components/klanten/pricegroups/PriceGroupFormPage.tsx"],"names":[],"mappings":"AAkLA,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,+BA0rB9E"}
@@ -0,0 +1,269 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import { useServerFn } from "@tanstack/react-start";
5
+ import { useNavigate } from "@tanstack/react-router";
6
+ import { Button, Input, Label, Separator, Card, CardContent, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Checkbox, } from "@flowselections/core";
7
+ import { ArrowRight, Check, ChevronDown, PencilLine, Calculator, Users, Globe2, Circle, CheckCircle2, Info, ChevronLeft, ChevronRight, ImageIcon } from "lucide-react";
8
+ import { BackLink } from "../BackLink";
9
+ import { createCustomerPriceGroup, updateCustomerPriceGroup, getCustomerPriceGroup, setPriceGroupMembers, } from "../../../lib/price-groups-local.functions";
10
+ import { computePrice } from "../../../lib/pricing";
11
+ import { listTradeItemsForPreview } from "../../../lib/floriday-trade-items.functions";
12
+ import { ProductPickerDialog } from "./ProductPickerDialog";
13
+ import { CustomerPickerDialog } from "./CustomerPickerDialog";
14
+ import { UserPlus, Users2 } from "lucide-react";
15
+ import { listExternalCustomers } from "../../../lib/external-customers.functions";
16
+ import { Badge } from "@flowselections/core";
17
+ import { toast } from "sonner";
18
+ const emptyInput = {
19
+ name: "",
20
+ currency: "EUR",
21
+ pricing_mode: "manual",
22
+ group_type: "customer_specific",
23
+ price_scope: "incl_delivery",
24
+ include_stickers: false,
25
+ unit: "per_fust",
26
+ calc_type: "discount",
27
+ calc_value_type: "fixed",
28
+ calc_value: 0,
29
+ handling_fee: 0,
30
+ distribute_over: "ladingdrager",
31
+ rounding: "none",
32
+ rounding_step: 0.05,
33
+ is_active: true,
34
+ };
35
+ const STEP_TITLES = [
36
+ "Prijsgroep",
37
+ "Levering, diensten en afname hoeveelheid",
38
+ "Prijsberekening",
39
+ "Doelgroep",
40
+ ];
41
+ function OptionCard({ selected, onClick, icon, title, description, }) {
42
+ return (_jsxs("button", { type: "button", onClick: onClick, className: "text-left rounded-lg border p-4 transition flex gap-3 items-start " +
43
+ (selected
44
+ ? "border-primary ring-2 ring-primary/30 bg-primary/5"
45
+ : "hover:border-primary/50 hover:bg-muted/40"), children: [_jsx("div", { className: "mt-0.5 " + (selected ? "text-primary" : "text-muted-foreground"), children: icon }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "font-medium text-sm", children: title }), _jsx("div", { className: "text-xs text-muted-foreground mt-0.5", children: description })] })] }));
46
+ }
47
+ function RadioBanner({ selected, onClick, title, }) {
48
+ return (_jsxs("button", { type: "button", onClick: onClick, className: "w-full text-left rounded-lg border px-4 py-3 flex items-center gap-3 transition " +
49
+ (selected
50
+ ? "bg-primary text-primary-foreground border-primary"
51
+ : "bg-background hover:border-primary/50 hover:bg-muted/40"), children: [selected
52
+ ? _jsx(CheckCircle2, { className: "h-5 w-5 shrink-0" })
53
+ : _jsx(Circle, { className: "h-5 w-5 shrink-0 text-muted-foreground" }), _jsx("span", { className: "font-medium text-sm", children: title })] }));
54
+ }
55
+ function BulletList({ items }) {
56
+ return (_jsx("ul", { className: "space-y-1.5 pl-1", children: items.map((t, i) => (_jsxs("li", { className: "flex items-start gap-2 text-sm", children: [_jsx(Check, { className: "h-4 w-4 mt-0.5 text-primary shrink-0" }), _jsx("span", { children: t })] }, i))) }));
57
+ }
58
+ function StepSection({ index, title, subtitle, open, completed, disabled, onToggle, children, }) {
59
+ return (_jsxs(Card, { className: open ? "ring-1 ring-primary/40" : "", children: [_jsxs("button", { type: "button", onClick: disabled ? undefined : onToggle, disabled: disabled, className: "w-full flex items-center gap-3 px-6 py-4 text-left " +
60
+ (disabled ? "cursor-not-allowed opacity-60" : "hover:bg-muted/40"), children: [_jsx("div", { className: "h-8 w-8 rounded-full flex items-center justify-center text-sm font-medium shrink-0 " +
61
+ (completed
62
+ ? "bg-primary/15 text-primary border border-primary/30"
63
+ : open
64
+ ? "bg-primary text-primary-foreground"
65
+ : "bg-muted text-muted-foreground border"), children: completed && !open ? _jsx(Check, { className: "h-4 w-4" }) : index + 1 }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "text-base " + (open ? "font-semibold" : "font-medium"), children: title }), !open && subtitle && (_jsx("div", { className: "text-xs text-muted-foreground truncate mt-0.5", children: subtitle }))] }), !disabled && (_jsx(ChevronDown, { className: "h-4 w-4 text-muted-foreground transition-transform " + (open ? "rotate-180" : "") }))] }), open && (_jsx(CardContent, { className: "pt-0 pb-6 px-6 space-y-4 border-t", children: _jsx("div", { className: "pt-6", children: children }) }))] }));
66
+ }
67
+ export function PriceGroupFormPage({ editingId }) {
68
+ const qc = useQueryClient();
69
+ const navigate = useNavigate();
70
+ const createFn = useServerFn(createCustomerPriceGroup);
71
+ const updateFn = useServerFn(updateCustomerPriceGroup);
72
+ const getFn = useServerFn(getCustomerPriceGroup);
73
+ const setMembersFn = useServerFn(setPriceGroupMembers);
74
+ const listCustomersFn = useServerFn(listExternalCustomers);
75
+ const [openStep, setOpenStep] = useState(0);
76
+ const [maxReached, setMaxReached] = useState(0);
77
+ const [form, setForm] = useState(emptyInput);
78
+ const [error, setError] = useState(null);
79
+ const [basePrice, setBasePrice] = useState(10);
80
+ const [selectedItemId, setSelectedItemId] = useState(null);
81
+ const [pickerOpen, setPickerOpen] = useState(false);
82
+ const [customerPickerOpen, setCustomerPickerOpen] = useState(false);
83
+ const [selectedCustomerKeys, setSelectedCustomerKeys] = useState([]);
84
+ const listTradeItemsFn = useServerFn(listTradeItemsForPreview);
85
+ const { data: tradeItemsData } = useQuery({
86
+ queryKey: ["trade-items-preview"],
87
+ queryFn: () => listTradeItemsFn(),
88
+ enabled: form.pricing_mode === "calculated",
89
+ staleTime: 5 * 60000,
90
+ });
91
+ const items = tradeItemsData?.items ?? [];
92
+ const selectedItem = useMemo(() => {
93
+ if (items.length === 0)
94
+ return null;
95
+ return items.find((i) => i.id === selectedItemId) ?? items[0];
96
+ }, [items, selectedItemId]);
97
+ useEffect(() => {
98
+ if (!selectedItemId && items[0])
99
+ setSelectedItemId(items[0].id);
100
+ }, [items, selectedItemId]);
101
+ const cycleItem = (dir) => {
102
+ if (items.length === 0 || !selectedItem)
103
+ return;
104
+ const idx = items.findIndex((i) => i.id === selectedItem.id);
105
+ const next = (idx + dir + items.length) % items.length;
106
+ setSelectedItemId(items[next].id);
107
+ };
108
+ const { data: existing, isLoading: loadingExisting } = useQuery({
109
+ queryKey: ["customer-price-group", editingId],
110
+ queryFn: () => getFn({ data: { id: editingId } }),
111
+ enabled: !!editingId,
112
+ });
113
+ useEffect(() => {
114
+ if (editingId && existing) {
115
+ const g = existing.group;
116
+ setForm({
117
+ name: g.name, currency: g.currency, pricing_mode: g.pricing_mode,
118
+ group_type: g.group_type ?? "customer_specific",
119
+ price_scope: g.price_scope, include_stickers: g.include_stickers,
120
+ unit: g.unit, calc_type: g.calc_type, calc_value_type: g.calc_value_type,
121
+ calc_value: g.calc_value, handling_fee: g.handling_fee,
122
+ distribute_over: g.distribute_over ?? "ladingdrager",
123
+ rounding: g.rounding ?? "none",
124
+ rounding_step: g.rounding_step != null ? Number(g.rounding_step) : 0.05,
125
+ is_active: g.is_active,
126
+ });
127
+ setSelectedCustomerKeys(existing.customerKeys ?? []);
128
+ setMaxReached(STEP_TITLES.length - 1);
129
+ }
130
+ }, [editingId, existing]);
131
+ const { data: allCustomersData } = useQuery({
132
+ queryKey: ["external-customers-picker"],
133
+ queryFn: () => listCustomersFn({ data: { search: "", status: "all", limit: 2000 } }),
134
+ });
135
+ const customersByKey = useMemo(() => {
136
+ const map = new Map();
137
+ for (const c of allCustomersData?.customers ?? [])
138
+ map.set(c.group_key, c);
139
+ return map;
140
+ }, [allCustomersData]);
141
+ const patch = (p) => setForm((f) => ({ ...f, ...p }));
142
+ const preview = useMemo(() => {
143
+ const calc = {
144
+ pricing_mode: form.pricing_mode,
145
+ calc_type: form.calc_type ?? null,
146
+ calc_value_type: form.calc_value_type ?? null,
147
+ calc_value: form.calc_value ?? null,
148
+ handling_fee: form.handling_fee ?? null,
149
+ distribute_over: form.distribute_over ?? null,
150
+ rounding: form.rounding,
151
+ rounding_step: form.rounding_step ?? 0.05,
152
+ };
153
+ return computePrice(basePrice, calc, {
154
+ unitsPerFust: selectedItem?.units_per_fust ?? 1,
155
+ packagesPerLayer: selectedItem?.packages_per_layer ?? 1,
156
+ layersPerCarrier: selectedItem?.layers_per_carrier ?? 1,
157
+ });
158
+ }, [form, basePrice, selectedItem]);
159
+ const handlingSuffix = useMemo(() => {
160
+ if (!selectedItem)
161
+ return "";
162
+ const upf = selectedItem.units_per_fust;
163
+ const ppl = selectedItem.packages_per_layer;
164
+ const lpc = selectedItem.layers_per_carrier;
165
+ switch (form.distribute_over) {
166
+ case "fust": return `/${upf}`;
167
+ case "laag": return `/(${upf}×${ppl})`;
168
+ case "ladingdrager": return `/(${upf}×${ppl}×${lpc})`;
169
+ default: return "";
170
+ }
171
+ }, [selectedItem, form.distribute_over]);
172
+ const goBack = () => navigate({ to: "/klanten/prijsgroepen" });
173
+ const saveMut = useMutation({
174
+ mutationFn: async () => {
175
+ if (form.group_type === "customer_specific" && selectedCustomerKeys.length === 0) {
176
+ throw new Error("Selecteer minimaal één organisatie in stap 4 voordat je een klantspecifieke prijsgroep opslaat.");
177
+ }
178
+ const row = editingId
179
+ ? await updateFn({ data: { id: editingId, input: form } })
180
+ : await createFn({ data: form });
181
+ const id = row?.id ?? editingId;
182
+ if (id) {
183
+ const keys = form.group_type === "customer_specific" ? selectedCustomerKeys : [];
184
+ await setMembersFn({ data: { id, customerKeys: keys } });
185
+ }
186
+ return row;
187
+ },
188
+ onSuccess: () => {
189
+ qc.invalidateQueries({ queryKey: ["customer-price-groups"] });
190
+ qc.invalidateQueries({ queryKey: ["customer-price-group", editingId] });
191
+ goBack();
192
+ },
193
+ onError: (e) => {
194
+ const msg = e?.message ?? "Opslaan mislukt";
195
+ setError(msg);
196
+ toast.error(msg);
197
+ },
198
+ });
199
+ const canProceed = (s) => (s === 0 ? form.name.trim().length > 0 : true);
200
+ const goToStep = (i) => {
201
+ if (i > maxReached)
202
+ return;
203
+ setOpenStep((cur) => (cur === i ? -1 : i));
204
+ };
205
+ const nextStep = (i) => {
206
+ if (!canProceed(i))
207
+ return;
208
+ // Van stap 2 naar stap 4 wanneer prijzen handmatig zijn (skip prijsberekening).
209
+ let target = i + 1;
210
+ if (i === 1 && form.pricing_mode === "manual")
211
+ target = 3;
212
+ // Skip stap 4 (Doelgroep) voor algemene prijsgroepen.
213
+ if (target === 3 && form.group_type !== "customer_specific") {
214
+ setMaxReached((m) => Math.max(m, 3));
215
+ setOpenStep(-1);
216
+ return;
217
+ }
218
+ const next = Math.min(target, STEP_TITLES.length - 1);
219
+ setMaxReached((m) => Math.max(m, next));
220
+ setOpenStep(next);
221
+ };
222
+ const summaries = [
223
+ form.name
224
+ ? `${form.name} · ${form.pricing_mode === "manual" ? "Handmatig" : "Berekend"} · ${form.group_type === "general" ? "Algemeen" : "Klantspecifiek"} · ${form.currency}`
225
+ : undefined,
226
+ `${form.price_scope === "incl_delivery" ? "Incl. levering" : "Excl. levering"} · ${form.unit === "per_fust" ? "Per fust" : form.unit === "per_laag" ? "Per laag" : "Per ladingdrager"}${form.include_stickers ? " · incl. stickers" : ""}`,
227
+ form.pricing_mode !== "calculated"
228
+ ? "Handmatige prijzen — niet van toepassing"
229
+ : (() => {
230
+ const typ = form.calc_type === "surcharge" ? "Opslag" : "Korting";
231
+ const val = form.calc_value_type === "percentage"
232
+ ? `${form.calc_value ?? 0}%`
233
+ : `€ ${(form.calc_value ?? 0).toFixed(3)}`;
234
+ const rnd = form.rounding === "none"
235
+ ? "niet afronden"
236
+ : `${form.rounding === "up" ? "omhoog" : form.rounding === "down" ? "omlaag" : "dichtstbijzijnde"} op € ${(form.rounding_step ?? 0.05).toFixed(2)}`;
237
+ return `${typ} ${val} · afhandelingskosten € ${(form.handling_fee ?? 0).toFixed(2)} · ${rnd}`;
238
+ })(),
239
+ form.group_type !== "customer_specific"
240
+ ? "Algemene prijsgroep — geen doelgroep"
241
+ : selectedCustomerKeys.length === 0
242
+ ? "Nog geen klanten"
243
+ : `${selectedCustomerKeys.length} klant${selectedCustomerKeys.length === 1 ? "" : "en"}`,
244
+ ];
245
+ if (editingId && loadingExisting) {
246
+ return _jsx("div", { className: "p-6 text-sm text-muted-foreground", children: "Laden\u2026" });
247
+ }
248
+ return (_jsxs("main", { className: "min-h-screen bg-muted/30 pb-28", children: [_jsxs("div", { className: "max-w-6xl mx-auto px-6 pt-6 space-y-4", children: [_jsx(BackLink, { to: "/klanten/prijsgroepen", label: "Terug naar prijsgroepen" }), _jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold", children: editingId ? "Prijsgroep bewerken" : "Nieuwe prijsgroep" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Stel prijsafspraken en berekening in. Klanten koppel je op het overzicht." })] }), _jsxs("div", { className: "grid gap-4 lg:grid-cols-[1fr_340px] items-start", children: [_jsxs("div", { className: "space-y-4 min-w-0", children: [_jsx(StepSection, { index: 0, title: STEP_TITLES[0], subtitle: summaries[0], open: openStep === 0, completed: maxReached > 0, disabled: false, onToggle: () => goToStep(0), children: _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx(Label, { children: "Naam van de prijsgroep" }), _jsx("div", { className: "text-xs text-muted-foreground mb-1.5", children: "Alleen zichtbaar voor jou." }), _jsx(Input, { value: form.name, onChange: (e) => patch({ name: e.target.value }), placeholder: "Bijvoorbeeld: trouwe klanten" })] }), _jsxs("div", { className: "max-w-sm", children: [_jsx(Label, { children: "Valuta" }), _jsxs(Select, { value: form.currency, onValueChange: (v) => patch({ currency: v }), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "EUR", children: "Euro" }), _jsx(SelectItem, { value: "USD", children: "Amerikaanse dollar" })] })] })] }), _jsxs("div", { children: [_jsx(Label, { children: "Prijsbepaling" }), _jsxs("div", { className: "grid sm:grid-cols-2 gap-3 mt-2", children: [_jsx(OptionCard, { selected: form.pricing_mode === "manual", onClick: () => patch({ pricing_mode: "manual" }), icon: _jsx(PencilLine, { className: "h-5 w-5" }), title: "Prijzen handmatig invoeren", description: "Geef per prijsgroep handmatig een prijs in bij het aanbieden van artikelen." }), _jsx(OptionCard, { selected: form.pricing_mode === "calculated", onClick: () => patch({ pricing_mode: "calculated" }), icon: _jsx(Calculator, { className: "h-5 w-5" }), title: "Prijzen berekenen", description: "Bereken prijzen automatisch op basis van een basisprijs met een door jou samen te stellen berekening." })] })] }), _jsxs("div", { children: [_jsx(Label, { children: "Type prijsgroep" }), _jsxs("div", { className: "grid sm:grid-cols-2 gap-3 mt-2", children: [_jsx(OptionCard, { selected: form.group_type === "general", onClick: () => patch({ group_type: "general" }), icon: _jsx(Globe2, { className: "h-5 w-5" }), title: "Algemene prijsgroep", description: "Zichtbaar voor alle klanten (alleen voor partijprijzen)." }), _jsx(OptionCard, { selected: form.group_type === "customer_specific", onClick: () => patch({ group_type: "customer_specific" }), icon: _jsx(Users, { className: "h-5 w-5" }), title: "Klantspecifieke prijsgroep", description: "Zichtbaar voor klanten die zijn ingedeeld in deze prijsgroep." })] })] }), _jsx("div", { className: "pt-2 flex justify-end", children: _jsxs(Button, { onClick: () => nextStep(0), disabled: !canProceed(0), children: ["Volgende stap ", _jsx(ArrowRight, { className: "h-4 w-4 ml-1" })] }) })] }) }), _jsx(StepSection, { index: 1, title: STEP_TITLES[1], subtitle: summaries[1], open: openStep === 1, completed: maxReached > 1, disabled: maxReached < 1, onToggle: () => goToStep(1), children: _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-3", children: [_jsx(RadioBanner, { selected: form.price_scope === "incl_delivery", onClick: () => patch({ price_scope: "incl_delivery" }), title: "Prijzen inclusief levering en transport" }), form.price_scope === "incl_delivery" && (_jsx(BulletList, { items: [
249
+ "Prijzen zichtbaar voor klanten die direct inkopen op Floriday",
250
+ "Prijzen zijn inclusief levering en transport, ongeacht het bestelvolume",
251
+ ] })), _jsx(RadioBanner, { selected: form.price_scope === "excl_delivery", onClick: () => patch({ price_scope: "excl_delivery" }), title: "Prijzen exclusief levering en transport" }), form.price_scope === "excl_delivery" && (_jsx(BulletList, { items: [
252
+ "Prijzen zichtbaar voor klanten die direct inkopen op Floriday",
253
+ "De ingestelde transport- en afleverkosten in de levervoorwaarden worden in rekening gebracht",
254
+ ] }))] }), _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) => {
255
+ const c = customersByKey.get(k);
256
+ 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].filter(Boolean).join(" · ") || "—" : "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));
257
+ }) })] })), _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
258
+ ? _jsx("img", { src: selectedItem.image_url, alt: "", className: "h-full w-full object-contain" })
259
+ : _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
260
+ ? [selectedItem.packaging_code,
261
+ `${selectedItem.units_per_fust}×${selectedItem.packages_per_layer}×${selectedItem.layers_per_carrier}`,
262
+ selectedItem.carrier_code]
263
+ .filter(Boolean).join(" - ")
264
+ : "Wacht op Floriday-cache" })] })] }), _jsxs("div", { children: [_jsx(Label, { className: "text-xs", children: "Basisprijs" }), _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: basePrice, onChange: (e) => setBasePrice(Number(e.target.value)), className: "pl-7" })] })] }), _jsxs("div", { children: [_jsx("div", { className: "flex justify-end text-xs text-muted-foreground mb-1", children: "Prijzen" }), _jsxs("div", { className: "space-y-1.5 text-sm tabular-nums border-t pt-2", children: [_jsxs("div", { className: "flex justify-between", children: [_jsx("span", { className: "text-muted-foreground", children: "Basisprijs" }), _jsxs("span", { children: ["\u20AC ", basePrice.toFixed(2)] })] }), Math.abs(preview.discountAmount) > 0 && (_jsxs("div", { className: "flex justify-between", children: [_jsxs("span", { className: "text-muted-foreground", children: [form.calc_type === "surcharge" ? "Opslag" : "Korting", " ", form.calc_value_type === "percentage"
265
+ ? `${(form.calc_value ?? 0).toLocaleString("nl-NL")}%`
266
+ : `€ ${(form.calc_value ?? 0).toFixed(3)}`] }), _jsxs("span", { children: [preview.discountAmount < 0 ? "− " : "", "\u20AC ", Math.abs(preview.discountAmount).toFixed(2)] })] })), preview.handlingPerUnit > 0 && (_jsxs("div", { className: "flex justify-between items-baseline gap-2", children: [_jsxs("span", { className: "text-muted-foreground truncate", children: ["Afhandelingskosten (optioneel)", " ", _jsxs("span", { className: "italic text-muted-foreground/80", children: ["\u20AC ", (form.handling_fee ?? 0).toFixed(2).replace(".", ","), handlingSuffix] })] }), _jsxs("span", { children: ["\u20AC ", preview.handlingPerUnit.toFixed(3)] })] })), _jsxs("div", { className: "flex justify-between border-t pt-1.5", children: [_jsx("span", { className: form.rounding === "none" ? "font-medium" : "text-muted-foreground", children: "Berekende prijs" }), _jsxs("span", { className: form.rounding === "none" ? "font-medium" : "", children: ["\u20AC ", preview.adjusted.toFixed(3)] })] }), form.rounding !== "none" && (_jsxs("div", { className: "flex justify-between font-medium border-t pt-1.5", children: [_jsx("span", { children: "Afgeronde prijs" }), _jsxs("span", { children: ["\u20AC ", preview.rounded.toFixed(2)] })] }))] })] })] }) }), _jsx(ProductPickerDialog, { open: pickerOpen, onOpenChange: setPickerOpen, items: items, onSelect: (i) => setSelectedItemId(i.id) })] }))] })] }), _jsx("div", { className: "fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur border-t z-10", children: _jsxs("div", { className: "max-w-6xl mx-auto px-6 py-3 flex items-center justify-between", children: [_jsx(Button, { variant: "ghost", onClick: goBack, children: "Annuleren" }), _jsx(Button, { onClick: () => saveMut.mutate(), disabled: saveMut.isPending ||
267
+ !form.name.trim() ||
268
+ (form.group_type === "customer_specific" && selectedCustomerKeys.length === 0), children: saveMut.isPending ? "Opslaan…" : "Opslaan" })] }) })] }));
269
+ }
@@ -0,0 +1,2 @@
1
+ export declare function PriceGroupsPage(): import("react").JSX.Element;
2
+ //# sourceMappingURL=PriceGroupsPage.d.ts.map