@flowselections/actie-mail-kwekers-erp-mathot 1.0.14 → 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 (34) hide show
  1. package/dist-lib/components/AanbiedingsmailtjePage.d.ts.map +1 -1
  2. package/dist-lib/components/AanbiedingsmailtjePage.js +102 -57
  3. package/dist-lib/components/ActiePage.d.ts.map +1 -1
  4. package/dist-lib/components/ActiePage.js +35 -10
  5. package/dist-lib/components/RapportagePage.d.ts.map +1 -1
  6. package/dist-lib/components/RapportagePage.js +37 -7
  7. package/dist-lib/components/aanbieding/EmailPreview.d.ts +22 -0
  8. package/dist-lib/components/aanbieding/EmailPreview.d.ts.map +1 -0
  9. package/dist-lib/components/aanbieding/EmailPreview.js +15 -0
  10. package/dist-lib/components/aanbieding/WizardSteps.d.ts +14 -0
  11. package/dist-lib/components/aanbieding/WizardSteps.d.ts.map +1 -0
  12. package/dist-lib/components/aanbieding/WizardSteps.js +21 -0
  13. package/dist-lib/components/settings/HuisstijlSettingsCard.d.ts.map +1 -1
  14. package/dist-lib/components/settings/HuisstijlSettingsCard.js +4 -4
  15. package/dist-lib/index.d.ts +2 -0
  16. package/dist-lib/index.d.ts.map +1 -1
  17. package/dist-lib/index.js +2 -0
  18. package/dist-lib/integrations/supabase/auth-middleware.d.ts +16340 -7104
  19. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  20. package/dist-lib/integrations/supabase/client.d.ts +16340 -7104
  21. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  22. package/dist-lib/integrations/supabase/client.server.d.ts +16340 -7104
  23. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  24. package/dist-lib/integrations/supabase/types.d.ts +16654 -7203
  25. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  26. package/dist-lib/integrations/supabase/types.js +59 -8
  27. package/dist-lib/lib/campaigns.functions.d.ts +81831 -35651
  28. package/dist-lib/lib/campaigns.functions.d.ts.map +1 -1
  29. package/dist-lib/lib/public-offer.functions.d.ts +7 -0
  30. package/dist-lib/lib/public-offer.functions.d.ts.map +1 -1
  31. package/dist-lib/lib/public-offer.functions.js +8 -0
  32. package/dist-lib/lib/stats.functions.d.ts +34582 -16110
  33. package/dist-lib/lib/stats.functions.d.ts.map +1 -1
  34. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"AanbiedingsmailtjePage.d.ts","sourceRoot":"","sources":["../../src/components/AanbiedingsmailtjePage.tsx"],"names":[],"mappings":"AA0CA,wBAAgB,sBAAsB,gCA8oBrC"}
1
+ {"version":3,"file":"AanbiedingsmailtjePage.d.ts","sourceRoot":"","sources":["../../src/components/AanbiedingsmailtjePage.tsx"],"names":[],"mappings":"AA4CA,wBAAgB,sBAAsB,gCA8zBrC"}
@@ -3,9 +3,11 @@ import { useState, useEffect, useMemo } from "react";
3
3
  import { useAuth, supabase, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Input, Label, Textarea, Badge, Separator, Skeleton, Switch, toast, } from "@flowselections/core";
4
4
  import { useServerFn } from "@tanstack/react-start";
5
5
  import { useNavigate } from "@tanstack/react-router";
6
- import { Mail, Send, Search, X, Package, Users, Percent, Tag, List, BarChart3 } from "lucide-react";
6
+ import { Mail, Send, Search, X, Package, Users, Percent, Tag, List, BarChart3, Info, ArrowLeft, ArrowRight, CheckCircle2 } from "lucide-react";
7
7
  import { createCampaign, sendCampaign } from "../lib/campaigns.functions";
8
8
  import { createVoorraadStockMap, voorraadAvailableStock } from "../lib/voorraad-service";
9
+ import { WizardSteps, AANBIEDING_STEPS } from "../components/aanbieding/WizardSteps";
10
+ import { EmailPreview } from "../components/aanbieding/EmailPreview";
9
11
  export function AanbiedingsmailtjePage() {
10
12
  const { user } = useAuth();
11
13
  const navigate = useNavigate();
@@ -15,6 +17,8 @@ export function AanbiedingsmailtjePage() {
15
17
  const [customers, setCustomers] = useState([]);
16
18
  const [loading, setLoading] = useState(true);
17
19
  const [busy, setBusy] = useState(false);
20
+ const [step, setStep] = useState(1);
21
+ const [maxStep, setMaxStep] = useState(1);
18
22
  const [name, setName] = useState("Nieuwe aanbieding");
19
23
  const [subject, setSubject] = useState("🌷 Speciale aanbieding voor u!");
20
24
  const [fromName, setFromName] = useState("");
@@ -227,17 +231,20 @@ export function AanbiedingsmailtjePage() {
227
231
  })();
228
232
  }, []);
