@flowselections/actie-mail-kwekers-erp-mathot 1.0.0

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 (75) hide show
  1. package/dist-lib/_core-safelist.d.ts +2 -0
  2. package/dist-lib/_core-safelist.d.ts.map +1 -0
  3. package/dist-lib/_core-safelist.js +15 -0
  4. package/dist-lib/components/AanbiedingsmailtjePage.d.ts +2 -0
  5. package/dist-lib/components/AanbiedingsmailtjePage.d.ts.map +1 -0
  6. package/dist-lib/components/AanbiedingsmailtjePage.js +239 -0
  7. package/dist-lib/components/ActiePage.d.ts +2 -0
  8. package/dist-lib/components/ActiePage.d.ts.map +1 -0
  9. package/dist-lib/components/ActiePage.js +173 -0
  10. package/dist-lib/components/CampagneDetail.d.ts +2 -0
  11. package/dist-lib/components/CampagneDetail.d.ts.map +1 -0
  12. package/dist-lib/components/CampagneDetail.js +61 -0
  13. package/dist-lib/components/CampagneOverzicht.d.ts +2 -0
  14. package/dist-lib/components/CampagneOverzicht.d.ts.map +1 -0
  15. package/dist-lib/components/CampagneOverzicht.js +16 -0
  16. package/dist-lib/components/DateRangePicker.d.ts +20 -0
  17. package/dist-lib/components/DateRangePicker.d.ts.map +1 -0
  18. package/dist-lib/components/DateRangePicker.js +140 -0
  19. package/dist-lib/components/RapportagePage.d.ts +2 -0
  20. package/dist-lib/components/RapportagePage.d.ts.map +1 -0
  21. package/dist-lib/components/RapportagePage.js +60 -0
  22. package/dist-lib/components/settings/AfzenderSettingsCard.d.ts +2 -0
  23. package/dist-lib/components/settings/AfzenderSettingsCard.d.ts.map +1 -0
  24. package/dist-lib/components/settings/AfzenderSettingsCard.js +30 -0
  25. package/dist-lib/components/settings/HuisstijlSettingsCard.d.ts +2 -0
  26. package/dist-lib/components/settings/HuisstijlSettingsCard.d.ts.map +1 -0
  27. package/dist-lib/components/settings/HuisstijlSettingsCard.js +30 -0
  28. package/dist-lib/components/settings/StickerSettingsCard.d.ts +2 -0
  29. package/dist-lib/components/settings/StickerSettingsCard.d.ts.map +1 -0
  30. package/dist-lib/components/settings/StickerSettingsCard.js +41 -0
  31. package/dist-lib/components/settings/TestmailCard.d.ts +2 -0
  32. package/dist-lib/components/settings/TestmailCard.d.ts.map +1 -0
  33. package/dist-lib/components/settings/TestmailCard.js +21 -0
  34. package/dist-lib/components/settings/TrackingSettingsCard.d.ts +2 -0
  35. package/dist-lib/components/settings/TrackingSettingsCard.d.ts.map +1 -0
  36. package/dist-lib/components/settings/TrackingSettingsCard.js +30 -0
  37. package/dist-lib/components/settings/UitsluitingenSettingsCard.d.ts +2 -0
  38. package/dist-lib/components/settings/UitsluitingenSettingsCard.d.ts.map +1 -0
  39. package/dist-lib/components/settings/UitsluitingenSettingsCard.js +41 -0
  40. package/dist-lib/index.d.ts +17 -0
  41. package/dist-lib/index.d.ts.map +1 -0
  42. package/dist-lib/index.js +34 -0
  43. package/dist-lib/integrations/supabase/auth-attacher.d.ts +2 -0
  44. package/dist-lib/integrations/supabase/auth-attacher.d.ts.map +1 -0
  45. package/dist-lib/integrations/supabase/auth-attacher.js +12 -0
  46. package/dist-lib/integrations/supabase/auth-middleware.d.ts +2555 -0
  47. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -0
  48. package/dist-lib/integrations/supabase/auth-middleware.js +52 -0
  49. package/dist-lib/integrations/supabase/client.d.ts +2551 -0
  50. package/dist-lib/integrations/supabase/client.d.ts.map +1 -0
  51. package/dist-lib/integrations/supabase/client.js +13 -0
  52. package/dist-lib/integrations/supabase/client.server.d.ts +2551 -0
  53. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -0
  54. package/dist-lib/integrations/supabase/client.server.js +30 -0
  55. package/dist-lib/integrations/supabase/types.d.ts +2685 -0
  56. package/dist-lib/integrations/supabase/types.d.ts.map +1 -0
  57. package/dist-lib/integrations/supabase/types.js +8 -0
  58. package/dist-lib/lib/campaigns.functions.d.ts +12881 -0
  59. package/dist-lib/lib/campaigns.functions.d.ts.map +1 -0
  60. package/dist-lib/lib/campaigns.functions.js +358 -0
  61. package/dist-lib/lib/public-offer.functions.d.ts +68 -0
  62. package/dist-lib/lib/public-offer.functions.d.ts.map +1 -0
  63. package/dist-lib/lib/public-offer.functions.js +116 -0
  64. package/dist-lib/lib/stats.functions.d.ts +5180 -0
  65. package/dist-lib/lib/stats.functions.d.ts.map +1 -0
  66. package/dist-lib/lib/stats.functions.js +63 -0
  67. package/dist-lib/lib/utils.d.ts +3 -0
  68. package/dist-lib/lib/utils.d.ts.map +1 -0
  69. package/dist-lib/lib/utils.js +5 -0
  70. package/dist-lib/lib/validationSchemas.d.ts +15 -0
  71. package/dist-lib/lib/validationSchemas.d.ts.map +1 -0
  72. package/dist-lib/lib/validationSchemas.js +25 -0
  73. package/dist-lib/styles.css +1 -0
  74. package/package.json +76 -0
  75. package/public/flowselections-assets/template-module/README.md +15 -0
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=_core-safelist.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_core-safelist.d.ts","sourceRoot":"","sources":["../src/_core-safelist.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,CAAC"}
@@ -0,0 +1,15 @@
1
+ // Forceert Tailwind classes die uit @flowselections/core komen maar niet
2
+ // voorkomen in de eigen broncode van deze module.
3
+ // Zonder dit bestand worden deze classes weggeoptimaliseerd op de
4
+ // Lovable productie build.
5
+ const _safelist = [
6
+ // Layout — Sidebar en shell structuur
7
+ 'h-full', 'flex-col', 'flex-1', 'min-h-screen', 'ml-64', 'pt-16',
8
+ 'items-start', 'items-center', 'justify-between', 'border-t',
9
+ 'w-64', 'h-screen', 'h-20', 'overflow-y-auto',
10
+ // Grid — InstellingenPage
11
+ 'grid', 'lg:grid-cols-2', 'gap-6', 'p-6', 'space-y-6',
12
+ // Toggle/Switch — aan/uit zichtbaarheid
13
+ 'data-[state=checked]:bg-primary', 'data-[state=unchecked]:bg-input',
14
+ ];
15
+ export {};
@@ -0,0 +1,2 @@
1
+ export declare function AanbiedingsmailtjePage(): import("react").JSX.Element;
2
+ //# sourceMappingURL=AanbiedingsmailtjePage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AanbiedingsmailtjePage.d.ts","sourceRoot":"","sources":["../../src/components/AanbiedingsmailtjePage.tsx"],"names":[],"mappings":"AAqCA,wBAAgB,sBAAsB,gCA+drC"}
@@ -0,0 +1,239 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect, useMemo } from "react";
3
+ import { useAuth, supabase, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Input, Label, Textarea, Badge, Separator, Skeleton, Switch, toast, } from "@flowselections/core";
4
+ import { useServerFn } from "@tanstack/react-start";
5
+ import { useNavigate } from "@tanstack/react-router";
6
+ import { Mail, Send, Search, X, Package, Users, Percent, Tag, List, BarChart3 } from "lucide-react";
7
+ import { createCampaign, sendCampaign } from "../lib/campaigns.functions";
8
+ export function AanbiedingsmailtjePage() {
9
+ const { user } = useAuth();
10
+ const navigate = useNavigate();
11
+ const createFn = useServerFn(createCampaign);
12
+ const sendFn = useServerFn(sendCampaign);
13
+ const [products, setProducts] = useState([]);
14
+ const [customers, setCustomers] = useState([]);
15
+ const [loading, setLoading] = useState(true);
16
+ const [busy, setBusy] = useState(false);
17
+ const [name, setName] = useState("Nieuwe aanbieding");
18
+ const [subject, setSubject] = useState("🌷 Speciale aanbieding voor u!");
19
+ const [fromName, setFromName] = useState("");
20
+ const [fromEmail, setFromEmail] = useState(user?.email ?? "");
21
+ const [intro, setIntro] = useState("Wij hebben een mooie aanbieding voor u geselecteerd.");
22
+ const [selProducts, setSelProducts] = useState([]);
23
+ const [selCustomers, setSelCustomers] = useState(new Set());
24
+ const [showCustomerClasses, setShowCustomerClasses] = useState(true);
25
+ const [productSearch, setProductSearch] = useState("");
26
+ const [customerSearch, setCustomerSearch] = useState("");
27
+ const [bulkPrice, setBulkPrice] = useState("");
28
+ const BULK_STICKER_PREF_KEY = "aanbiedingsmailtje:bulkStickerOn";
29
+ const [bulkStickerOn, setBulkStickerOn] = useState(() => {
30
+ if (typeof window === "undefined")
31
+ return true;
32
+ const v = window.localStorage.getItem(BULK_STICKER_PREF_KEY);
33
+ return v === null ? true : v === "true";
34
+ });
35
+ useEffect(() => {
36
+ if (typeof window !== "undefined") {
37
+ window.localStorage.setItem(BULK_STICKER_PREF_KEY, String(bulkStickerOn));
38
+ }
39
+ }, [bulkStickerOn]);
40
+ useEffect(() => {
41
+ if (!bulkStickerOn)
42
+ return;
43
+ const v = Number((bulkPrice || "").replace(",", "."));
44
+ if (!Number.isFinite(v) || v < 0)
45
+ return;
46
+ setSelProducts((prev) => prev.map((p) => (p.sticker_cost === v ? p : { ...p, sticker_cost: v })));
47
+ }, [bulkStickerOn, bulkPrice, selProducts.length]);
48
+ useEffect(() => {
49
+ (async () => {
50
+ setLoading(true);
51
+ const sb = supabase;
52
+ const [{ data: ps }, { data: cs }, { data: settings }] = await Promise.all([
53
+ sb.from("products").select("id, product, sale_price, quantity, image_url, barcode").order("product").limit(500),
54
+ sb.from("customers").select("id, company_name, email, customer_class, country").not("email", "is", null).order("company_name").limit(1000),
55
+ sb.from("offer_settings").select("from_name, from_email, default_subject, default_intro, default_sticker_cost, show_customer_classes").maybeSingle(),
56
+ ]);
57
+ setProducts((ps ?? []));
58
+ setCustomers((cs ?? []));
59
+ if (settings) {
60
+ if (settings.from_name)
61
+ setFromName(settings.from_name);
62
+ if (settings.from_email)
63
+ setFromEmail(settings.from_email);
64
+ if (settings.default_subject)
65
+ setSubject(settings.default_subject);
66
+ if (settings.default_intro)
67
+ setIntro(settings.default_intro);
68
+ if (settings.default_sticker_cost != null && Number(settings.default_sticker_cost) > 0) {
69
+ setBulkPrice(String(settings.default_sticker_cost));
70
+ }
71
+ if (typeof settings.show_customer_classes === "boolean")
72
+ setShowCustomerClasses(settings.show_customer_classes);
73
+ }
74
+ setLoading(false);
75
+ })();
76
+ }, []);
77
+ const addProduct = (p) => {
78
+ if (selProducts.find((sp) => sp.product_id === p.id))
79
+ return;
80
+ const price = Number(p.sale_price ?? 0);
81
+ setSelProducts((prev) => [...prev, {
82
+ product_id: p.id,
83
+ product_name: p.product,
84
+ description: "",
85
+ image_url: p.image_url,
86
+ normal_price: price,
87
+ discount_price: price,
88
+ available_stock: p.quantity ?? 0,
89
+ plants_per_crate: 10,
90
+ belading: "",
91
+ barcode: p.barcode ?? "",
92
+ crate_price: 0,
93
+ sticker_cost: null,
94
+ sticker_available: true,
95
+ tiers: [{ min_crates: 1, price_per_crate: price }],
96
+ }]);
97
+ setProductSearch("");
98
+ };
99
+ const removeProduct = (id) => setSelProducts((prev) => prev.filter((p) => p.product_id !== id));
100
+ const updateProd = (id, patch) => setSelProducts((prev) => prev.map((p) => (p.product_id === id ? { ...p, ...patch } : p)));
101
+ const applyDiscount = (pct) => {
102
+ setSelProducts((prev) => prev.map((p) => ({
103
+ ...p, discount_price: Number((p.normal_price * (1 - pct / 100)).toFixed(2)),
104
+ })));
105
+ };
106
+ const setAllStickerAvailable = (available) => {
107
+ setSelProducts((prev) => prev.map((p) => ({ ...p, sticker_available: available })));
108
+ toast.success(`Stickeropdruk ${available ? "ingeschakeld" : "uitgeschakeld"} voor ${selProducts.length} product(en)`);
109
+ };
110
+ const toggleCustomer = (id) => {
111
+ setSelCustomers((prev) => {
112
+ const next = new Set(prev);
113
+ if (next.has(id))
114
+ next.delete(id);
115
+ else
116
+ next.add(id);
117
+ return next;
118
+ });
119
+ };
120
+ const filteredCustomers = customers.filter((c) => c.company_name?.toLowerCase().includes(customerSearch.toLowerCase()) ||
121
+ c.email?.toLowerCase().includes(customerSearch.toLowerCase()));
122
+ const allVisibleSelected = filteredCustomers.length > 0 && filteredCustomers.every((c) => selCustomers.has(c.id));
123
+ const toggleAllVisible = () => {
124
+ setSelCustomers((prev) => {
125
+ const next = new Set(prev);
126
+ if (allVisibleSelected)
127
+ filteredCustomers.forEach((c) => next.delete(c.id));
128
+ else
129
+ filteredCustomers.forEach((c) => next.add(c.id));
130
+ return next;
131
+ });
132
+ };
133
+ // Klantgroep counts
134
+ const classGroups = useMemo(() => {
135
+ const letters = ["A", "B", "C", "D", "E", "F", "G", "H"];
136
+ return letters.map((l) => ({
137
+ letter: l,
138
+ customers: customers.filter((c) => (c.customer_class ?? "").toUpperCase() === l),
139
+ }));
140
+ }, [customers]);
141
+ const toggleClass = (letter) => {
142
+ const group = classGroups.find((g) => g.letter === letter);
143
+ if (!group || group.customers.length === 0)
144
+ return;
145
+ const ids = group.customers.map((c) => c.id);
146
+ const allSelected = ids.every((id) => selCustomers.has(id));
147
+ setSelCustomers((prev) => {
148
+ const next = new Set(prev);
149
+ if (allSelected)
150
+ ids.forEach((id) => next.delete(id));
151
+ else
152
+ ids.forEach((id) => next.add(id));
153
+ return next;
154
+ });
155
+ };
156
+ const handleSave = async (andSend) => {
157
+ if (selProducts.length === 0) {
158
+ toast.error("Voeg minimaal één product toe");
159
+ return;
160
+ }
161
+ if (selCustomers.size === 0) {
162
+ toast.error("Selecteer minimaal één klant");
163
+ return;
164
+ }
165
+ if (!fromEmail) {
166
+ toast.error("Vul een afzender e-mailadres in");
167
+ return;
168
+ }
169
+ setBusy(true);
170
+ try {
171
+ const recipients = customers
172
+ .filter((c) => selCustomers.has(c.id) && c.email)
173
+ .map((c) => ({
174
+ customer_id: c.id,
175
+ recipient_name: c.company_name,
176
+ recipient_email: c.email,
177
+ }));
178
+ const res = await createFn({ data: {
179
+ name, subject, from_name: fromName || (user?.email ?? "FlowSelections"),
180
+ from_email: fromEmail, intro_message: intro,
181
+ products: selProducts.map((p, i) => ({ ...p, sort_order: i })),
182
+ recipients,
183
+ } });
184
+ if (andSend) {
185
+ await sendFn({ data: { campaignId: res.campaignId } });
186
+ toast.success(`Campagne verstuurd naar ${recipients.length} ontvangers`);
187
+ }
188
+ else {
189
+ toast.success("Campagne opgeslagen als concept");
190
+ }
191
+ navigate({ to: "/aanbiedingsmailtje/$campaignId", params: { campaignId: res.campaignId } });
192
+ }
193
+ catch (e) {
194
+ toast.error(e?.message ?? "Fout bij opslaan");
195
+ }
196
+ finally {
197
+ setBusy(false);
198
+ }
199
+ };
200
+ const filteredProducts = products.filter((p) => Number(p.quantity ?? 0) > 0 && p.product.toLowerCase().includes(productSearch.toLowerCase()));
201
+ if (loading)
202
+ return _jsxs("div", { className: "p-6 space-y-4", children: [_jsx(Skeleton, { className: "h-32 w-full" }), _jsx(Skeleton, { className: "h-64 w-full" })] });
203
+ return (_jsxs("main", { className: "p-6 pb-28 space-y-6", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Tag, { className: "h-6 w-6 text-primary" }), _jsx("h1", { className: "text-2xl font-bold", children: "Acties & Promoties" })] }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Maak promoties en verstuur naar klanten" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => navigate({ to: "/aanbiedingsmailtje/overzicht" }), children: [_jsx(List, { className: "h-4 w-4 mr-2" }), "Overzicht"] }), _jsxs(Button, { variant: "outline", onClick: () => navigate({ to: "/aanbiedingsmailtje/rapportage" }), children: [_jsx(BarChart3, { className: "h-4 w-4 mr-2" }), "Rapportage"] })] })] }), _jsxs("div", { className: "grid gap-6 xl:grid-cols-2", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Package, { className: "h-5 w-5" }), _jsx(CardTitle, { children: "Producten Selecteren" })] }), _jsx(CardDescription, { children: "Kies producten en stel actieprijzen in" })] }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { className: "pl-9", placeholder: "Zoek product...", value: productSearch, onChange: (e) => setProductSearch(e.target.value) })] }), productSearch && (_jsx("div", { className: "max-h-40 overflow-y-auto border rounded-md", children: filteredProducts.slice(0, 20).map((p) => (_jsxs("button", { onClick: () => addProduct(p), className: "w-full text-left px-3 py-2 hover:bg-muted text-sm", children: [p.product, " ", _jsxs("span", { className: "text-muted-foreground", children: ["\u20AC ", Number(p.sale_price ?? 0).toFixed(2), " \u00B7 voorraad ", p.quantity ?? 0] })] }, p.id))) })), _jsxs("div", { className: "text-sm font-medium", children: ["Geselecteerde producten (", selProducts.length, ")"] }), selProducts.length === 0 ? (_jsx("div", { className: "text-sm text-muted-foreground py-6", children: "Geen producten geselecteerd" })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex flex-wrap gap-2 items-center", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: "Snelle korting:" }), [10, 15, 20, 25, 30].map((p) => (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => applyDiscount(p), children: [_jsx(Percent, { className: "h-3 w-3 mr-1" }), p, "%"] }, p)))] }), _jsxs("div", { className: "flex flex-wrap gap-2 items-center rounded-md border bg-muted/30 p-2", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: "Stickeropdruk \u2014 \u00E9\u00E9n prijs voor \u00E1lle producten: \u20AC" }), _jsx(Input, { type: "number", step: "0.01", placeholder: "0,00", className: "w-28 h-8", value: bulkPrice, onChange: (e) => setBulkPrice(e.target.value) }), _jsxs("div", { className: "flex items-center gap-2 ml-2", children: [_jsx(Switch, { id: "bulk-sticker-toggle", checked: bulkStickerOn, onCheckedChange: (checked) => {
204
+ setBulkStickerOn(checked);
205
+ if (!checked) {
206
+ setSelProducts((prev) => prev.map((p) => ({ ...p, sticker_cost: null })));
207
+ }
208
+ else {
209
+ const v = Number((bulkPrice || "").replace(",", "."));
210
+ if (Number.isFinite(v) && v >= 0) {
211
+ setSelProducts((prev) => prev.map((p) => ({ ...p, sticker_cost: v })));
212
+ }
213
+ }
214
+ } }), _jsxs(Label, { htmlFor: "bulk-sticker-toggle", className: "text-xs cursor-pointer", children: ["Toepassen op alle (", selProducts.length, ") \u2014 ", bulkStickerOn ? "Aan" : "Uit"] })] })] }), _jsxs("div", { className: "flex flex-wrap gap-2 items-center rounded-md border bg-muted/30 p-2", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: "Stickeropdruk voor alle planten:" }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => setAllStickerAvailable(true), children: "Aan voor allemaal" }), _jsx(Button, { size: "sm", variant: "outline", onClick: () => setAllStickerAvailable(false), children: "Uit voor allemaal" })] })] }), _jsx(Separator, {}), _jsx("div", { className: "space-y-3 max-h-[500px] overflow-y-auto pr-2", children: selProducts.map((p) => {
215
+ const updateTier = (idx, patch) => {
216
+ const tiers = [...p.tiers];
217
+ tiers[idx] = { ...tiers[idx], ...patch };
218
+ updateProd(p.product_id, { tiers });
219
+ };
220
+ const addTier = () => {
221
+ const last = p.tiers[p.tiers.length - 1];
222
+ const nextMin = last ? last.min_crates + 5 : 1;
223
+ updateProd(p.product_id, { tiers: [...p.tiers, { min_crates: nextMin, price_per_crate: p.normal_price }] });
224
+ };
225
+ const removeTier = (idx) => {
226
+ updateProd(p.product_id, { tiers: p.tiers.filter((_, i) => i !== idx) });
227
+ };
228
+ return (_jsxs("div", { className: "border rounded-md p-3 space-y-3 bg-muted/30", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { children: [_jsx("div", { className: "font-medium text-sm", children: p.product_name }), _jsxs("div", { className: "text-xs text-muted-foreground", children: ["Originele prijs: \u20AC ", p.normal_price.toFixed(2)] })] }), _jsx(Button, { variant: "ghost", size: "icon", onClick: () => removeProduct(p.product_id), children: _jsx(X, { className: "h-4 w-4" }) })] }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { className: "text-xs w-24 shrink-0", children: "Planten/fust:" }), _jsx(Input, { type: "number", value: p.plants_per_crate, onChange: (e) => updateProd(p.product_id, { plants_per_crate: Number(e.target.value) }) })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { className: "text-xs w-20 shrink-0", children: "Belading:" }), _jsx(Input, { placeholder: "AxBxC", value: p.belading, onChange: (e) => updateProd(p.product_id, { belading: e.target.value }) })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { className: "text-xs w-24 shrink-0", children: "Barcode:" }), _jsx(Input, { value: p.barcode, onChange: (e) => updateProd(p.product_id, { barcode: e.target.value }) })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { className: "text-xs w-20 shrink-0", children: "Fustprijs: \u20AC" }), _jsx(Input, { type: "number", step: "0.01", value: p.crate_price, onChange: (e) => updateProd(p.product_id, { crate_price: Number(e.target.value) }) })] })] }), _jsxs("div", { className: "flex items-center gap-2 border-t pt-2", children: [_jsx(Label, { className: "text-xs", children: "Stickeropdruk beschikbaar:" }), _jsx(Button, { type: "button", size: "sm", variant: p.sticker_available ? "default" : "outline", onClick: () => updateProd(p.product_id, { sticker_available: true }), children: "Ja" }), _jsx(Button, { type: "button", size: "sm", variant: !p.sticker_available ? "default" : "outline", onClick: () => updateProd(p.product_id, { sticker_available: false }), children: "Nee" })] }), _jsxs("div", { className: "space-y-2 border-t pt-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Label, { className: "text-xs font-medium", children: "Prijs aftuin ex. Fust vanaf .." }), _jsx(Button, { variant: "outline", size: "sm", onClick: addTier, children: "+ Staffel" })] }), p.tiers.map((t, idx) => {
229
+ const pct = p.normal_price > 0 ? Math.round((1 - t.price_per_crate / p.normal_price) * 100) : 0;
230
+ return (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: "Vanaf" }), _jsx(Input, { type: "number", className: "w-20", value: t.min_crates, onChange: (e) => updateTier(idx, { min_crates: Number(e.target.value) }) }), _jsx("span", { className: "text-xs text-muted-foreground", children: "fust(en): \u20AC" }), _jsx(Input, { type: "number", step: "0.01", className: "w-24", value: t.price_per_crate, onChange: (e) => updateTier(idx, { price_per_crate: Number(e.target.value) }) }), _jsxs("span", { className: `text-xs font-medium ${pct > 0 ? "text-destructive" : "text-muted-foreground"}`, children: ["(", pct, "%)"] }), p.tiers.length > 1 && (_jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7", onClick: () => removeTier(idx), children: _jsx(X, { className: "h-3 w-3" }) }))] }, idx));
231
+ })] }), _jsxs("div", { className: "grid grid-cols-2 gap-2 border-t pt-2", children: [_jsxs("div", { children: [_jsx(Label, { className: "text-xs", children: "Actieprijs \u20AC" }), _jsx(Input, { type: "number", step: "0.01", value: p.discount_price, onChange: (e) => updateProd(p.product_id, { discount_price: Number(e.target.value) }) })] }), _jsxs("div", { children: [_jsx(Label, { className: "text-xs", children: "Voorraad" }), _jsx(Input, { type: "number", value: p.available_stock, onChange: (e) => updateProd(p.product_id, { available_stock: Number(e.target.value) }) })] })] }), _jsx(Textarea, { placeholder: "Omschrijving (optioneel)", rows: 2, value: p.description, onChange: (e) => updateProd(p.product_id, { description: e.target.value }) })] }, p.product_id));
232
+ }) })] }))] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Users, { className: "h-5 w-5" }), _jsx(CardTitle, { children: "Klanten Selecteren" })] }), _jsx(CardDescription, { children: "Kies klanten om de actie naar te versturen" })] }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { className: "pl-9", placeholder: "Zoek klant...", value: customerSearch, onChange: (e) => setCustomerSearch(e.target.value) })] }), _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm cursor-pointer", children: [_jsx("input", { type: "checkbox", checked: allVisibleSelected, onChange: toggleAllVisible }), "Selecteer alle klanten met email"] }), _jsxs(Badge, { variant: "secondary", children: [selCustomers.size, " geselecteerd"] })] }), showCustomerClasses && (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "text-xs font-medium text-muted-foreground", children: "Selecteer op klantgroep" }), _jsx("div", { className: "flex flex-wrap gap-2", children: classGroups.map((g) => {
233
+ const allSel = g.customers.length > 0 && g.customers.every((c) => selCustomers.has(c.id));
234
+ return (_jsxs(Button, { type: "button", variant: allSel ? "default" : "outline", size: "sm", onClick: () => toggleClass(g.letter), className: "gap-2", children: ["Klasse ", g.letter, _jsx(Badge, { variant: "secondary", className: "px-1.5 py-0 text-xs", children: g.customers.length })] }, g.letter));
235
+ }) })] })), _jsx(Separator, {}), _jsx("div", { className: "max-h-[500px] overflow-y-auto space-y-2 pr-1", children: filteredCustomers.slice(0, 500).map((c) => {
236
+ const checked = selCustomers.has(c.id);
237
+ return (_jsxs("button", { onClick: () => toggleCustomer(c.id), className: `w-full text-left p-3 rounded-md border hover:bg-muted text-sm flex items-center gap-3 ${checked ? "bg-primary/5 border-primary/40" : ""}`, children: [_jsx("input", { type: "checkbox", checked: checked, readOnly: true }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "font-medium truncate", children: c.company_name }), _jsx("div", { className: "text-xs text-muted-foreground truncate", children: c.email })] }), c.customer_class && _jsx(Badge, { variant: "secondary", children: c.customer_class })] }, c.id));
238
+ }) })] })] })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Mail, { className: "h-5 w-5" }), _jsx(CardTitle, { children: "Email Inhoud" })] }), _jsx(CardDescription, { children: "Pas het onderwerp en de boodschap aan" })] }), _jsxs(CardContent, { className: "grid gap-4 md:grid-cols-2", children: [_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Onderwerp" }), _jsx(Input, { value: subject, onChange: (e) => setSubject(e.target.value) })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: "Bericht" }), _jsx(Textarea, { value: intro, onChange: (e) => setIntro(e.target.value), rows: 3 })] }), _jsxs("div", { className: "space-y-2 md:col-span-2 max-w-xs", children: [_jsx(Label, { children: "Kosten opdrukken eigen sticker (\u20AC per stuk)" }), _jsx(Input, { type: "number", step: "0.01", placeholder: "0,00", value: bulkPrice, onChange: (e) => setBulkPrice(e.target.value) })] }), _jsxs("div", { className: "space-y-2 md:col-span-2 hidden", children: [_jsx(Label, { children: "Naam (intern)" }), _jsx(Input, { value: name, onChange: (e) => setName(e.target.value) }), _jsx(Label, { children: "Afzendernaam" }), _jsx(Input, { value: fromName, onChange: (e) => setFromName(e.target.value) }), _jsx(Label, { children: "Afzender e-mail" }), _jsx(Input, { type: "email", value: fromEmail, onChange: (e) => setFromEmail(e.target.value) })] })] })] }), _jsx("div", { className: "fixed bottom-0 left-0 right-0 z-30 border-t bg-background/95 backdrop-blur px-6 py-3", children: _jsxs("div", { className: "flex items-center justify-between gap-4", children: [_jsxs("div", { children: [_jsx("div", { className: "font-semibold", children: "Samenvatting" }), _jsxs("div", { className: "text-sm text-muted-foreground", children: [selProducts.length, " product(en) \u2022 ", selCustomers.size, " klant(en)"] })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "outline", disabled: busy, onClick: () => handleSave(false), children: "Opslaan als concept" }), _jsxs(Button, { disabled: busy || selProducts.length === 0 || selCustomers.size === 0, onClick: () => handleSave(true), children: [_jsx(Send, { className: "h-4 w-4 mr-2" }), busy ? "Bezig..." : "Actie Versturen"] })] })] }) })] }));
239
+ }
@@ -0,0 +1,2 @@
1
+ export declare function ActiePage(): import("react").JSX.Element;
2
+ //# sourceMappingURL=ActiePage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActiePage.d.ts","sourceRoot":"","sources":["../../src/components/ActiePage.tsx"],"names":[],"mappings":"AAqCA,wBAAgB,SAAS,gCAqbxB"}
@@ -0,0 +1,173 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { useParams } from "@tanstack/react-router";
4
+ import { useServerFn } from "@tanstack/react-start";
5
+ import { Button, Input, Label, Textarea, Skeleton, toast } from "@flowselections/core";
6
+ import { getOfferByLinkCode, trackOfferClick, placeOfferOrder } from "../lib/public-offer.functions";
7
+ import { ShoppingCart, Check, User, Package, Truck, Minus, Plus, Send, Leaf } from "lucide-react";
8
+ function addBusinessDays(start, days) {
9
+ const d = new Date(start);
10
+ let added = 0;
11
+ while (added < days) {
12
+ d.setDate(d.getDate() + 1);
13
+ const day = d.getDay();
14
+ if (day !== 0 && day !== 6)
15
+ added++;
16
+ }
17
+ return d;
18
+ }
19
+ function toDateInput(d) {
20
+ const y = d.getFullYear();
21
+ const m = String(d.getMonth() + 1).padStart(2, "0");
22
+ const day = String(d.getDate()).padStart(2, "0");
23
+ return `${y}-${m}-${day}`;
24
+ }
25
+ function formatDateNL(iso) {
26
+ if (!iso)
27
+ return "";
28
+ try {
29
+ return new Date(iso).toLocaleDateString("nl-NL", {
30
+ weekday: "long", day: "numeric", month: "long", year: "numeric",
31
+ });
32
+ }
33
+ catch {
34
+ return iso;
35
+ }
36
+ }
37
+ const fmtEuro = (n) => `€ ${Number(n ?? 0).toLocaleString("nl-NL", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
38
+ export function ActiePage() {
39
+ const { linkCode } = useParams({ strict: false });
40
+ const getFn = useServerFn(getOfferByLinkCode);
41
+ const trackFn = useServerFn(trackOfferClick);
42
+ const orderFn = useServerFn(placeOfferOrder);
43
+ const [data, setData] = useState(null);
44
+ const [loading, setLoading] = useState(true);
45
+ const [qty, setQty] = useState({});
46
+ const [sticker, setSticker] = useState({});
47
+ const [name, setName] = useState("");
48
+ const [email, setEmail] = useState("");
49
+ const [phone, setPhone] = useState("");
50
+ const [notes, setNotes] = useState("");
51
+ const [deliveryDate, setDeliveryDate] = useState(() => toDateInput(addBusinessDays(new Date(), 3)));
52
+ const [placed, setPlaced] = useState(null);
53
+ const [busy, setBusy] = useState(false);
54
+ const minDate = useMemo(() => toDateInput(addBusinessDays(new Date(), 3)), []);
55
+ useEffect(() => {
56
+ let cancelled = false;
57
+ const load = async (initial) => {
58
+ try {
59
+ const result = await getFn({ data: { code: linkCode } });
60
+ if (cancelled)
61
+ return;
62
+ setData(result);
63
+ if (result && initial) {
64
+ setName(result.recipient.recipient_name);
65
+ setEmail(result.recipient.recipient_email);
66
+ trackFn({ data: { code: linkCode } }).catch(() => { });
67
+ }
68
+ }
69
+ finally {
70
+ if (initial && !cancelled)
71
+ setLoading(false);
72
+ }
73
+ };
74
+ load(true);
75
+ const iv = setInterval(() => load(false), 15000);
76
+ return () => { cancelled = true; clearInterval(iv); };
77
+ }, [linkCode]);
78
+ if (loading) {
79
+ return (_jsx("div", { className: "min-h-screen bg-[#f0fdf4] p-6", children: _jsx("div", { className: "max-w-6xl mx-auto", children: _jsx(Skeleton, { className: "h-64" }) }) }));
80
+ }
81
+ if (!data) {
82
+ return (_jsx("div", { className: "min-h-screen bg-[#f0fdf4] flex items-center justify-center p-6", children: _jsxs("div", { className: "text-center", children: [_jsx("h1", { className: "text-3xl font-bold text-slate-900", children: "Aanbieding niet gevonden" }), _jsx("p", { className: "text-slate-600 mt-2", children: "Deze link is mogelijk verlopen." })] }) }));
83
+ }
84
+ const safeProducts = Array.isArray(data?.products) ? data.products : [];
85
+ const lineFor = (p) => {
86
+ const stockPlants = Math.max(0, Number(p.live_stock ?? p.available_stock ?? 0));
87
+ const ppc = Math.max(1, Math.floor(Number(p.plants_per_crate ?? 1) || 1));
88
+ const maxCrates = Math.floor(stockPlants / ppc);
89
+ const soldOut = maxCrates <= 0;
90
+ const crates = Math.max(0, Math.floor(qty[p.id] ?? 0));
91
+ const plants = crates * ppc;
92
+ const unit = Number(p.discount_price ?? 0);
93
+ const stickerCost = Number(p.sticker_cost ?? 0);
94
+ const stickerAvail = p.sticker_available !== false;
95
+ const stickerOn = stickerAvail && !!sticker[p.id];
96
+ const productSubtotal = plants * unit;
97
+ const stickerSubtotal = stickerOn ? plants * stickerCost : 0;
98
+ return { q: crates, plants, ppc, unit, stickerCost, stickerAvail, stickerOn, productSubtotal, stickerSubtotal, total: productSubtotal + stickerSubtotal, stock: stockPlants, maxCrates, soldOut };
99
+ };
100
+ const totalAmount = safeProducts.reduce((s, p) => s + lineFor(p).total, 0);
101
+ const hasItems = safeProducts.some((p) => (qty[p.id] ?? 0) > 0);
102
+ const setQ = (id, value, maxCrates) => setQty({ ...qty, [id]: Math.max(0, Math.min(maxCrates, Math.floor(Number.isFinite(value) ? value : 0))) });
103
+ const submit = async () => {
104
+ const items = safeProducts
105
+ .filter((p) => (qty[p.id] ?? 0) > 0)
106
+ .map((p) => {
107
+ const l = lineFor(p);
108
+ return {
109
+ product_id: p.id,
110
+ product_name: p.product_name,
111
+ quantity: l.q,
112
+ unit_price: l.unit,
113
+ sticker: l.stickerOn,
114
+ sticker_unit_price: l.stickerOn ? l.stickerCost : 0,
115
+ };
116
+ });
117
+ if (items.length === 0) {
118
+ toast.error("Voeg minimaal één product toe");
119
+ return;
120
+ }
121
+ if (!deliveryDate) {
122
+ toast.error("Kies een leverdatum");
123
+ return;
124
+ }
125
+ setBusy(true);
126
+ try {
127
+ const r = await orderFn({
128
+ data: {
129
+ code: linkCode,
130
+ customer_name: name,
131
+ customer_email: email,
132
+ customer_phone: phone,
133
+ notes: `Gewenste leverdatum: ${formatDateNL(deliveryDate)}${notes ? `\n\n${notes}` : ""}`,
134
+ items,
135
+ },
136
+ });
137
+ setPlaced({ id: r.orderId, total: r.total });
138
+ }
139
+ catch (e) {
140
+ toast.error(e?.message ?? "Fout");
141
+ }
142
+ finally {
143
+ setBusy(false);
144
+ }
145
+ };
146
+ if (placed) {
147
+ return (_jsx("div", { className: "min-h-screen bg-[#f0fdf4] flex items-center justify-center p-6", children: _jsxs("div", { className: "max-w-md w-full bg-white rounded-2xl shadow-lg border border-green-100 p-8 text-center", children: [_jsx("div", { className: "mx-auto rounded-full bg-green-100 p-3 w-fit", children: _jsx(Check, { className: "h-8 w-8 text-green-600" }) }), _jsx("h2", { className: "mt-4 text-2xl font-bold text-slate-900", children: "Bedankt voor uw bestelling!" }), _jsxs("p", { className: "mt-2 text-slate-600", children: ["Bestelling #", placed.id.slice(0, 8)] }), _jsxs("p", { className: "mt-1 text-lg font-semibold text-green-700", children: ["Totaal: ", fmtEuro(placed.total)] }), _jsx("p", { className: "mt-4 text-sm text-slate-500", children: "We nemen zo spoedig mogelijk contact met u op." })] }) }));
148
+ }
149
+ const fromName = data?.campaign?.from_name ?? "";
150
+ return (_jsxs("div", { className: "min-h-screen bg-[#f0fdf4] flex flex-col", children: [_jsx("header", { className: "bg-gradient-to-r from-green-600 to-green-700 text-white", children: _jsxs("div", { className: "max-w-6xl mx-auto px-6 py-10 text-center", children: [_jsxs("div", { className: "mx-auto mb-4 inline-flex items-center justify-center bg-white rounded-md px-4 py-2 shadow-sm", children: [_jsx(Leaf, { className: "h-5 w-5 text-green-700 mr-2" }), _jsx("span", { className: "font-bold text-slate-900 tracking-tight", children: "Flow Selections" })] }), _jsxs("h1", { className: "text-3xl md:text-4xl font-extrabold tracking-tight", children: ["\uD83C\uDF31 ", data?.campaign?.subject ?? "Speciale Aanbieding"] }), data?.campaign?.intro_message ? (_jsx("p", { className: "mt-3 text-green-50 whitespace-pre-line max-w-2xl mx-auto", children: data.campaign.intro_message })) : (_jsx("p", { className: "mt-3 text-green-50", children: "Wij hebben een mooie aanbieding voor u!" }))] }) }), _jsxs("main", { className: "flex-1 max-w-6xl w-full mx-auto px-4 md:px-6 py-8 space-y-6", children: [_jsxs("div", { className: "bg-green-50 border border-green-200 rounded-xl p-5 flex items-center gap-4", children: [_jsx("div", { className: "rounded-full bg-green-600 p-3 text-white shrink-0", children: _jsx(User, { className: "h-5 w-5" }) }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "font-bold text-slate-900", children: ["Welkom, ", data?.recipient?.recipient_name, "!"] }), _jsx("div", { className: "text-green-700 text-sm truncate", children: data?.recipient?.recipient_email })] })] }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[minmax(0,1fr)_360px]", children: [_jsxs("section", { children: [_jsxs("h2", { className: "flex items-center gap-2 text-xl font-bold text-slate-900 mb-4", children: [_jsx(Package, { className: "h-5 w-5 text-green-700" }), "Selecteer uw producten"] }), safeProducts.some((p) => p.sticker_available !== false) && (_jsxs("div", { className: "mb-4 flex items-center justify-between gap-3 bg-blue-50 border border-blue-200 rounded-xl px-4 py-3", children: [_jsxs("div", { className: "text-sm text-blue-900", children: [_jsx("strong", { children: "Stickeropdruk voor alle planten" }), _jsx("div", { className: "text-xs text-blue-700", children: "Schakel met \u00E9\u00E9n klik in/uit voor alle producten waar het beschikbaar is." })] }), _jsxs("div", { className: "flex gap-2 shrink-0", children: [_jsx("button", { type: "button", onClick: () => {
151
+ const next = { ...sticker };
152
+ safeProducts.forEach((p) => { if (p.sticker_available !== false)
153
+ next[p.id] = true; });
154
+ setSticker(next);
155
+ }, className: "text-xs font-semibold bg-blue-600 text-white rounded px-3 py-1.5 hover:bg-blue-700", children: "Alles aan" }), _jsx("button", { type: "button", onClick: () => {
156
+ const next = { ...sticker };
157
+ safeProducts.forEach((p) => { next[p.id] = false; });
158
+ setSticker(next);
159
+ }, className: "text-xs font-semibold bg-white text-blue-700 border border-blue-300 rounded px-3 py-1.5 hover:bg-blue-50", children: "Alles uit" })] })] })), _jsx("div", { className: "space-y-4", children: safeProducts.map((p) => {
160
+ const normal = Number(p.normal_price ?? 0);
161
+ const price = Number(p.discount_price ?? 0);
162
+ const showStrike = normal > price;
163
+ const l = lineFor(p);
164
+ const currentQty = l.q;
165
+ return (_jsxs("div", { className: `bg-white rounded-xl border shadow-sm overflow-hidden ${l.soldOut ? "border-slate-200 opacity-75" : "border-slate-200"}`, children: [_jsxs("div", { className: "p-4 flex gap-4", children: [_jsxs("div", { className: "shrink-0 relative", children: [p.image_url ? (_jsx("img", { src: p.image_url, alt: p.product_name, className: `w-24 h-24 rounded-lg object-cover border border-slate-200 bg-slate-50 ${l.soldOut ? "grayscale" : ""}` })) : (_jsx("div", { className: `w-24 h-24 rounded-lg border border-slate-200 bg-green-50 grid place-items-center overflow-hidden ${l.soldOut ? "grayscale" : ""}`, children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 96 96", className: "w-full h-full", children: [_jsx("path", { d: "M48 60 Q46 75 44 86", stroke: "#16a34a", strokeWidth: "2.5", fill: "none", strokeLinecap: "round" }), _jsx("path", { d: "M48 72 Q40 70 36 64 Q42 66 48 72", fill: "#22c55e" }), _jsx("path", { d: "M48 78 Q56 76 60 70 Q54 72 48 78", fill: "#22c55e" }), _jsxs("g", { transform: "translate(48 40)", children: [_jsx("ellipse", { cx: "0", cy: "-14", rx: "9", ry: "14", fill: "#f9a8d4" }), _jsx("ellipse", { cx: "13.3", cy: "-4.3", rx: "9", ry: "14", fill: "#f472b6", transform: "rotate(72)" }), _jsx("ellipse", { cx: "8.2", cy: "11.3", rx: "9", ry: "14", fill: "#ec4899", transform: "rotate(144)" }), _jsx("ellipse", { cx: "-8.2", cy: "11.3", rx: "9", ry: "14", fill: "#f472b6", transform: "rotate(216)" }), _jsx("ellipse", { cx: "-13.3", cy: "-4.3", rx: "9", ry: "14", fill: "#f9a8d4", transform: "rotate(288)" }), _jsx("circle", { cx: "0", cy: "0", r: "6", fill: "#fbbf24" }), _jsx("circle", { cx: "0", cy: "0", r: "3", fill: "#f59e0b" })] })] }) })), l.soldOut && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: _jsx("span", { className: "bg-red-600 text-white text-xs font-bold px-2 py-1 rounded rotate-[-8deg] shadow", children: "UITVERKOCHT" }) }))] }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "font-bold text-slate-900 truncate", children: p.product_name }), p.plants_per_crate ? (_jsxs("div", { className: "text-sm text-slate-500", children: [p.plants_per_crate, " planten/fust"] })) : null, _jsxs("div", { className: "mt-2 flex items-baseline gap-2 flex-wrap", children: [showStrike && (_jsx("span", { className: "text-slate-400 line-through text-sm", children: fmtEuro(normal) })), _jsx("span", { className: "text-green-700 font-bold text-lg", children: fmtEuro(price) })] }), _jsx("div", { className: "mt-2", children: l.soldOut ? (_jsx("span", { className: "inline-flex items-center gap-1 text-xs font-semibold bg-red-100 text-red-800 border border-red-200 rounded px-2 py-1", children: "\u25CF Uitverkocht" })) : l.stock <= 10 ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs font-semibold bg-amber-100 text-amber-800 border border-amber-200 rounded px-2 py-1", children: ["\u25CF Nog ", l.stock, " op voorraad"] })) : (_jsxs("span", { className: "inline-flex items-center gap-1 text-xs font-semibold bg-green-100 text-green-800 border border-green-200 rounded px-2 py-1", children: ["\u25CF Op voorraad: ", l.stock, " stuks"] })) }), !l.soldOut && l.stickerAvail && (_jsxs("label", { className: "mt-2 flex items-start gap-2 text-xs text-blue-900 bg-blue-50 border-l-4 border-blue-400 rounded px-2 py-2 cursor-pointer select-none", children: [_jsx("input", { type: "checkbox", className: "mt-0.5", checked: l.stickerOn, onChange: (e) => setSticker({ ...sticker, [p.id]: e.target.checked }) }), _jsxs("span", { children: ["Stickeropdruk toevoegen", l.stickerCost > 0 ? _jsxs(_Fragment, { children: [" \u2014 ", _jsx("strong", { children: fmtEuro(l.stickerCost) }), " per stuk"] }) : _jsxs(_Fragment, { children: [" \u2014 ", _jsx("strong", { children: "geen meerprijs" })] })] })] })), !l.soldOut && !l.stickerAvail && (_jsx("div", { className: "mt-2 text-xs text-slate-500 bg-slate-50 border-l-4 border-slate-300 rounded px-2 py-2", children: "Stickeropdruk niet beschikbaar voor dit product" })), !l.soldOut && currentQty > 0 && (_jsxs("div", { className: "mt-3 text-xs text-slate-600 space-y-0.5 border-t border-slate-100 pt-2", children: [_jsxs("div", { className: "flex justify-between", children: [_jsxs("span", { children: ["Productprijs (", l.plants, " \u00D7 ", fmtEuro(l.unit), ")"] }), _jsx("span", { children: fmtEuro(l.productSubtotal) })] }), l.stickerOn && (_jsxs("div", { className: "flex justify-between", children: [_jsxs("span", { children: ["Stickeropdruk (", l.plants, " \u00D7 ", fmtEuro(l.stickerCost), ")"] }), _jsx("span", { children: fmtEuro(l.stickerSubtotal) })] })), _jsxs("div", { className: "flex justify-between font-bold text-slate-900 pt-0.5", children: [_jsx("span", { children: "Totaalprijs" }), _jsx("span", { className: "text-green-700", children: fmtEuro(l.total) })] })] }))] })] }), l.soldOut ? (_jsx("div", { className: "border-t border-slate-100 px-4 py-3 text-center text-sm text-red-700 bg-red-50 font-medium", children: "Dit product is niet meer beschikbaar" })) : (_jsxs("div", { className: "border-t border-slate-100 px-4 py-3 flex flex-wrap items-center justify-between gap-2", children: [_jsxs("span", { className: "text-sm text-slate-600", children: ["Aantal fusten:", currentQty > 0 && (_jsxs("span", { className: "ml-1 text-slate-500", children: ["= ", l.plants.toLocaleString("nl-NL"), " planten"] }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("button", { type: "button", onClick: () => setQ(p.id, currentQty - 1, l.maxCrates), className: "h-9 w-9 rounded-md border border-slate-200 grid place-items-center text-slate-600 hover:bg-slate-50 disabled:opacity-40", disabled: currentQty <= 0, "aria-label": "Minder", children: _jsx(Minus, { className: "h-4 w-4" }) }), _jsx(Input, { type: "number", inputMode: "numeric", step: 1, min: 0, max: l.maxCrates, value: currentQty, onChange: (e) => setQ(p.id, parseInt(e.target.value, 10), l.maxCrates), onKeyDown: (e) => {
166
+ if (["e", "E", "+", "-", ".", ","].includes(e.key))
167
+ e.preventDefault();
168
+ }, className: "w-28 text-center" }), _jsx("button", { type: "button", onClick: () => setQ(p.id, currentQty + 1, l.maxCrates), className: "h-9 w-9 rounded-md border border-slate-200 grid place-items-center text-slate-600 hover:bg-slate-50 disabled:opacity-40", disabled: currentQty >= l.maxCrates, "aria-label": "Meer", children: _jsx(Plus, { className: "h-4 w-4" }) })] })] }))] }, p.id));
169
+ }) })] }), _jsx("aside", { className: "lg:sticky lg:top-6 self-start", children: _jsxs("div", { className: "bg-white rounded-xl border border-slate-200 shadow-sm p-5 space-y-5", children: [_jsxs("h3", { className: "flex items-center gap-2 text-lg font-bold text-slate-900", children: [_jsx(ShoppingCart, { className: "h-5 w-5 text-green-700" }), "Uw bestelling"] }), hasItems ? (_jsxs("div", { className: "space-y-2 text-sm", children: [safeProducts.filter((p) => (qty[p.id] ?? 0) > 0).map((p) => {
170
+ const l = lineFor(p);
171
+ return (_jsxs("div", { className: "space-y-0.5", children: [_jsxs("div", { className: "flex justify-between gap-2", children: [_jsxs("span", { className: "text-slate-700 truncate", children: [p.product_name, " \u00D7 ", l.q, " fust", l.q === 1 ? "" : "en", " (", l.plants.toLocaleString("nl-NL"), " planten)"] }), _jsx("span", { className: "font-medium text-slate-900 shrink-0", children: fmtEuro(l.productSubtotal) })] }), l.stickerOn && (_jsxs("div", { className: "flex justify-between gap-2 text-xs text-blue-800", children: [_jsxs("span", { className: "truncate", children: ["\u21B3 Stickeropdruk \u00D7 ", l.plants.toLocaleString("nl-NL")] }), _jsx("span", { className: "shrink-0", children: fmtEuro(l.stickerSubtotal) })] }))] }, p.id));
172
+ }), _jsxs("div", { className: "border-t border-slate-200 pt-2 flex justify-between font-bold text-slate-900", children: [_jsx("span", { children: "Totaal" }), _jsx("span", { className: "text-green-700", children: fmtEuro(totalAmount) })] })] })) : (_jsx("p", { className: "text-sm text-slate-500 text-center py-2", children: "Selecteer producten om te bestellen" })), _jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { className: "flex items-center gap-2 text-slate-800", children: [_jsx(Truck, { className: "h-4 w-4 text-green-700" }), "Gewenste leverdatum"] }), _jsx(Input, { type: "date", min: minDate, value: deliveryDate, onChange: (e) => setDeliveryDate(e.target.value) }), _jsx("p", { className: "text-xs text-slate-500", children: "Minimaal 3 werkdagen van tevoren" })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-slate-800", children: "Opmerkingen (optioneel)" }), _jsx(Textarea, { placeholder: "Speciale wensen of vragen...", value: notes, onChange: (e) => setNotes(e.target.value), rows: 3 })] }), _jsxs(Button, { onClick: submit, disabled: busy || !hasItems || !name || !email, className: "w-full bg-green-600 hover:bg-green-700 text-white", size: "lg", children: [_jsx(Send, { className: "h-4 w-4 mr-2" }), busy ? "Bezig..." : "Bestelling plaatsen"] })] }) })] })] }), _jsxs("footer", { className: "bg-slate-900 text-slate-300 text-center py-5 text-sm", children: ["\u00A9 ", new Date().getFullYear(), " ", fromName || "Flow Selections"] })] }));
173
+ }
@@ -0,0 +1,2 @@
1
+ export declare function CampagneDetail(): import("react").JSX.Element;
2
+ //# sourceMappingURL=CampagneDetail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CampagneDetail.d.ts","sourceRoot":"","sources":["../../src/components/CampagneDetail.tsx"],"names":[],"mappings":"AASA,wBAAgB,cAAc,gCA+G7B"}
@@ -0,0 +1,61 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { useParams } from "@tanstack/react-router";
4
+ import { useServerFn } from "@tanstack/react-start";
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle, Badge, Button, Skeleton, toast } from "@flowselections/core";
6
+ import { getCampaign, sendCampaign, deleteCampaign } from "../lib/campaigns.functions";
7
+ import { getCampaignStats } from "../lib/stats.functions";
8
+ import { Send, Trash2, Mail, MousePointerClick, ShoppingCart, Eye, ArrowLeft } from "lucide-react";
9
+ import { useNavigate, Link } from "@tanstack/react-router";
10
+ export function CampagneDetail() {
11
+ const { campaignId } = useParams({ strict: false });
12
+ const navigate = useNavigate();
13
+ const getFn = useServerFn(getCampaign);
14
+ const statsFn = useServerFn(getCampaignStats);
15
+ const sendFn = useServerFn(sendCampaign);
16
+ const delFn = useServerFn(deleteCampaign);
17
+ const [data, setData] = useState(null);
18
+ const [stats, setStats] = useState(null);
19
+ const [busy, setBusy] = useState(false);
20
+ const reload = async () => {
21
+ const [d, s] = await Promise.all([
22
+ getFn({ data: { id: campaignId } }),
23
+ statsFn({ data: { campaignId } }),
24
+ ]);
25
+ setData(d);
26
+ setStats(s);
27
+ };
28
+ useEffect(() => { reload(); }, [campaignId]);
29
+ if (!data?.campaign)
30
+ return _jsx("div", { className: "p-6", children: _jsx(Skeleton, { className: "h-64" }) });
31
+ const c = data.campaign;
32
+ const origin = typeof window !== "undefined" ? window.location.origin : "";
33
+ const handleSend = async () => {
34
+ setBusy(true);
35
+ try {
36
+ const r = await sendFn({ data: { campaignId } });
37
+ toast.success(`Verzonden naar ${r.sent} ontvangers`);
38
+ await reload();
39
+ }
40
+ catch (e) {
41
+ toast.error(e?.message ?? "Fout");
42
+ }
43
+ finally {
44
+ setBusy(false);
45
+ }
46
+ };
47
+ const handleDel = async () => {
48
+ if (!confirm("Campagne verwijderen?"))
49
+ return;
50
+ await delFn({ data: { id: campaignId } });
51
+ toast.success("Verwijderd");
52
+ navigate({ to: "/aanbiedingsmailtje/overzicht" });
53
+ };
54
+ return (_jsxs("main", { className: "p-6 space-y-6", children: [_jsx(Link, { to: "/aanbiedingsmailtje/overzicht", children: _jsxs(Button, { variant: "ghost", size: "sm", className: "-ml-2", children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1" }), "Terug"] }) }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs("div", { className: "flex justify-between items-start", children: [_jsxs("div", { children: [_jsx(CardTitle, { children: c.name }), _jsx(CardDescription, { children: c.subject })] }), _jsx(Badge, { variant: c.status === "sent" ? "default" : "secondary", children: c.status })] }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "text-sm text-muted-foreground", children: ["Van: ", c.from_name, " <", c.from_email, ">"] }), c.intro_message && _jsx("p", { className: "text-sm", children: c.intro_message }), _jsxs("div", { className: "flex gap-2", children: [c.status !== "sent" && (_jsxs(Button, { onClick: handleSend, disabled: busy, children: [_jsx(Send, { className: "h-4 w-4 mr-1" }), "Versturen"] })), _jsxs(Button, { variant: "destructive", onClick: handleDel, children: [_jsx(Trash2, { className: "h-4 w-4 mr-1" }), "Verwijderen"] })] })] })] }), stats && (_jsxs("div", { className: "grid grid-cols-2 md:grid-cols-5 gap-3", children: [_jsx(StatCard, { icon: _jsx(Mail, {}), label: "Verzonden", value: stats.sent }), _jsx(StatCard, { icon: _jsx(Eye, {}), label: "Geopend", value: stats.opened }), _jsx(StatCard, { icon: _jsx(MousePointerClick, {}), label: "Klikken", value: stats.clicked }), _jsx(StatCard, { icon: _jsx(ShoppingCart, {}), label: "Bestellingen", value: stats.ordered }), _jsx(StatCard, { icon: _jsx("span", { children: "\u20AC" }), label: "Omzet", value: `€ ${Number(stats.revenue).toFixed(2)}` })] })), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsxs(CardTitle, { children: ["Producten (", data.products.length, ")"] }) }), _jsx(CardContent, { className: "space-y-2", children: data.products.map((p) => (_jsxs("div", { className: "flex justify-between border-b py-2 text-sm", children: [_jsx("span", { children: p.product_name }), _jsxs("span", { children: [_jsxs("span", { className: "line-through text-muted-foreground mr-2", children: ["\u20AC ", Number(p.normal_price).toFixed(2)] }), _jsxs("span", { className: "font-semibold", children: ["\u20AC ", Number(p.discount_price).toFixed(2)] })] })] }, p.id))) })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs(CardTitle, { children: ["Ontvangers (", data.recipients.length, ")"] }), _jsx(CardDescription, { children: "Klik op een rij om de unieke actie-URL te kopi\u00EBren" })] }), _jsx(CardContent, { children: _jsx("div", { className: "max-h-[400px] overflow-y-auto divide-y text-sm", children: data.recipients.map((r) => {
55
+ const url = `${origin}/actie/${r.link_code}`;
56
+ return (_jsxs("button", { onClick: () => { navigator.clipboard.writeText(url); toast.success("Link gekopieerd"); }, className: "w-full text-left py-2 hover:bg-muted/50 grid grid-cols-12 gap-2 items-center", children: [_jsx("div", { className: "col-span-4 font-medium truncate", children: r.recipient_name }), _jsx("div", { className: "col-span-4 text-muted-foreground truncate", children: r.recipient_email }), _jsxs("div", { className: "col-span-4 flex gap-1 justify-end", children: [r.sent_at && _jsx(Badge, { variant: "outline", children: "verzonden" }), r.opened_at && _jsx(Badge, { variant: "secondary", children: "geopend" }), r.clicked_at && _jsx(Badge, { children: "geklikt" }), r.ordered_at && _jsx(Badge, { variant: "default", children: "besteld" })] })] }, r.id));
57
+ }) }) })] })] }));
58
+ }
59
+ function StatCard({ icon, label, value }) {
60
+ return (_jsx(Card, { children: _jsxs(CardContent, { className: "p-4", children: [_jsxs("div", { className: "flex items-center gap-2 text-muted-foreground text-xs mb-1", children: [icon, _jsx("span", { children: label })] }), _jsx("div", { className: "text-2xl font-bold", children: value })] }) }));
61
+ }
@@ -0,0 +1,2 @@
1
+ export declare function CampagneOverzicht(): import("react").JSX.Element;
2
+ //# sourceMappingURL=CampagneOverzicht.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CampagneOverzicht.d.ts","sourceRoot":"","sources":["../../src/components/CampagneOverzicht.tsx"],"names":[],"mappings":"AAOA,wBAAgB,iBAAiB,gCAuChC"}
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Card, CardContent, CardHeader, CardTitle, Badge, Button, Skeleton } from "@flowselections/core";
4
+ import { Link } from "@tanstack/react-router";
5
+ import { useServerFn } from "@tanstack/react-start";
6
+ import { listCampaigns } from "../lib/campaigns.functions";
7
+ import { Plus, ArrowLeft } from "lucide-react";
8
+ export function CampagneOverzicht() {
9
+ const fn = useServerFn(listCampaigns);
10
+ const [data, setData] = useState(null);
11
+ useEffect(() => { fn({}).then(setData); }, []);
12
+ if (!data)
13
+ return _jsx("div", { className: "p-6", children: _jsx(Skeleton, { className: "h-64" }) });
14
+ const campaigns = Array.isArray(data.campaigns) ? data.campaigns : [];
15
+ return (_jsxs("main", { className: "p-6 space-y-4", children: [_jsx(Link, { to: "/aanbiedingsmailtje", children: _jsxs(Button, { variant: "ghost", size: "sm", className: "-ml-2", children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1" }), "Terug"] }) }), _jsxs("div", { className: "flex justify-between items-center", children: [_jsx("h2", { className: "text-xl font-semibold", children: "Alle campagnes" }), _jsx(Link, { to: "/aanbiedingsmailtje", children: _jsxs(Button, { children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), "Nieuwe campagne"] }) })] }), campaigns.length === 0 ? (_jsx(Card, { children: _jsx(CardContent, { className: "p-6 text-center text-muted-foreground", children: "Nog geen campagnes" }) })) : (_jsx("div", { className: "grid gap-3", children: campaigns.map((c) => (_jsx(Link, { to: "/aanbiedingsmailtje/$campaignId", params: { campaignId: c.id }, children: _jsxs(Card, { className: "hover:bg-muted/50 transition-colors", children: [_jsx(CardHeader, { className: "pb-2", children: _jsxs("div", { className: "flex justify-between items-start", children: [_jsx(CardTitle, { className: "text-base", children: c.name }), _jsx(Badge, { variant: c.status === "sent" ? "default" : "secondary", children: c.status })] }) }), _jsxs(CardContent, { className: "pt-0 text-sm text-muted-foreground", children: [c.subject, " \u00B7 ", c.sent_at ? `verzonden ${new Date(c.sent_at).toLocaleString("nl-NL")}` : `aangemaakt ${new Date(c.created_at).toLocaleDateString("nl-NL")}`] })] }) }, c.id))) }))] }));
16
+ }