@flowselections/actie-mail-kwekers-erp-mathot 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-lib/components/AanbiedingsmailtjePage.d.ts.map +1 -1
- package/dist-lib/components/AanbiedingsmailtjePage.js +102 -57
- package/dist-lib/components/ActiePage.d.ts.map +1 -1
- package/dist-lib/components/ActiePage.js +35 -10
- package/dist-lib/components/RapportagePage.d.ts.map +1 -1
- package/dist-lib/components/RapportagePage.js +37 -7
- package/dist-lib/components/aanbieding/EmailPreview.d.ts +22 -0
- package/dist-lib/components/aanbieding/EmailPreview.d.ts.map +1 -0
- package/dist-lib/components/aanbieding/EmailPreview.js +15 -0
- package/dist-lib/components/aanbieding/WizardSteps.d.ts +14 -0
- package/dist-lib/components/aanbieding/WizardSteps.d.ts.map +1 -0
- package/dist-lib/components/aanbieding/WizardSteps.js +21 -0
- package/dist-lib/components/settings/HuisstijlSettingsCard.d.ts.map +1 -1
- package/dist-lib/components/settings/HuisstijlSettingsCard.js +4 -4
- package/dist-lib/index.d.ts +2 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +2 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +16340 -7104
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +16340 -7104
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +16340 -7104
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +16654 -7203
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +59 -8
- package/dist-lib/lib/campaigns.functions.d.ts +81831 -35651
- package/dist-lib/lib/campaigns.functions.d.ts.map +1 -1
- package/dist-lib/lib/public-offer.functions.d.ts +7 -0
- package/dist-lib/lib/public-offer.functions.d.ts.map +1 -1
- package/dist-lib/lib/public-offer.functions.js +8 -0
- package/dist-lib/lib/stats.functions.d.ts +34582 -16110
- package/dist-lib/lib/stats.functions.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AanbiedingsmailtjePage.d.ts","sourceRoot":"","sources":["../../src/components/AanbiedingsmailtjePage.tsx"],"names":[],"mappings":"
|
|
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
|
-
|
|
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 ??
|
|
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
|
-
|
|
254
|
-
|
|
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
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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":"
|
|
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,
|
|
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
|
-
|
|
266
|
-
|
|
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: "
|
|
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-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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"}
|