229
233
  const addProduct = (p) => {
230
- if (selProducts.find((sp) => sp.product_id === p.id))
234
+ const existing = selProducts.find((sp) => sp.product_id === p.id);
235
+ if (existing) {
236
+ toast.info("Dit product is al geselecteerd");
231
237
  return;
238
+ }
232
239
  setSelProducts((prev) => [...prev, {
233
- product_id: p.id,
234
- product_name: p.product,
240
+ product_id: p.id ?? `tmp:${p.product}`,
241
+ product_name: p.product ?? "(naamloos)",
235
242
  description: "",
236
243
  image_url: p.image_url,
237
244
  normal_price: 0,
238
245
  discount_price: 0,
239
246
  available_stock: p.quantity ?? 0,
240
- plants_per_crate: p.plants_per_crate ?? 10,
247
+ plants_per_crate: p.plants_per_crate ?? 0,
241
248
  belading: p.belading ?? "",
242
249
  barcode: p.barcode ?? "",
243
250
  crate_price: 0,
@@ -246,17 +253,34 @@ export function AanbiedingsmailtjePage() {
246
253
  tiers: [{ min_crates: 1, price_per_crate: 0 }],
247
254
  }]);
248
255
  setProductSearch("");
256
+ setProductSearchFocused(false);
249
257
  };
250
258
  const removeProduct = (id) => setSelProducts((prev) => prev.filter((p) => p.product_id !== id));
251
259
  const updateProd = (id, patch) => setSelProducts((prev) => prev.map((p) => (p.product_id === id ? { ...p, ...patch } : p)));
252
260
  const applyDiscount = (pct) => {
253
- setSelProducts((prev) => prev.map((p) => ({
254
- ...p, discount_price: Number((p.normal_price * (1 - pct / 100)).toFixed(2)),
255
- })));
261
+ const canApply = selProducts.some((p) => Math.max(p.normal_price, p.crate_price, ...p.tiers.map((t) => t.price_per_crate)) > 0);
262
+ if (!canApply) {
263
+ toast.error("Vul eerst een fustprijs in voordat je korting toepast");
264
+ return;
265
+ }
266
+ setSelProducts((prev) => prev.map((p) => {
267
+ const base = Math.max(p.normal_price, p.crate_price, ...p.tiers.map((t) => t.price_per_crate));
268
+ if (!(base > 0))
269
+ return p;
270
+ const newPrice = Number((base * (1 - pct / 100)).toFixed(2));
271
+ return {
272
+ ...p,
273
+ normal_price: base,
274
+ crate_price: newPrice,
275
+ discount_price: newPrice,
276
+ tiers: p.tiers.map((t) => ({ ...t, price_per_crate: newPrice })),
277
+ };
278
+ }));
279
+ toast.success(`${pct}% korting toegepast`);
256
280
  };
257
281
  const setAllStickerAvailable = (available) => {
258
282
  setSelProducts((prev) => prev.map((p) => ({ ...p, sticker_available: available })));
259
- toast.success(`Stickeropdruk ${available ? "ingeschakeld" : "uitgeschakeld"} voor ${selProducts.length} product(en)`);
283
+ toast.success(`Stickeropdruk ${available ? "ingeschakeld" : "uitgeschakeld"} voor het product`);
260
284
  };
261
285
  const toggleCustomer = (id) => {
262
286
  setSelCustomers((prev) => {
@@ -289,6 +313,8 @@ export function AanbiedingsmailtjePage() {
289
313
  customers: customers.filter((c) => (c.customer_class ?? "").toUpperCase() === l),
290
314
  }));
291
315
  }, [customers]);
316
+ const allStickerAvailable = selProducts.length > 0 && selProducts.every((p) => p.sticker_available);
317
+ const allStickerUnavailable = selProducts.length > 0 && selProducts.every((p) => !p.sticker_available);
292
318
  const toggleClass = (letter) => {
293
319
  const group = classGroups.find((g) => g.letter === letter);
294
320
  if (!group || group.customers.length === 0)
@@ -344,54 +370,73 @@ export function AanbiedingsmailtjePage() {
344
370
  setBusy(false);
345
371
  }
346
372
  };
347
- const filteredProducts = products.filter((p) => p.product.toLowerCase().includes(productSearch.toLowerCase()));
373
+ const filteredProducts = products.filter((p) => (p.product ?? "").toLowerCase().includes(productSearch.toLowerCase()));
348
374
  if (loading)
349
375
  return _jsxs("div", { className: "p-6 space-y-4", children: [_jsx(Skeleton, { className: "h-32 w-full" }), _jsx(Skeleton, { className: "h-64 w-full" })] });
