@flowselections/actie-mail-kwekers-erp-mathot 1.0.16 → 1.0.18

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 (57) hide show
  1. package/dist-lib/components/AanbiedingsmailtjePage.d.ts.map +1 -1
  2. package/dist-lib/components/AanbiedingsmailtjePage.js +177 -41
  3. package/dist-lib/components/ActiePage.d.ts.map +1 -1
  4. package/dist-lib/components/ActiePage.js +30 -32
  5. package/dist-lib/components/CampagneOverzicht.d.ts.map +1 -1
  6. package/dist-lib/components/CampagneOverzicht.js +117 -10
  7. package/dist-lib/components/RapportagePage.d.ts +4 -0
  8. package/dist-lib/components/RapportagePage.d.ts.map +1 -1
  9. package/dist-lib/components/RapportagePage.js +11 -86
  10. package/dist-lib/components/aanbieding/EmailPreview.d.ts +6 -1
  11. package/dist-lib/components/aanbieding/EmailPreview.d.ts.map +1 -1
  12. package/dist-lib/components/aanbieding/EmailPreview.js +18 -6
  13. package/dist-lib/components/overzicht/PrestatieTegels.d.ts +13 -0
  14. package/dist-lib/components/overzicht/PrestatieTegels.d.ts.map +1 -0
  15. package/dist-lib/components/overzicht/PrestatieTegels.js +22 -0
  16. package/dist-lib/components/overzicht/RecenteCampagnes.d.ts +22 -0
  17. package/dist-lib/components/overzicht/RecenteCampagnes.d.ts.map +1 -0
  18. package/dist-lib/components/overzicht/RecenteCampagnes.js +15 -0
  19. package/dist-lib/components/overzicht/TopLijsten.d.ts +15 -0
  20. package/dist-lib/components/overzicht/TopLijsten.d.ts.map +1 -0
  21. package/dist-lib/components/overzicht/TopLijsten.js +16 -0
  22. package/dist-lib/components/overzicht/campaignStatus.d.ts +22 -0
  23. package/dist-lib/components/overzicht/campaignStatus.d.ts.map +1 -0
  24. package/dist-lib/components/overzicht/campaignStatus.js +47 -0
  25. package/dist-lib/components/settings/HuisstijlSettingsCard.d.ts.map +1 -1
  26. package/dist-lib/components/settings/HuisstijlSettingsCard.js +122 -9
  27. package/dist-lib/index.d.ts +6 -0
  28. package/dist-lib/index.d.ts.map +1 -1
  29. package/dist-lib/index.js +6 -0
  30. package/dist-lib/integrations/supabase/auth-middleware.d.ts +299 -5
  31. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  32. package/dist-lib/integrations/supabase/auth-middleware.js +4 -7
  33. package/dist-lib/integrations/supabase/client.d.ts +298 -4
  34. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  35. package/dist-lib/integrations/supabase/client.server.d.ts +298 -4
  36. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  37. package/dist-lib/integrations/supabase/types.d.ts +299 -5
  38. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  39. package/dist-lib/integrations/supabase/types.js +12 -1
  40. package/dist-lib/lib/branding.d.ts +29 -0
  41. package/dist-lib/lib/branding.d.ts.map +1 -0
  42. package/dist-lib/lib/branding.js +70 -0
  43. package/dist-lib/lib/campaigns.functions.d.ts +1536 -66
  44. package/dist-lib/lib/campaigns.functions.d.ts.map +1 -1
  45. package/dist-lib/lib/campaigns.functions.js +22 -13
  46. package/dist-lib/lib/pricing.d.ts +66 -0
  47. package/dist-lib/lib/pricing.d.ts.map +1 -0
  48. package/dist-lib/lib/pricing.js +132 -0
  49. package/dist-lib/lib/public-offer.functions.d.ts.map +1 -1
  50. package/dist-lib/lib/public-offer.functions.js +20 -2
  51. package/dist-lib/lib/soft-auth.server.d.ts +11 -0
  52. package/dist-lib/lib/soft-auth.server.d.ts.map +1 -0
  53. package/dist-lib/lib/soft-auth.server.js +27 -0
  54. package/dist-lib/lib/stats.functions.d.ts +332 -20447
  55. package/dist-lib/lib/stats.functions.d.ts.map +1 -1
  56. package/dist-lib/lib/stats.functions.js +98 -29
  57. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"AanbiedingsmailtjePage.d.ts","sourceRoot":"","sources":["../../src/components/AanbiedingsmailtjePage.tsx"],"names":[],"mappings":"AA4CA,wBAAgB,sBAAsB,gCA8zBrC"}
