@flowselections/actie-mail-kwekers-erp-mathot 1.0.15 → 1.0.17
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 +177 -41
- package/dist-lib/components/ActiePage.d.ts.map +1 -1
- package/dist-lib/components/ActiePage.js +30 -32
- package/dist-lib/components/CampagneOverzicht.d.ts.map +1 -1
- package/dist-lib/components/CampagneOverzicht.js +117 -10
- package/dist-lib/components/RapportagePage.d.ts +4 -0
- package/dist-lib/components/RapportagePage.d.ts.map +1 -1
- package/dist-lib/components/RapportagePage.js +11 -86
- package/dist-lib/components/aanbieding/EmailPreview.d.ts +6 -1
- package/dist-lib/components/aanbieding/EmailPreview.d.ts.map +1 -1
- package/dist-lib/components/aanbieding/EmailPreview.js +18 -6
- package/dist-lib/components/overzicht/PrestatieTegels.d.ts +13 -0
- package/dist-lib/components/overzicht/PrestatieTegels.d.ts.map +1 -0
- package/dist-lib/components/overzicht/PrestatieTegels.js +22 -0
- package/dist-lib/components/overzicht/RecenteCampagnes.d.ts +22 -0
- package/dist-lib/components/overzicht/RecenteCampagnes.d.ts.map +1 -0
- package/dist-lib/components/overzicht/RecenteCampagnes.js +15 -0
- package/dist-lib/components/overzicht/TopLijsten.d.ts +15 -0
- package/dist-lib/components/overzicht/TopLijsten.d.ts.map +1 -0
- package/dist-lib/components/overzicht/TopLijsten.js +16 -0
- package/dist-lib/components/overzicht/campaignStatus.d.ts +22 -0
- package/dist-lib/components/overzicht/campaignStatus.d.ts.map +1 -0
- package/dist-lib/components/overzicht/campaignStatus.js +47 -0
- package/dist-lib/components/settings/HuisstijlSettingsCard.d.ts.map +1 -1
- package/dist-lib/components/settings/HuisstijlSettingsCard.js +122 -9
- package/dist-lib/index.d.ts +6 -0
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +6 -0
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +299 -5
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/auth-middleware.js +4 -7
- package/dist-lib/integrations/supabase/client.d.ts +298 -4
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +298 -4
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +299 -5
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.js +12 -1
- package/dist-lib/lib/branding.d.ts +29 -0
- package/dist-lib/lib/branding.d.ts.map +1 -0
- package/dist-lib/lib/branding.js +70 -0
- package/dist-lib/lib/campaigns.functions.d.ts +1536 -66
- package/dist-lib/lib/campaigns.functions.d.ts.map +1 -1
- package/dist-lib/lib/campaigns.functions.js +22 -13
- package/dist-lib/lib/pricing.d.ts +66 -0
- package/dist-lib/lib/pricing.d.ts.map +1 -0
- package/dist-lib/lib/pricing.js +132 -0
- package/dist-lib/lib/public-offer.functions.d.ts.map +1 -1
- package/dist-lib/lib/public-offer.functions.js +20 -2
- package/dist-lib/lib/soft-auth.server.d.ts +11 -0
- package/dist-lib/lib/soft-auth.server.d.ts.map +1 -0
- package/dist-lib/lib/soft-auth.server.js +27 -0
- package/dist-lib/lib/stats.functions.d.ts +332 -20447
- package/dist-lib/lib/stats.functions.d.ts.map +1 -1
- package/dist-lib/lib/stats.functions.js +98 -29
- 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":"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,
|
|
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
|
-
|
|
262
|
-
|
|
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) =>
|
|
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
|
|
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
|
|
411
|
-
|
|
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
|
|
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 =
|
|
417
|
-
const
|
|
418
|
-
updateProd(p.product_id, {
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
|
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":"
|
|
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
|
|
151
|
-
const
|
|
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 =
|
|
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)
|
|
172
|
-
// (3) standaardprijs
|
|
173
|
-
const tiers = Array.isArray(p.tiers)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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: [
|
|
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) =>
|
|
305
|
-
|
|
306
|
-
|
|
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":"
|
|
1
|
+
{"version":3,"file":"CampagneOverzicht.d.ts","sourceRoot":"","sources":["../../src/components/CampagneOverzicht.tsx"],"names":[],"mappings":"AAmBA,wBAAgB,iBAAiB,gCAwPhC"}
|