350
- 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), onFocus: () => setProductSearchFocused(true), onBlur: () => setTimeout(() => setProductSearchFocused(false), 200) })] }), (productSearchFocused || productSearch) && (_jsxs("div", { className: "max-h-80 overflow-y-auto border rounded-md", children: [filteredProducts.map((p) => (_jsxs("button", { onMouseDown: () => addProduct(p), className: "w-full text-left px-3 py-2 hover:bg-muted text-sm", children: [p.product, " ", _jsx("span", { className: "text-muted-foreground", children: p.is_draft ? "· concept" : `· voorraad ${p.quantity ?? 0}` })] }, p.id))), filteredProducts.length === 0 && (_jsx("div", { className: "px-3 py-2 text-sm text-muted-foreground", children: "Geen producten gevonden" }))] })), _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) => {
351
- setBulkStickerOn(checked);
352
- if (!checked) {
353
- setSelProducts((prev) => prev.map((p) => ({ ...p, sticker_cost: null })));
354
- }
355
- else {
356
- const v = Number((bulkPrice || "").replace(",", "."));
357
- if (Number.isFinite(v) && v >= 0) {
358
- setSelProducts((prev) => prev.map((p) => ({ ...p, sticker_cost: v })));
359
- }
360
- }
361
- } }), _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) => {
362
- const updateTier = (idx, patch) => {
363
- const tiers = [...p.tiers];
364
- tiers[idx] = { ...tiers[idx], ...patch };
365
- const maxPrice = Math.max(p.crate_price, ...tiers.map((t) => t.price_per_crate));
366
- updateProd(p.product_id, { tiers, normal_price: Math.max(p.normal_price, maxPrice) });
367
- };
368
- const addTier = () => {
369
- const last = p.tiers[p.tiers.length - 1];
370
- const nextMin = last ? last.min_crates + 5 : 1;
371
- const basePrice = Math.max(p.crate_price, p.normal_price);
372
- const newTiers = [...p.tiers, { min_crates: nextMin, price_per_crate: basePrice }];
373
- updateProd(p.product_id, { tiers: newTiers, normal_price: Math.max(p.normal_price, basePrice) });
374
- };
375
- const removeTier = (idx) => {
376
- const tiers = p.tiers.filter((_, i) => i !== idx);
377
- const maxPrice = Math.max(p.crate_price, ...tiers.map((t) => t.price_per_crate));
378
- updateProd(p.product_id, { tiers, normal_price: Math.max(p.normal_price, maxPrice) });
379
- };
380
- 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) => {
381
- const newPrice = Number(e.target.value);
382
- const oldPrice = p.crate_price;
383
- const tiers = p.tiers.map((t) => t.price_per_crate === oldPrice ? { ...t, price_per_crate: newPrice } : t);
384
- const normalPrice = Math.max(p.normal_price, newPrice, ...tiers.map((t) => t.price_per_crate));
385
- updateProd(p.product_id, { crate_price: newPrice, tiers, normal_price: normalPrice });
386
- } })] })] }), _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) => {
387
- const pct = p.normal_price > 0 ? Math.round((1 - t.price_per_crate / p.normal_price) * 100) : 0;
388
- 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));
389
- })] }), _jsx(Textarea, { placeholder: "Omschrijving (optioneel)", rows: 2, value: p.description, onChange: (e) => updateProd(p.product_id, { description: e.target.value }) })] }, p.product_id));
390
- }) })] }))] })] }), _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) => {
391
- const allSel = g.customers.length > 0 && g.customers.every((c) => selCustomers.has(c.id));
392
- 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));
393
- }) })] })), _jsx(Separator, {}), _jsx("div", { className: "max-h-[500px] overflow-y-auto space-y-2 pr-1", children: filteredCustomers.slice(0, 500).map((c) => {
394
- const checked = selCustomers.has(c.id);
395
- 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: [_jsxs("div", { className: "font-medium truncate flex items-center gap-2", children: [_jsx("span", { className: "truncate", children: c.company_name }), c.source === "ext" && _jsx(Badge, { variant: "outline", className: "text-[10px] px-1.5 py-0", children: "Floriday" })] }), _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));
396
- }) })] })] })] }), _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"] })] })] }) })] }));
376
+ const stepDef = AANBIEDING_STEPS.find((s) => s.id === step) ?? AANBIEDING_STEPS[0];
377
+ const canNext = step === 1 ? selProducts.length > 0 : step === 2 ? selCustomers.size > 0 : true;
378
+ const nextHint = step === 1 && selProducts.length === 0
379
+ ? "Voeg minimaal één product toe om verder te gaan."
380
+ : step === 2 && selCustomers.size === 0
381
+ ? "Selecteer minimaal één klant om verder te gaan."
382
+ : "";
383
+ const goTo = (target) => {
384
+ const clamped = Math.min(4, Math.max(1, target));
385
+ setStep(clamped);
386
+ setMaxStep((m) => Math.max(m, clamped));
387
+ };
388
+ const goNext = () => {
389
+ if (!canNext) {
390
+ toast.error(nextHint);
391
+ return;
392
+ }
393
+ goTo(step + 1);
394
+ };
395
+ 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 flex-wrap", children: [_jsx(Tag, { className: "h-6 w-6 text-primary" }), _jsx("h1", { className: "text-2xl font-bold", children: "Nieuwe aanbieding" }), _jsxs(Badge, { variant: "secondary", children: ["Stap ", step, " van 4"] })] }), _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"] })] })] }), _jsx(WizardSteps, { current: step, maxReached: maxStep, onSelect: goTo }), _jsxs("div", { className: "grid gap-6 xl:grid-cols-[minmax(0,1fr)_420px] items-start", children: [_jsxs("div", { className: "space-y-6", children: [step === 1 && (_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: "Wat wil je aanbieden?" })] }), _jsx(CardDescription, { children: "Kies je 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 en voeg producten toe...", value: productSearch, onChange: (e) => setProductSearch(e.target.value), onFocus: () => setProductSearchFocused(true), onBlur: () => setTimeout(() => setProductSearchFocused(false), 200) })] }), (productSearchFocused || productSearch) && (_jsxs("div", { className: "max-h-80 overflow-y-auto border rounded-md", children: [filteredProducts.map((p, i) => (_jsxs("button", { type: "button", onMouseDown: (e) => { e.preventDefault(); addProduct(p); }, onClick: () => addProduct(p), className: "w-full text-left px-3 py-2 hover:bg-muted text-sm flex items-center gap-3", children: [_jsx("div", { className: "h-10 w-10 shrink-0 rounded-md border bg-muted overflow-hidden flex items-center justify-center", children: p.image_url ? (_jsx("img", { src: p.image_url, alt: p.product ?? "", className: "h-full w-full object-cover" })) : (_jsx(Package, { className: "h-5 w-5 text-muted-foreground" })) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "truncate font-medium", children: p.product }), _jsx("div", { className: "text-xs text-muted-foreground truncate", children: p.is_draft ? "Concept" : `Voorraad: ${p.quantity ?? 0}` })] })] }, `${p.id ?? "x"}-${i}`))), filteredProducts.length === 0 && (_jsx("div", { className: "px-3 py-2 text-sm text-muted-foreground", children: "Geen producten gevonden" }))] })), _jsxs("div", { className: "text-sm font-medium", children: ["Geselecteerde producten", selProducts.length > 0 && (_jsxs("span", { className: "ml-2 inline-flex items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-xs font-normal text-primary", children: [selProducts.length, " product", selProducts.length === 1 ? "" : "en"] }))] }), selProducts.length === 0 ? (_jsxs("div", { className: "rounded-md border border-dashed p-6 text-center text-sm text-muted-foreground", children: [_jsx(Package, { className: "mx-auto mb-2 h-5 w-5" }), _jsx("div", { className: "font-medium text-foreground", children: "Nog geen producten toegevoegd" }), _jsx("div", { children: "Zoek en voeg producten toe om je aanbieding samen te stellen." })] })) : (_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 prijs: \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) => {
396
+ setBulkStickerOn(checked);
397
+ if (!checked) {
398
+ setSelProducts((prev) => prev.map((p) => ({ ...p, sticker_cost: null })));
399
+ }
400
+ else {
401
+ const v = Number((bulkPrice || "").replace(",", "."));
402
+ if (Number.isFinite(v) && v >= 0) {
403
+ setSelProducts((prev) => prev.map((p) => ({ ...p, sticker_cost: v })));
404
+ }
405
+ }
406
+ } }), _jsxs(Label, { htmlFor: "bulk-sticker-toggle", className: "text-xs cursor-pointer", children: ["Toepassen op producten \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: allStickerAvailable ? "default" : "outline", onClick: () => setAllStickerAvailable(true), children: "Aan" }), _jsx(Button, { size: "sm", variant: allStickerUnavailable ? "default" : "outline", onClick: () => setAllStickerAvailable(false), children: "Uit" }), _jsx("span", { className: "text-xs text-muted-foreground ml-1", children: "Standaard: Aan" })] })] }), _jsx(Separator, {}), _jsx("div", { className: "space-y-3", children: selProducts.map((p) => {
407
+ const updateTier = (idx, patch) => {
408
+ const tiers = [...p.tiers];
409
+ tiers[idx] = { ...tiers[idx], ...patch };
410
+ const maxPrice = Math.max(p.crate_price, ...tiers.map((t) => t.price_per_crate));
411
+ updateProd(p.product_id, { tiers, normal_price: Math.max(p.normal_price, maxPrice) });
412
+ };
413
+ const addTier = () => {
414
+ const last = p.tiers[p.tiers.length - 1];
415
+ const nextMin = last ? last.min_crates + 5 : 1;
416
+ const basePrice = Math.max(p.crate_price, p.normal_price);
417
+ const newTiers = [...p.tiers, { min_crates: nextMin, price_per_crate: basePrice }];
418
+ updateProd(p.product_id, { tiers: newTiers, normal_price: Math.max(p.normal_price, basePrice) });
419
+ };
420
+ const removeTier = (idx) => {
421
+ const tiers = p.tiers.filter((_, i) => i !== idx);
422
+ const maxPrice = Math.max(p.crate_price, ...tiers.map((t) => t.price_per_crate));
423
+ updateProd(p.product_id, { tiers, normal_price: Math.max(p.normal_price, maxPrice) });
424
+ };
425
+ 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", { className: "flex items-start gap-3 min-w-0", children: [_jsx("div", { className: "h-14 w-14 shrink-0 rounded-md border bg-muted overflow-hidden flex items-center justify-center", children: p.image_url ? (_jsx("img", { src: p.image_url, alt: p.product_name, className: "h-full w-full object-cover" })) : (_jsx(Package, { className: "h-6 w-6 text-muted-foreground" })) }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "font-medium text-sm truncate", 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("span", { className: "text-sm font-medium", children: p.plants_per_crate || "—" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { className: "text-xs w-20 shrink-0", children: "Belading:" }), _jsx("span", { className: "text-sm font-medium", children: p.belading || "—" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { className: "text-xs w-24 shrink-0", children: "Barcode:" }), _jsx("span", { className: "text-sm font-medium", children: p.barcode || "—" })] }), _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) => {
426
+ const newPrice = Number(e.target.value);
427
+ const oldPrice = p.crate_price;
428
+ const tiers = p.tiers.map((t) => t.price_per_crate === oldPrice ? { ...t, price_per_crate: newPrice } : t);
429
+ const normalPrice = Math.max(p.normal_price, newPrice, ...tiers.map((t) => t.price_per_crate));
430
+ updateProd(p.product_id, { crate_price: newPrice, tiers, normal_price: normalPrice });
431
+ } })] })] }), _jsx("p", { className: "text-xs text-muted-foreground -mt-1", children: "Plantgegevens komen automatisch uit het product (voorraad) en zijn hier niet aanpasbaar." }), _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) => {
432
+ const pct = p.normal_price > 0 ? Math.round((1 - t.price_per_crate / p.normal_price) * 100) : 0;
433
+ 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));
434
+ })] }), _jsx(Textarea, { placeholder: "Omschrijving (optioneel)", rows: 2, value: p.description, onChange: (e) => updateProd(p.product_id, { description: e.target.value }) })] }, p.product_id));
435
+ }) })] }))] })] })), step === 2 && (_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: "Aan wie stuur je het?" })] }), _jsx(CardDescription, { children: "Kies de klanten die deze aanbieding ontvangen." }), _jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground pt-1", children: [_jsx(Info, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { children: "Er worden alleen klanten getoond met een e-mailadres. Klanten zonder e-mailadres zijn niet selecteerbaar." })] })] }), _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) => {
436
+ const allSel = g.customers.length > 0 && g.customers.every((c) => selCustomers.has(c.id));
437
+ 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));
438
+ }) })] })), _jsx(Separator, {}), _jsx("div", { className: "max-h-[500px] overflow-y-auto space-y-2 pr-1", children: filteredCustomers.slice(0, 500).map((c) => {
439
+ const checked = selCustomers.has(c.id);
440
+ 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: [_jsxs("div", { className: "font-medium truncate flex items-center gap-2", children: [_jsx("span", { className: "truncate", children: c.company_name }), c.source === "ext" && _jsx(Badge, { variant: "outline", className: "text-[10px] px-1.5 py-0", children: "Floriday" })] }), _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));
441
+ }) })] })] })), step === 3 && (_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: "Wat staat er in de mail?" })] }), _jsx(CardDescription, { children: "Pas het onderwerp en de boodschap aan \u2014 je ziet het direct in het voorbeeld." })] }), _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) })] }), _jsxs("div", { className: "md:col-span-2 flex items-start gap-2 rounded-md border bg-muted/40 p-3 text-xs text-muted-foreground", children: [_jsx(Info, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }), _jsx("span", { children: "Tip: een duidelijk onderwerp en korte samenvatting zorgen voor meer openingen." })] })] })] })), step === 4 && (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(CheckCircle2, { className: "h-5 w-5" }), _jsx(CardTitle, { children: "Controleren & versturen" })] }), _jsx(CardDescription, { children: "Controleer je aanbieding en verstuur of bewaar als concept." })] }), _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "grid gap-3 sm:grid-cols-3", children: [_jsxs("div", { className: "rounded-md border p-3", children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Producten" }), _jsx("div", { className: "text-xl font-semibold", children: selProducts.length })] }), _jsxs("div", { className: "rounded-md border p-3", children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Ontvangers" }), _jsx("div", { className: "text-xl font-semibold", children: selCustomers.size })] }), _jsxs("div", { className: "rounded-md border p-3", children: [_jsx("div", { className: "text-xs text-muted-foreground", children: "Onderwerp" }), _jsx("div", { className: "truncate text-sm font-medium", children: subject || "—" })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "text-sm font-medium", children: "Producten in deze aanbieding" }), selProducts.length === 0 ? (_jsx("div", { className: "text-sm text-muted-foreground", children: "Nog geen producten geselecteerd." })) : (_jsx("div", { className: "divide-y rounded-md border", children: selProducts.map((p) => (_jsxs("div", { className: "flex items-center justify-between gap-3 p-3 text-sm", children: [_jsx("span", { className: "truncate", children: p.product_name }), _jsxs("span", { className: "shrink-0 font-medium", children: ["\u20AC ", p.crate_price.toFixed(2)] })] }, p.product_id))) }))] }), _jsxs("div", { className: "flex flex-wrap 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"] })] })] })] }))] }), _jsx(EmailPreview, { subject: subject, intro: intro, fromName: fromName || (user?.email ?? ""), products: selProducts, recipientCount: selCustomers.size })] }), _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", { className: "min-w-0", children: [_jsx("div", { className: "truncate font-semibold", children: stepDef.title }), _jsxs("div", { className: "truncate text-sm text-muted-foreground", children: [selProducts.length, " product(en) \u2022 ", selCustomers.size, " klant(en)", nextHint ? ` • ${nextHint}` : ""] })] }), _jsxs("div", { className: "flex shrink-0 gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => (step === 1 ? navigate({ to: "/aanbiedingsmailtje/overzicht" }) : goTo(step - 1)), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-2" }), step === 1 ? "Annuleren" : "Vorige"] }), step < 4 ? (_jsxs(Button, { onClick: goNext, disabled: !canNext, children: ["Verder naar ", AANBIEDING_STEPS[step].title.toLowerCase(), _jsx(ArrowRight, { className: "h-4 w-4 ml-2" })] })) : (_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"] }))] })] }) })] }));
397
442
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ActiePage.d.ts","sourceRoot":"","sources":["../../src/components/ActiePage.tsx"],"names":[],"mappings":"AAoEA,wBAAgB,SAAS,gCAooBxB"}
1
+ {"version":3,"file":"ActiePage.d.ts","sourceRoot":"","sources":["../../src/components/ActiePage.tsx"],"names":[],"mappings":"AAqEA,wBAAgB,SAAS,gCAwrBxB"}
@@ -3,8 +3,8 @@ import { useEffect, useMemo, useState } from "react";
3
3
  import { useParams } from "@tanstack/react-router";