1
+ {"version":3,"file":"AanbiedingsmailtjePage.d.ts","sourceRoot":"","sources":["../../src/components/AanbiedingsmailtjePage.tsx"],"names":[],"mappings":"AAqDA,wBAAgB,sBAAsB,gCAmgCrC"}
@@ -3,11 +3,13 @@ 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, Info, ArrowLeft, ArrowRight, CheckCircle2 } from "lucide-react";
6
+ import { Mail, Send, Search, X, Package, Users, Percent, Tag, BarChart3, Info, ArrowLeft, ArrowRight, CheckCircle2, Check } from "lucide-react";
7
7
  import { createCampaign, sendCampaign } from "../lib/campaigns.functions";
8
8
  import { createVoorraadStockMap, voorraadAvailableStock } from "../lib/voorraad-service";
9
9
  import { WizardSteps, AANBIEDING_STEPS } from "../components/aanbieding/WizardSteps";
10
10
  import { EmailPreview } from "../components/aanbieding/EmailPreview";
11
+ import { resolveBranding } from "../lib/branding";
12
+ import { round2, sortTiers, validateTiers, discountPercent, applyPercentDiscount, applyAmountDiscount, discountTiersByPercent, discountTiersByAmount, } from "../lib/pricing";
11
13
  export function AanbiedingsmailtjePage() {
12
14
  const { user } = useAuth();
13
15
  const navigate = useNavigate();
@@ -31,6 +33,9 @@ export function AanbiedingsmailtjePage() {
31
33
  const [productSearchFocused, setProductSearchFocused] = useState(false);
32
34
  const [customerSearch, setCustomerSearch] = useState("");
33
35
  const [bulkPrice, setBulkPrice] = useState("");
36
+ const [prodPct, setProdPct] = useState({});
37
+ const [prodEur, setProdEur] = useState({});
38
+ const [bulkEur, setBulkEur] = useState("");
34
39
  const BULK_STICKER_PREF_KEY = "aanbiedingsmailtje:bulkStickerOn";
35
40
  const [bulkStickerOn, setBulkStickerOn] = useState(() => {
36
41
  if (typeof window === "undefined")
@@ -38,6 +43,7 @@ export function AanbiedingsmailtjePage() {
38
43
  const v = window.localStorage.getItem(BULK_STICKER_PREF_KEY);
39
44
  return v === null ? true : v === "true";
40
45
  });
46
+ const [branding, setBranding] = useState(() => resolveBranding(null));
41
47
  useEffect(() => {
42
48
  if (typeof window !== "undefined") {
43
49
  window.localStorage.setItem(BULK_STICKER_PREF_KEY, String(bulkStickerOn));
@@ -88,7 +94,7 @@ export function AanbiedingsmailtjePage() {
88
94
  sb.from("products").select("id, product, sale_price, quantity, image_url, barcode, plants_per_crate, crates_per_layer, layers_per_cart, trade_item_key").order("product").limit(5000),
89
95
  sb.from("customers").select("id, company_name, email, customer_class, country").not("email", "is", null).order("company_name").limit(1000),
90
96
  sb.from("external_customers_grouped").select("group_key, display_name, email, country, is_active").not("email", "is", null).eq("is_active", true).limit(2000),
91
- sb.from("offer_settings").select("default_subject, default_intro, default_sticker_cost, show_customer_classes").maybeSingle(),
97
+ sb.from("offer_settings").select("default_subject, default_intro, default_sticker_cost, show_customer_classes, brand_name, logo_url, primary_color, button_color").maybeSingle(),
92
98
  ]);
93
99
  const stockMap = createVoorraadStockMap((Array.isArray(stockRows) ? stockRows : []));
94
100
  const stockFor = (key) => {
@@ -223,6 +229,7 @@ export function AanbiedingsmailtjePage() {
223
229
  }
224
230
  if (typeof settings.show_customer_classes === "boolean")
225
231
  setShowCustomerClasses(settings.show_customer_classes);
232
+ setBranding(resolveBranding(settings));
226
233
  }
227
234
  if (!activeId) {
228
235
  toast.error("Geen actieve Floriday-koppeling — stel deze in via de Voorraad Module");
@@ -230,10 +237,17 @@ export function AanbiedingsmailtjePage() {
230
237
  setLoading(false);
231
238
  })();
232
239
  }, []);
240
+ const toggleProduct = (p) => {
241
+ const id = p.id ?? `tmp:${p.product}`;
242
+ if (selProducts.some((sp) => sp.product_id === id)) {
243
+ removeProduct(id);
244
+ return;
245
+ }
246
+ addProduct(p);
247
+ };
233
248
  const addProduct = (p) => {
234
249
  const existing = selProducts.find((sp) => sp.product_id === p.id);
235
250
  if (existing) {
236
- toast.info("Dit product is al geselecteerd");
237
251
  return;
238
252
  }
239
253
  setSelProducts((prev) => [...prev, {
@@ -248,36 +262,130 @@ export function AanbiedingsmailtjePage() {
248
262
  belading: p.belading ?? "",
249
263
  barcode: p.barcode ?? "",
250
264
  crate_price: 0,
265
+ base_crate_price: 0,
251
266
  sticker_cost: null,
252
267
  sticker_available: true,
253
268
  tiers: [{ min_crates: 1, price_per_crate: 0 }],
269
+ base_tiers: [{ min_crates: 1, price_per_crate: 0 }],
254
270
  }]);
255
- setProductSearch("");
256
- setProductSearchFocused(false);
257
271
  };
258
272
  const removeProduct = (id) => setSelProducts((prev) => prev.filter((p) => p.product_id !== id));
259
273
  const updateProd = (id, patch) => setSelProducts((prev) => prev.map((p) => (p.product_id === id ? { ...p, ...patch } : p)));
274
+ /** Originele (ongekorte) referenties van een product. */
275
+ const baseOf = (p) => {
276
+ const baseTiers = (p.base_tiers && p.base_tiers.length > 0 ? p.base_tiers : p.tiers) ?? [];
277
+ const baseCrate = p.base_crate_price > 0
278
+ ? p.base_crate_price
279
+ : Math.max(p.crate_price, p.normal_price, 0);
280
+ return { baseTiers, baseCrate };
281
+ };
282
+ const hasPrice = (p) => {
283
+ const { baseTiers, baseCrate } = baseOf(p);
284
+ return baseCrate > 0 || baseTiers.some((t) => Number(t.price_per_crate) > 0);
285
+ };
286
+ /** Percentage-korting op fustprijs én op elke staffel afzonderlijk. */
287
+ const discountProduct = (p, pct) => {
288
+ const { baseTiers, baseCrate } = baseOf(p);
289
+ const newCrate = applyPercentDiscount(baseCrate, pct);
290
+ return {
291
+ ...p,
292
+ base_crate_price: baseCrate,
293
+ base_tiers: baseTiers,
294
+ normal_price: baseCrate,
295
+ crate_price: newCrate,
296
+ discount_price: newCrate,
297
+ tiers: discountTiersByPercent(baseTiers, pct),
298
+ };
299
+ };
300
+ /** Euro-korting op fustprijs én op elke staffel afzonderlijk. */
301
+ const discountProductAmount = (p, eur) => {
302
+ const { baseTiers, baseCrate } = baseOf(p);
303
+ const newCrate = applyAmountDiscount(baseCrate, eur);
304
+ return {
305
+ ...p,
306
+ base_crate_price: baseCrate,
307
+ base_tiers: baseTiers,
308
+ normal_price: baseCrate,
309
+ crate_price: newCrate,
310
+ discount_price: newCrate,
311
+ tiers: discountTiersByAmount(baseTiers, eur),
312
+ };
313
+ };
260
314
  const applyDiscount = (pct) => {
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) {
315
+ if (!Number.isFinite(pct) || pct <= 0 || pct >= 100) {
316
+ toast.error("Vul een kortingspercentage tussen 1 en 99 in");
317
+ return;
318
+ }
319
+ if (!selProducts.some(hasPrice)) {
263
320
  toast.error("Vul eerst een fustprijs in voordat je korting toepast");
264
321
  return;
265
322
  }
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
- }));
323
+ setSelProducts((prev) => prev.map((p) => (hasPrice(p) ? discountProduct(p, pct) : p)));
279
324
  toast.success(`${pct}% korting toegepast`);
280
325
  };
326
+ const applyProductDiscount = (id, pct) => {
327
+ if (!Number.isFinite(pct) || pct <= 0 || pct >= 100) {
328
+ toast.error("Vul een kortingspercentage tussen 1 en 99 in");
329
+ return;
330
+ }
331
+ const prod = selProducts.find((p) => p.product_id === id);
332
+ if (!prod)
333
+ return;
334
+ if (!hasPrice(prod)) {
335
+ toast.error("Vul eerst een fustprijs in voordat je korting toepast");
336
+ return;
337
+ }
338
+ updateProd(id, discountProduct(prod, pct));
339
+ toast.success(`${pct}% korting toegepast op ${prod.product_name}`);
340
+ };
341
+ /** Laagste originele prijs waarop een euro-korting van toepassing is. */
342
+ const lowestBasePrice = (p) => {
343
+ const { baseTiers, baseCrate } = baseOf(p);
344
+ const candidates = [
345
+ ...(baseCrate > 0 ? [baseCrate] : []),
346
+ ...baseTiers.map((t) => Number(t.price_per_crate)).filter((n) => n > 0),
347
+ ];
348
+ return candidates.length > 0 ? Math.min(...candidates) : 0;
349
+ };
350
+ /** Korting als vast eurobedrag per fust — voor alle producten. */
351
+ const applyDiscountAmount = (eur) => {
352
+ if (!Number.isFinite(eur) || eur <= 0) {
353
+ toast.error("Vul een kortingsbedrag in euro's in");
354
+ return;
355
+ }
356
+ const applicable = selProducts.filter(hasPrice);
357
+ if (applicable.length === 0) {
358
+ toast.error("Vul eerst een fustprijs in voordat je korting toepast");
359
+ return;
360
+ }
361
+ const tooHigh = applicable.find((p) => eur >= lowestBasePrice(p));
362
+ if (tooHigh) {
363
+ toast.error(`Kortingsbedrag is hoger dan de laagste prijs van ${tooHigh.product_name}`);
364
+ return;
365
+ }
366
+ setSelProducts((prev) => prev.map((p) => (hasPrice(p) ? discountProductAmount(p, eur) : p)));
367
+ toast.success(`€ ${eur.toFixed(2)} korting toegepast`);
368
+ };
369
+ /** Korting als vast eurobedrag per fust — voor één product. */
370
+ const applyProductDiscountAmount = (id, eur) => {
371
+ if (!Number.isFinite(eur) || eur <= 0) {
372
+ toast.error("Vul een kortingsbedrag in euro's in");
373
+ return;
374
+ }
375
+ const prod = selProducts.find((p) => p.product_id === id);
376
+ if (!prod)
377
+ return;
378
+ if (!hasPrice(prod)) {
379
+ toast.error("Vul eerst een fustprijs in voordat je korting toepast");
380
+ return;
381
+ }
382
+ if (eur >= lowestBasePrice(prod)) {
383
+ toast.error("Het kortingsbedrag is hoger dan de laagste (staffel)prijs");
384
+ return;
385
+ }
386
+ updateProd(id, discountProductAmount(prod, eur));
387
+ toast.success(`€ ${eur.toFixed(2)} korting toegepast op ${prod.product_name}`);
388
+ };
281
389
  const setAllStickerAvailable = (available) => {
282
390
  setSelProducts((prev) => prev.map((p) => ({ ...p, sticker_available: available })));
283
391
  toast.success(`Stickeropdruk ${available ? "ingeschakeld" : "uitgeschakeld"} voor het product`);
@@ -348,10 +456,22 @@ export function AanbiedingsmailtjePage() {
348
456
  recipient_name: c.company_name,
349
457
  recipient_email: c.email,
350
458
  }));