4
4
  import { useServerFn } from "@tanstack/react-start";
5
5
  import { Button, Input, Label, Textarea, Skeleton, toast } from "@flowselections/core";
6
- import { getOfferByLinkCode, trackOfferClick, placeOfferOrder, getOfferDeliveryOptions, } from "../lib/public-offer.functions";
7
- import { ShoppingCart, Check, User, Package, Truck, Minus, Plus, Send, Leaf, MapPin } from "lucide-react";
6
+ import { getOfferByLinkCode, trackOfferClick, placeOfferOrder, getOfferDeliveryOptions, getOfferBranding, } from "../lib/public-offer.functions";
7
+ import { ShoppingCart, Check, Package, Truck, Minus, Plus, Send, Leaf, MapPin } from "lucide-react";
8
8
  // ISO weekday: Ma=1 .. Zo=7
9
9
  function isoWeekday(d) {
10
10
  const js = d.getDay(); // 0..6, 0=Sun
@@ -64,6 +64,8 @@ export function ActiePage() {
64
64
  const trackFn = useServerFn(trackOfferClick);
65
65
  const orderFn = useServerFn(placeOfferOrder);
66
66
  const optionsFn = useServerFn(getOfferDeliveryOptions);
67
+ const brandingFn = useServerFn(getOfferBranding);
68
+ const [branding, setBranding] = useState(null);
67
69
  const [data, setData] = useState(null);
68
70
  const [loading, setLoading] = useState(true);
69
71
  const [qty, setQty] = useState({});
@@ -136,8 +138,24 @@ export function ActiePage() {
136
138
  const iv = setInterval(() => load(false), 15000);
137
139
  return () => { cancelled = true; clearInterval(iv); };
138
140
  }, [linkCode]);
141
+ // Huisstijl uit de instellingen (logo, kleuren, weergavenaam)
142
+ useEffect(() => {
143
+ let cancelled = false;
144
+ brandingFn()
145
+ .then((b) => { if (!cancelled && b)
146
+ setBranding(b); })
147
+ .catch(() => { });
148
+ return () => { cancelled = true; };
149
+ }, []);
150
+ const primaryColor = branding?.primary_color || "#16a34a";
151
+ const buttonColor = branding?.button_color || primaryColor;
152
+ const brandStyle = {
153
+ ["--offer-primary"]: primaryColor,
154
+ ["--offer-button"]: buttonColor,
155
+ };
156
+ const brandName = branding?.brand_name || data?.campaign?.from_name || "Flow Selections";
139
157
  if (loading) {
140
- 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" }) }) }));
158
+ return (_jsx("div", { className: "min-h-screen bg-[#f0fdf4] p-4 sm:p-6", children: _jsx("div", { className: "max-w-6xl mx-auto", children: _jsx(Skeleton, { className: "h-64" }) }) }));
141
159
  }
142
160
  if (!data) {
143
161
  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." })] }) }));
@@ -262,8 +280,13 @@ export function ActiePage() {
262
280
  if (placed) {
263
281
  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.orderNumber ? placed.orderNumber : `#${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." })] }) }));
264
282
  }
265
- const fromName = data?.campaign?.from_name ?? "";
266
- 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: () => {
283
+ return (_jsxs("div", { className: "min-h-screen bg-[#f0fdf4] flex flex-col", style: brandStyle, children: [_jsx("header", { className: "text-white", style: { backgroundColor: "var(--offer-primary)" }, children: _jsxs("div", { className: "max-w-6xl mx-auto px-4 sm:px-6 py-6 sm:py-10 text-center", children: [_jsxs("div", { className: "mx-auto mb-3 sm:mb-4 inline-flex max-w-full items-center justify-center gap-2 bg-white rounded-md px-3 py-2 shadow-sm", children: [branding?.logo_url ? (_jsx("img", { src: branding.logo_url, alt: brandName, className: "h-6 sm:h-7 w-auto max-w-[180px] object-contain" })) : (_jsx(Leaf, { className: "h-5 w-5 shrink-0", style: { color: "var(--offer-primary)" } })), _jsx("span", { className: "font-bold text-slate-900 tracking-tight truncate", children: brandName })] }), _jsxs("h1", { className: "text-2xl sm:text-3xl md:text-4xl font-extrabold tracking-tight break-words", children: ["\uD83C\uDF31 ", data?.campaign?.subject ?? "Speciale Aanbieding"] }), data?.campaign?.intro_message ? (_jsx("p", { className: "mt-3 text-white/90 text-sm sm:text-base whitespace-pre-line max-w-2xl mx-auto", children: data.campaign.intro_message })) : (_jsx("p", { className: "mt-3 text-white/90 text-sm sm:text-base", children: "Wij hebben een mooie aanbieding voor u!" }))] }) }), _jsxs("main", { className: "flex-1 max-w-6xl w-full mx-auto px-3 sm:px-6 py-6 sm:py-8 space-y-6 pb-28 lg:pb-8", children: [_jsxs("div", { className: "w-full text-left p-3 rounded-md border border-slate-200 bg-white text-sm flex items-center gap-3", children: [_jsx("div", { className: "h-10 w-10 rounded-md bg-primary text-white grid place-items-center text-sm font-bold shrink-0", children: data?.recipient?.recipient_name
284
+ ?.split(" ")
285
+ .map((w) => w[0])
286
+ .filter(Boolean)
287
+ .slice(0, 2)
288
+ .join("")
289
+ .toUpperCase() ?? "?" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "font-medium truncate text-slate-900", children: data?.recipient?.recipient_name }), _jsx("div", { className: "text-xs text-muted-foreground 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-lg sm:text-xl font-bold text-slate-900 mb-4", children: [_jsx(Package, { className: "h-5 w-5", style: { color: "var(--offer-primary)" } }), "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: () => {
267
290
  const next = { ...sticker };
268
291
  safeProducts.forEach((p) => { if (p.sticker_available !== false)
269
292
  next[p.id] = true; });
@@ -278,13 +301,13 @@ export function ActiePage() {
278
301
  const price = l.cratePrice > 0 ? l.cratePrice : Number(p.discount_price ?? 0);
279
302
  const showStrike = normal > 0 && normal > price && price > 0;
280
303
  const currentQty = l.q;
281
- 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("span", { className: "text-xs text-slate-500", children: "per fust" })] }), l.tiers.length > 0 && (_jsx("div", { className: "mt-1 flex flex-wrap gap-1", children: l.tiers.map((t) => (_jsxs("span", { className: `text-[11px] px-2 py-0.5 rounded border ${l.cratePrice === t.price_per_crate && l.priceSource === "tier"
304
+ 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-3 sm:p-4 flex gap-3 sm: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-16 h-16 sm:w-24 sm:h-24 rounded-lg object-cover border border-slate-200 bg-slate-50 ${l.soldOut ? "grayscale" : ""}` })) : (_jsx("div", { className: `w-16 h-16 sm:w-24 sm: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: "font-bold text-lg", style: { color: "var(--offer-primary)" }, children: fmtEuro(price) }), _jsx("span", { className: "text-xs text-slate-500", children: "per fust" })] }), l.tiers.length > 0 && (_jsx("div", { className: "mt-1 flex flex-wrap gap-1", children: l.tiers.map((t) => (_jsxs("span", { className: `text-[11px] px-2 py-0.5 rounded border ${l.cratePrice === t.price_per_crate && l.priceSource === "tier"
282
305
  ? "bg-green-100 border-green-300 text-green-800 font-semibold"
283
- : "bg-slate-50 border-slate-200 text-slate-600"}`, children: ["\u2265 ", t.min_crates, " fust: ", fmtEuro(t.price_per_crate)] }, t.min_crates))) })), _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) })] }), _jsxs("div", { className: "pt-1 text-[11px] text-slate-500 italic", children: ["Herkomst prijs: ", l.priceSourceLabel] })] }))] })] }), 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) => {
306
+ : "bg-slate-50 border-slate-200 text-slate-600"}`, children: ["\u2265 ", t.min_crates, " fust: ", fmtEuro(t.price_per_crate)] }, t.min_crates))) })), _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) })] }), _jsxs("div", { className: "pt-1 text-[11px] text-slate-500 italic", children: ["Herkomst prijs: ", l.priceSourceLabel] })] }))] })] }), 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-3 sm:px-4 py-3 flex flex-col sm:flex-row sm:items-center sm: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) => {
284
307
  if (["e", "E", "+", "-", ".", ","].includes(e.key))
285
308
  e.preventDefault();
286
- }, 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));
287
- }) })] }), _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) => {
309
+ }, className: "w-20 sm: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));
310
+ }) })] }), _jsx("aside", { id: "bestel-formulier", className: "lg:sticky lg:top-6 self-start scroll-mt-4", children: _jsxs("div", { className: "bg-white rounded-xl border border-slate-200 shadow-sm p-4 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", style: { color: "var(--offer-primary)" } }), "Uw bestelling"] }), hasItems ? (_jsxs("div", { className: "space-y-2 text-sm", children: [safeProducts.filter((p) => (qty[p.id] ?? 0) > 0).map((p) => {
288
311
  const l = lineFor(p);
289
312
  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));
290
313
  }), _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(MapPin, { className: "h-4 w-4 text-green-700" }), "Aflevertuin"] }), _jsx("div", { className: "rounded-md border border-slate-200 bg-white max-h-64 overflow-auto divide-y divide-slate-100", children: (options?.delivery_gardens ?? []).length === 0 ? (_jsx("div", { className: "px-3 py-3 text-sm text-slate-500", children: "Geen aflevertuinen bekend voor deze klant." })) : ((options?.delivery_gardens ?? []).map((g) => {
@@ -306,5 +329,7 @@ export function ActiePage() {
306
329
  }
307
330
  }
308
331
  setDeliveryDate(v);
309
- } }), !deliveryGardenId ? (_jsx("p", { className: "text-xs text-amber-700", children: "Kies eerst een aflevertuin om de eerst mogelijke leverdatum te berekenen." })) : minDeliveryDate ? (_jsxs("p", { className: "text-xs text-slate-600", children: ["Eerst mogelijk: ", _jsx("strong", { children: formatDateNL(minDeliveryDate) })] })) : (_jsx("p", { className: "text-xs text-red-700", children: "Er kon geen leverdatum worden berekend \u2014 controleer de vervoerdersinstellingen." }))] }), _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 || !deliveryGardenId || !deliveryDate, 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"] })] }));
332
+ } }), !deliveryGardenId ? (_jsx("p", { className: "text-xs text-amber-700", children: "Kies eerst een aflevertuin om de eerst mogelijke leverdatum te berekenen." })) : minDeliveryDate ? (_jsxs("p", { className: "text-xs text-slate-600", children: ["Eerst mogelijk: ", _jsx("strong", { children: formatDateNL(minDeliveryDate) })] })) : (_jsx("p", { className: "text-xs text-red-700", children: "Er kon geen leverdatum worden berekend \u2014 controleer de vervoerdersinstellingen." }))] }), _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 || !deliveryGardenId || !deliveryDate, className: "w-full text-white hover:opacity-90", style: { backgroundColor: "var(--offer-button)" }, size: "lg", children: [_jsx(Send, { className: "h-4 w-4 mr-2" }), busy ? "Bezig..." : "Bestelling plaatsen"] })] }) })] })] }), hasItems && (_jsxs("div", { className: "lg:hidden fixed bottom-0 inset-x-0 z-40 border-t border-slate-200 bg-white/95 backdrop-blur px-3 py-2.5 flex items-center justify-between gap-3 shadow-[0_-2px_10px_rgba(0,0,0,0.06)]", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-[11px] text-slate-500", children: "Totaal" }), _jsx("div", { className: "font-bold truncate", style: { color: "var(--offer-primary)" }, children: fmtEuro(totalAmount) })] }), _jsx(Button, { type: "button", className: "text-white shrink-0", style: { backgroundColor: "var(--offer-button)" }, onClick: () => {
333
+ document.getElementById("bestel-formulier")?.scrollIntoView({ behavior: "smooth", block: "start" });
334
+ }, children: "Naar bestellen" })] })), _jsxs("footer", { className: "bg-slate-900 text-slate-300 text-center py-5 text-sm", children: ["\u00A9 ", new Date().getFullYear(), " ", brandName] })] }));
310
335
  }
@@ -1 +1 @@
1
- {"version":3,"file":"RapportagePage.d.ts","sourceRoot":"","sources":["../../src/components/RapportagePage.tsx"],"names":[],"mappings":"AAQA,wBAAgB,cAAc,gCAiG7B"}
1
+ {"version":3,"file":"RapportagePage.d.ts","sourceRoot":"","sources":["../../src/components/RapportagePage.tsx"],"names":[],"mappings":"AAQA,wBAAgB,cAAc,gCAmI7B"}
@@ -11,24 +11,52 @@ export function RapportagePage() {
11
11
  const [range, setRange] = useState(null);
12
12
  const [data, setData] = useState(null);
13
13
  const [loading, setLoading] = useState(false);
14
+ const [error, setError] = useState(null);
15
+ const [reloadKey, setReloadKey] = useState(0);
14
16
  useEffect(() => { setRange(loadStoredRange()); }, []);
15
17
  useEffect(() => {
16
18
  if (!range)
17
19
  return;
20
+ let cancelled = false;
18
21
  setLoading(true);
22
+ setError(null);
19
23
  const payload = {};
20
24
  if (range.from)
21
25
  payload.from = range.from.toISOString();
22
26
  if (range.to)
23
27
  payload.to = range.to.toISOString();
24
- fn({ data: payload }).then((r) => { setData(r); setLoading(false); });
25
- }, [range]);
28
+ Promise.resolve()
29
+ .then(() => fn({ data: payload }))
30
+ .then((r) => { if (!cancelled)
31
+ setData(r ?? {}); })
32
+ .catch(async (e) => {
33
+ if (cancelled)
34
+ return;
35
+ let msg = "Rapportage kon niet worden geladen";
36
+ if (typeof Response !== "undefined" && e instanceof Response) {
37
+ if (e.status === 401)
38
+ msg = "Je sessie is verlopen. Log opnieuw in om de rapportage te bekijken.";
39
+ else {
40
+ const txt = await e.text().catch(() => "");
41
+ msg = txt || `Serverfout (${e.status})`;
42
+ }
43
+ }
44
+ else if (e?.message) {
45
+ msg = e.message;
46
+ }
47
+ setError(msg);
48
+ })
49
+ .finally(() => { if (!cancelled)
50
+ setLoading(false); });
51
+ return () => { cancelled = true; };
52
+ }, [range, reloadKey]);
26
53
  const handleChange = (r) => { storeRange(r); setRange(r); };
27
54
  const { topCustomers, topProducts } = useMemo(() => {
28
- if (!data)
55
+ const orders = Array.isArray(data?.orders) ? data.orders : [];
56
+ if (orders.length === 0)
29
57
  return { topCustomers: [], topProducts: [] };
30
58
  const byCustomer = new Map();
31
- for (const o of data.orders) {
59
+ for (const o of orders) {
32
60
  const key = o.customer_id ?? o.customer_name;
33
61
  const cur = byCustomer.get(key) ?? { name: o.customer_name, orders: 0, revenue: 0 };
34
62
  cur.orders += 1;
@@ -36,7 +64,7 @@ export function RapportagePage() {
36
64
  byCustomer.set(key, cur);
37
65
  }
38
66
  const byProduct = new Map();
39
- for (const o of data.orders) {
67
+ for (const o of orders) {
40
68
  for (const it of (Array.isArray(o.items) ? o.items : [])) {
41
69
  const cur = byProduct.get(it.product_name) ?? { name: it.product_name, qty: 0, revenue: 0 };
42
70
  cur.qty += Number(it.quantity ?? 0);
@@ -49,10 +77,12 @@ export function RapportagePage() {
49
77
  topProducts: [...byProduct.values()].sort((a, b) => b.qty - a.qty).slice(0, 10),
50
78
  };
51
79
  }, [data]);
80
+ if (error)
81
+ return (_jsxs("main", { className: "p-6 space-y-3", 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"] }) }), _jsx(Card, { children: _jsxs(CardContent, { className: "p-6 space-y-3", children: [_jsx("p", { className: "font-medium", children: "Rapportage kon niet worden geladen" }), _jsx("p", { className: "text-sm text-muted-foreground", children: error }), _jsx(Button, { size: "sm", onClick: () => setReloadKey((k) => k + 1), children: "Opnieuw proberen" })] }) })] }));
52
82
  if (!range || !data)
53
83
  return _jsx("div", { className: "p-6", children: _jsx(Skeleton, { className: "h-64" }) });
54
- const t = data.totals;
55
- const totalRecords = t.sent + t.orders;
84
+ const t = { campaigns: 0, sent: 0, opened: 0, clicked: 0, orders: 0, revenue: 0, ...(data?.totals ?? {}) };
85
+ const totalRecords = Number(t.sent ?? 0) + Number(t.orders ?? 0);
56
86
  return (_jsxs("main", { className: "p-6 space-y-6", 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 items-center justify-between gap-2 flex-wrap", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(BarChart3, { className: "h-6 w-6 text-primary" }), _jsx("h2", { className: "text-2xl font-bold", children: "Rapportage" })] }), range && _jsx(DateRangePicker, { value: range, onChange: handleChange })] }), _jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx("span", { children: "Actieve periode:" }), _jsx(Badge, { variant: "secondary", className: "font-medium", children: range.label }), _jsxs("span", { className: "text-xs", children: ["\u00B7 ", totalRecords, " records ", loading && "(bijwerken…)"] })] }), _jsxs("div", { className: "grid grid-cols-2 md:grid-cols-6 gap-3", children: [_jsx(Stat, { icon: _jsx(Mail, {}), label: "Verzonden", value: t.sent }), _jsx(Stat, { icon: _jsx(Eye, {}), label: "Geopend", value: t.opened }), _jsx(Stat, { icon: _jsx(MousePointerClick, {}), label: "Klikken", value: t.clicked }), _jsx(Stat, { icon: _jsx(ShoppingCart, {}), label: "Bestellingen", value: t.orders }), _jsx(Stat, { icon: _jsx(Euro, {}), label: "Omzet", value: `€ ${t.revenue.toFixed(2)}` }), _jsx(Stat, { icon: _jsx(BarChart3, {}), label: "Campagnes", value: t.campaigns })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Top klanten op omzet" }) }), _jsx(CardContent, { children: topCustomers.length === 0 ? _jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen bestellingen in deze periode" }) : (_jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "text-left text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "py-1", children: "Klant" }), _jsx("th", { children: "Bestellingen" }), _jsx("th", { className: "text-right", children: "Omzet" })] }) }), _jsx("tbody", { children: topCustomers.map((c, i) => (_jsxs("tr", { className: "border-t", children: [_jsx("td", { className: "py-2", children: c.name }), _jsx("td", { children: c.orders }), _jsxs("td", { className: "text-right", children: ["\u20AC ", c.revenue.toFixed(2)] })] }, i))) })] })) })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Best verkochte actieproducten" }) }), _jsx(CardContent, { children: topProducts.length === 0 ? _jsx("p", { className: "text-sm text-muted-foreground", children: "Nog geen bestellingen in deze periode" }) : (_jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "text-left text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "py-1", children: "Product" }), _jsx("th", { children: "Aantal" }), _jsx("th", { className: "text-right", children: "Omzet" })] }) }), _jsx("tbody", { children: topProducts.map((p, i) => (_jsxs("tr", { className: "border-t", children: [_jsx("td", { className: "py-2", children: p.name }), _jsx("td", { children: p.qty }), _jsxs("td", { className: "text-right", children: ["\u20AC ", p.revenue.toFixed(2)] })] }, i))) })] })) })] })] }));
57
87
  }
58
88
  function Stat({ icon, label, value }) {
@@ -0,0 +1,22 @@
1
+ export interface EmailPreviewProduct {
2
+ product_id: string;
3
+ product_name: string;
4
+ description?: string;
5
+ image_url: string | null;
6
+ normal_price: number;
7
+ crate_price: number;
8
+ tiers?: Array<{
9
+ min_crates: number;
10
+ price_per_crate: number;
11
+ }>;
12
+ }
13
+ interface EmailPreviewProps {
14
+ subject: string;
15
+ intro: string;
16
+ fromName: string;
17
+ products: EmailPreviewProduct[];
18
+ recipientCount: number;
19
+ }
20
+ export declare function EmailPreview({ subject, intro, fromName, products, recipientCount }: EmailPreviewProps): import("react").JSX.Element;
21
+ export {};
22
+ //# sourceMappingURL=EmailPreview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EmailPreview.d.ts","sourceRoot":"","sources":["../../../src/components/aanbieding/EmailPreview.tsx"],"names":[],"mappings":"AAIA,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChE;AAED,UAAU,iBAAiB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,cAAc,EAAE,MAAM,CAAC;CACxB;AAID,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,cAAc,EAAE,EAAE,iBAAiB,+BAqIrG"}