459
+ // Blokkeer ongeldige staffels (bijv. dubbele "vanaf"-aantallen) vóór opslaan.
460
+ for (const p of selProducts) {
461
+ const blocking = validateTiers(p.tiers).find((i) => i.level === "error");
462
+ if (blocking) {
463
+ toast.error(`${p.product_name}: ${blocking.message}`);
464
+ setBusy(false);
465
+ return;
466
+ }
467
+ }
351
468
  const res = await createFn({ data: {
352
469
  name, subject, from_name: fromName || (user?.email ?? "FlowSelections"),
353
470
  from_email: fromEmail, intro_message: intro,
354
- products: selProducts.map((p, i) => ({ ...p, sort_order: i })),
471
+ products: selProducts.map((p, i) => {
472
+ const { base_tiers: _bt, base_crate_price: _bc, ...rest } = p;
473
+ return { ...rest, tiers: sortTiers(p.tiers), sort_order: i };
474
+ }),
355
475
  recipients,
356
476
  } });
357
477
  if (andSend) {
@@ -392,7 +512,11 @@ export function AanbiedingsmailtjePage() {
392
512
  }
393
513
  goTo(step + 1);
394
514
  };
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) => {
515
+ return (_jsxs("main", { className: "p-6 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" })] }), _jsx("div", { className: "flex items-center gap-2", children: _jsxs(Button, { variant: "outline", onClick: () => navigate({ to: "/aanbiedingsmailtje/overzicht" }), children: [_jsx(BarChart3, { className: "h-4 w-4 mr-2" }), "Campagne overzicht"] }) })] }), _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: "space-y-1.5", children: [_jsx(Label, { children: "Naam van deze aanbieding" }), _jsx(Input, { value: name, onChange: (e) => setName(e.target.value), placeholder: "Bijv. Weekaanbieding groene planten week 34" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Deze naam zie je terug in het overzicht \u2014 kies iets herkenbaars." })] }), _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) => {
516
+ const pid = p.id ?? `tmp:${p.product}`;
517
+ const isSel = selProducts.some((sp) => sp.product_id === pid);
518
+ return (_jsxs("button", { type: "button", onMouseDown: (e) => { e.preventDefault(); toggleProduct(p); }, className: `w-full text-left px-3 py-2 text-sm flex items-center gap-3 ${isSel ? "bg-primary/5" : "hover:bg-muted"}`, children: [_jsx("span", { className: `flex h-5 w-5 shrink-0 items-center justify-center rounded border ${isSel ? "border-primary bg-primary text-primary-foreground" : "border-input bg-background"}`, "aria-hidden": true, children: isSel && _jsx(Check, { className: "h-3.5 w-3.5" }) }), _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 ${isSel ? "font-semibold text-primary" : "font-medium"}`, children: p.product }), _jsx("div", { className: "text-xs text-muted-foreground truncate", children: p.is_draft ? "Concept" : `Voorraad: ${p.quantity ?? 0}` })] }), isSel && _jsx("span", { className: "shrink-0 text-xs font-medium text-primary", children: "Geselecteerd" })] }, `${p.id ?? "x"}-${i}`));
519
+ }), 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", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: "Korting in euro's (per fust):" }), _jsx(Input, { type: "number", step: "0.01", min: "0.01", placeholder: "\u20AC 0,00", className: "w-24 h-8", value: bulkEur, onChange: (e) => setBulkEur(e.target.value) }), _jsx(Button, { type: "button", size: "sm", variant: "secondary", onClick: () => applyDiscountAmount(Number((bulkEur || "").replace(",", "."))), children: "Toepassen op alle producten" })] }), _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: "Kosten stickeropdruk (\u20AC per stuk): \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
520
  setBulkStickerOn(checked);
397
521
  if (!checked) {
398
522
  setSelProducts((prev) => prev.map((p) => ({ ...p, sticker_cost: null })));
@@ -403,40 +527,52 @@ export function AanbiedingsmailtjePage() {
403
527
  setSelProducts((prev) => prev.map((p) => ({ ...p, sticker_cost: v })));
404
528
  }
405
529
  }
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) => {
530
+ } }), _jsxs(Label, { htmlFor: "bulk-sticker-toggle", className: "text-xs cursor-pointer", children: ["Toepassen op producten \u2014 ", bulkStickerOn ? "Aan" : "Uit"] })] }), _jsx("p", { className: "w-full text-[11px] text-muted-foreground", children: "Dit bedrag geldt voor alle geselecteerde producten. Per product kun je de opdruk uitzetten bij \"Stickeropdruk beschikbaar\"." })] }), _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) => {
531
+ // Handmatig aangepaste staffelprijzen worden de nieuwe "originele" staffel,
532
+ // zodat een korting daarna niet stapelt.
407
533
  const updateTier = (idx, patch) => {
408
534
  const tiers = [...p.tiers];
409
535
  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) });
536
+ const base = [...(p.base_tiers ?? p.tiers)];
537
+ base[idx] = { ...(base[idx] ?? tiers[idx]), ...patch };
538
+ updateProd(p.product_id, { tiers, base_tiers: base });
412
539
  };
413
540
  const addTier = () => {
414
- const last = p.tiers[p.tiers.length - 1];
541
+ const sorted = sortTiers(p.tiers);
542
+ const last = sorted[sorted.length - 1];
415
543
  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) });
544
+ const basePrice = p.base_crate_price > 0 ? p.base_crate_price : (last?.price_per_crate ?? 0);
545
+ const newTier = { min_crates: nextMin, price_per_crate: basePrice };
546
+ updateProd(p.product_id, {
547
+ tiers: [...p.tiers, newTier],
548
+ base_tiers: [...(p.base_tiers ?? p.tiers), newTier],
549
+ });
419
550
  };
420
551
  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) });
552
+ updateProd(p.product_id, {
553
+ tiers: p.tiers.filter((_, i) => i !== idx),
554
+ base_tiers: (p.base_tiers ?? p.tiers).filter((_, i) => i !== idx),
555
+ });
424
556
  };
557
+ const tierIssues = validateTiers(p.tiers);
425
558
  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;
559
+ const newPrice = round2(Number(e.target.value));
560
+ // Fustprijs is de originele adviesprijs; staffelprijzen blijven ongemoeid.
561
+ updateProd(p.product_id, {
562
+ crate_price: newPrice,
563
+ base_crate_price: newPrice,
564
+ normal_price: newPrice,
565
+ discount_price: 0,
566
+ });
567
+ } })] })] }), _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 flex-wrap items-center gap-2 border-t pt-2", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: "Korting dit product:" }), [10, 15, 20, 25, 30].map((pct) => (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => applyProductDiscount(p.product_id, pct), children: [_jsx(Percent, { className: "h-3 w-3 mr-1" }), pct, "%"] }, pct))), _jsx(Input, { type: "number", min: 1, max: 99, placeholder: "%", className: "w-20 h-8", value: prodPct[p.product_id] ?? "", onChange: (e) => setProdPct((prev) => ({ ...prev, [p.product_id]: e.target.value })) }), _jsx(Button, { type: "button", size: "sm", variant: "secondary", onClick: () => applyProductDiscount(p.product_id, Number((prodPct[p.product_id] || "").replace(",", "."))), children: "Toepassen" }), _jsx("span", { className: "text-xs text-muted-foreground ml-1", children: "of \u20AC korting:" }), _jsx(Input, { type: "number", step: "0.01", min: "0.01", placeholder: "\u20AC", className: "w-24 h-8", value: prodEur[p.product_id] ?? "", onChange: (e) => setProdEur((prev) => ({ ...prev, [p.product_id]: e.target.value })) }), _jsx(Button, { type: "button", size: "sm", variant: "secondary", onClick: () => applyProductDiscountAmount(p.product_id, Number((prodEur[p.product_id] || "").replace(",", "."))), children: "Toepassen" })] }), _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) => {
568
+ const pct = discountPercent(p.normal_price, t.price_per_crate);
433
569
  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));
570
+ }), _jsxs("p", { className: "text-[11px] text-muted-foreground", children: ["Percentage t.o.v. de originele fustprijs (\u20AC ", p.normal_price.toFixed(2), "), afgerond op hele procenten. Bij minder fusten dan de laagste staffel geldt de gewone prijs."] }), tierIssues.map((issue, i) => (_jsxs("p", { className: `text-[11px] ${issue.level === "error" ? "text-destructive" : "text-amber-600"}`, children: [issue.level === "error" ? "Fout: " : "Let op: ", issue.message] }, i)))] }), _jsx(Textarea, { placeholder: "Omschrijving (optioneel)", rows: 2, value: p.description, onChange: (e) => updateProd(p.product_id, { description: e.target.value }) })] }, p.product_id));
435
571
  }) })] }))] })] })), 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
572
  const allSel = g.customers.length > 0 && g.customers.every((c) => selCustomers.has(c.id));
437
573
  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
574
  }) })] })), _jsx(Separator, {}), _jsx("div", { className: "max-h-[500px] overflow-y-auto space-y-2 pr-1", children: filteredCustomers.slice(0, 500).map((c) => {
439
575
  const checked = selCustomers.has(c.id);
440
576
  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"] }))] })] }) })] }));
577
+ }) })] })] })), 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 hidden", children: [_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, offerName: name, branding: branding })] }), _jsx("div", { className: "sticky bottom-0 z-30 -mx-6 -mb-6 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"] }))] })] }) })] }));
442
578
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ActiePage.d.ts","sourceRoot":"","sources":["../../src/components/ActiePage.tsx"],"names":[],"mappings":"AAqEA,wBAAgB,SAAS,gCAwrBxB"}
1
+ {"version":3,"file":"ActiePage.d.ts","sourceRoot":"","sources":["../../src/components/ActiePage.tsx"],"names":[],"mappings":"AAuEA,wBAAgB,SAAS,gCAyrBxB"}
@@ -5,6 +5,8 @@ import { useServerFn } from "@tanstack/react-start";
5
5
  import { Button, Input, Label, Textarea, Skeleton, toast } from "@flowselections/core";
6
6
  import { getOfferByLinkCode, trackOfferClick, placeOfferOrder, getOfferDeliveryOptions, getOfferBranding, } from "../lib/public-offer.functions";
7
7
  import { ShoppingCart, Check, Package, Truck, Minus, Plus, Send, Leaf, MapPin } from "lucide-react";
8
+ import { resolveBranding } from "../lib/branding";
9
+ import { sortTiers, resolveTierPrice, discountPercent } from "../lib/pricing";
8
10
  // ISO weekday: Ma=1 .. Zo=7
9
11
  function isoWeekday(d) {
10
12
  const js = d.getDay(); // 0..6, 0=Sun
@@ -147,13 +149,16 @@ export function ActiePage() {
147
149
  .catch(() => { });
148
150
  return () => { cancelled = true; };
149
151
  }, []);
150
- const primaryColor = branding?.primary_color || "#16a34a";
151
- const buttonColor = branding?.button_color || primaryColor;
152
+ const brand = resolveBranding(branding);
153
+ const primaryColor = brand.primary;
154
+ const buttonColor = brand.button;
152
155
  const brandStyle = {
153
156
  ["--offer-primary"]: primaryColor,
154
157
  ["--offer-button"]: buttonColor,
158
+ ["--offer-button-text"]: brand.buttonText,
159
+ ["--offer-soft"]: brand.primarySoft,
155
160
  };
156
- const brandName = branding?.brand_name || data?.campaign?.from_name || "Flow Selections";
161
+ const brandName = brand.brandName || data?.campaign?.from_name || "Aanbieding";
157
162
  if (loading) {
158
163
  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" }) }) }));
159
164
  }
@@ -168,31 +173,20 @@ export function ActiePage() {
168
173
  const soldOut = maxCrates <= 0;
169
174
  const crates = Math.max(0, Math.floor(qty[p.id] ?? 0));
170
175
  const plants = crates * ppc;
171
- // Prijsprioriteit: (1) tier-prijs uit actie-e-mail, (2) discount_price uit e-mail,
172
- // (3) standaardprijs uit artikelbeheer. De e-mailprijs is ALTIJD leidend.
173
- const tiers = Array.isArray(p.tiers)
174
- ? [...p.tiers].map((t) => ({ min_crates: Number(t.min_crates), price_per_crate: Number(t.price_per_crate) }))
175
- .sort((a, b) => a.min_crates - b.min_crates)
176
- : [];
177
- let unit = 0;
178
- let cratePrice = 0;
179
- let priceSource = "normal";
180
- if (tiers.length > 0) {
181
- const applicable = [...tiers].reverse().find((t) => crates >= t.min_crates) ?? tiers[0];
182
- cratePrice = applicable.price_per_crate;
183
- unit = cratePrice / ppc;
184
- priceSource = "tier";
185
- }
186
- else if (Number(p.discount_price) > 0) {
187
- unit = Number(p.discount_price);
188
- cratePrice = unit * ppc;
189
- priceSource = "discount";
190
- }
191
- else {
192
- unit = Number(p.normal_price ?? 0);
193
- cratePrice = unit * ppc;
194
- priceSource = "normal";
195
- }
176
+ // Prijsprioriteit: (1) geldende staffelprijs uit actie-e-mail, (2) discount_price uit
177
+ // e-mail, (3) standaardprijs. Onder de laagste staffel geldt (2)/(3).
178
+ const tiers = sortTiers((Array.isArray(p.tiers) ? p.tiers : []).map((t) => ({
179
+ min_crates: Number(t.min_crates),
180
+ price_per_crate: Number(t.price_per_crate),
181
+ })));
182
+ const resolved = resolveTierPrice(tiers, crates, {
183
+ discount_price: Number(p.discount_price ?? 0),
184
+ normal_price: Number(p.normal_price ?? 0),
185
+ });
186
+ const priceSource = resolved.source;
187
+ // Staffelprijs is per fust; discount/normal zijn per plant.
188
+ const cratePrice = priceSource === "tier" ? resolved.price : resolved.price * ppc;
189
+ const unit = priceSource === "tier" ? resolved.price / ppc : resolved.price;
196
190
  const priceSourceLabel = priceSource === "tier" ? "Prijs uit actie-e-mail"
197
191
  : priceSource === "discount" ? "Actieprijs uit e-mail"
198
192
  : "Standaardprijs artikelbeheer";
@@ -280,7 +274,7 @@ export function ActiePage() {
280
274
  if (placed) {
281
275
  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." })] }) }));
282
276
  }
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
277
+ 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: [brand.logoUrl ? (_jsx("img", { src: brand.logoUrl, 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
278
  ?.split(" ")
285
279
  .map((w) => w[0])
286
280
  .filter(Boolean)
@@ -301,9 +295,13 @@ export function ActiePage() {
301
295
  const price = l.cratePrice > 0 ? l.cratePrice : Number(p.discount_price ?? 0);
302
296
  const showStrike = normal > 0 && normal > price && price > 0;
303
297
  const currentQty = l.q;
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"
305
- ? "bg-green-100 border-green-300 text-green-800 font-semibold"
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) => {
298
+ 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) => {
299
+ const pct = discountPercent(normal, t.price_per_crate);
300
+ const active = l.priceSource === "tier" && l.cratePrice === t.price_per_crate;
301
+ return (_jsxs("span", { className: `text-[11px] px-2 py-0.5 rounded border ${active
302
+ ? "bg-green-100 border-green-300 text-green-800 font-semibold"
303
+ : "bg-slate-50 border-slate-200 text-slate-600"}`, children: ["\u2265 ", t.min_crates, " fust: ", fmtEuro(t.price_per_crate), pct > 0 ? ` (-${pct}%)` : ""] }, t.min_crates));
304
+ }) })), _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) => {
307
305
  if (["e", "E", "+", "-", ".", ","].includes(e.key))
308
306
  e.preventDefault();
309
307
  }, 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));
@@ -1 +1 @@
1
- {"version":3,"file":"CampagneOverzicht.d.ts","sourceRoot":"","sources":["../../src/components/CampagneOverzicht.tsx"],"names":[],"mappings":"AAOA,wBAAgB,iBAAiB,gCAuChC"}
1
+ {"version":3,"file":"CampagneOverzicht.d.ts","sourceRoot":"","sources":["../../src/components/CampagneOverzicht.tsx"],"names":[],"mappings":"AAmBA,wBAAgB,iBAAiB,gCAwPhC"}