@flowselections/actie-mail-kwekers-erp-mathot 1.0.9 → 1.0.11
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 +55 -8
- package/dist-lib/components/ActiePage.d.ts.map +1 -1
- package/dist-lib/components/ActiePage.js +122 -14
- package/dist-lib/integrations/supabase/auth-middleware.d.ts +765 -17
- package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.d.ts +765 -17
- package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/client.server.d.ts +765 -17
- package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
- package/dist-lib/integrations/supabase/types.d.ts +810 -42
- package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
- package/dist-lib/lib/campaigns.functions.d.ts +3755 -15
- package/dist-lib/lib/campaigns.functions.d.ts.map +1 -1
- package/dist-lib/lib/campaigns.functions.js +22 -1
- package/dist-lib/lib/public-offer.functions.d.ts +36 -0
- package/dist-lib/lib/public-offer.functions.d.ts.map +1 -1
- package/dist-lib/lib/public-offer.functions.js +24 -3
- package/dist-lib/lib/stats.functions.d.ts +1574 -77
- 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":"AA0CA,wBAAgB,sBAAsB,
|
|
1
|
+
{"version":3,"file":"AanbiedingsmailtjePage.d.ts","sourceRoot":"","sources":["../../src/components/AanbiedingsmailtjePage.tsx"],"names":[],"mappings":"AA0CA,wBAAgB,sBAAsB,gCA8oBrC"}
|
|
@@ -24,6 +24,7 @@ export function AanbiedingsmailtjePage() {
|
|
|
24
24
|
const [selCustomers, setSelCustomers] = useState(new Set());
|
|
25
25
|
const [showCustomerClasses, setShowCustomerClasses] = useState(true);
|
|
26
26
|
const [productSearch, setProductSearch] = useState("");
|
|
27
|
+
const [productSearchFocused, setProductSearchFocused] = useState(false);
|
|
27
28
|
const [customerSearch, setCustomerSearch] = useState("");
|
|
28
29
|
const [bulkPrice, setBulkPrice] = useState("");
|
|
29
30
|
const BULK_STICKER_PREF_KEY = "aanbiedingsmailtje:bulkStickerOn";
|
|
@@ -76,10 +77,11 @@ export function AanbiedingsmailtjePage() {
|
|
|
76
77
|
.eq("connection_id", activeId)
|
|
77
78
|
.limit(5000)
|
|
78
79
|
: Promise.resolve({ data: [] });
|
|
79
|
-
const [{ data: cacheRows }, { data: draftRows }, { data: stockRows }, { data: cs }, { data: extCs }, { data: settings }] = await Promise.all([
|
|
80
|
+
const [{ data: cacheRows }, { data: draftRows }, { data: stockRows }, { data: localProdRows }, { data: cs }, { data: extCs }, { data: settings }] = await Promise.all([
|
|
80
81
|
cachePromise,
|
|
81
82
|
draftPromise,
|
|
82
83
|
stockPromise,
|
|
84
|
+
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),
|
|
83
85
|
sb.from("customers").select("id, company_name, email, customer_class, country").not("email", "is", null).order("company_name").limit(1000),
|
|
84
86
|
sb.from("external_customers_grouped").select("group_key, display_name, email, country, is_active").not("email", "is", null).eq("is_active", true).limit(2000),
|
|
85
87
|
sb.from("offer_settings").select("default_subject, default_intro, default_sticker_cost, show_customer_classes").maybeSingle(),
|
|
@@ -152,10 +154,44 @@ export function AanbiedingsmailtjePage() {
|
|
|
152
154
|
is_draft: true,
|
|
153
155
|
};
|
|
154
156
|
});
|
|
157
|
+
const localProducts = (Array.isArray(localProdRows) ? localProdRows : []).map((r) => {
|
|
158
|
+
const ppc = Number(r.plants_per_crate);
|
|
159
|
+
const cpl = Number(r.crates_per_layer);
|
|
160
|
+
const lpc = Number(r.layers_per_cart);
|
|
161
|
+
const parts = [ppc, cpl, lpc].filter((n) => Number.isFinite(n) && n > 0);
|
|
162
|
+
const key = r.trade_item_key ?? `erp:${r.id}`;
|
|
163
|
+
return {
|
|
164
|
+
id: key,
|
|
165
|
+
product: r.product ?? "(naamloos)",
|
|
166
|
+
sale_price: r.sale_price ?? null,
|
|
167
|
+
quantity: stockFor(key) || Number(r.quantity ?? 0),
|
|
168
|
+
image_url: r.image_url ?? null,
|
|
169
|
+
barcode: r.barcode ?? "",
|
|
170
|
+
plants_per_crate: Number.isFinite(ppc) && ppc > 0 ? ppc : null,
|
|
171
|
+
belading: parts.length === 3 ? `${ppc}x${cpl}x${lpc}` : null,
|
|
172
|
+
is_draft: false,
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
// Overlay lokale ERP-packing/barcode over Floriday-publicaties met dezelfde key
|
|
176
|
+
const localByKey = new Map(localProducts.map((p) => [p.id, p]));
|
|
177
|
+
const publishedMerged = published.map((p) => {
|
|
178
|
+
const local = localByKey.get(p.id);
|
|
179
|
+
if (!local)
|
|
180
|
+
return p;
|
|
181
|
+
return {
|
|
182
|
+
...p,
|
|
183
|
+
plants_per_crate: local.plants_per_crate ?? p.plants_per_crate,
|
|
184
|
+
belading: local.belading ?? p.belading,
|
|
185
|
+
barcode: local.barcode || p.barcode,
|
|
186
|
+
image_url: p.image_url ?? local.image_url,
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
const seenIds = new Set();
|
|
155
190
|
const merged = [
|
|
156
191
|
...drafts,
|
|
157
|
-
...
|
|
158
|
-
|
|
192
|
+
...publishedMerged.sort((a, b) => a.product.localeCompare(b.product)),
|
|
193
|
+
...localProducts.sort((a, b) => a.product.localeCompare(b.product)),
|
|
194
|
+
].filter((p) => (seenIds.has(p.id) ? false : (seenIds.add(p.id), true)));
|
|
159
195
|
setProducts(merged);
|
|
160
196
|
const localList = (cs ?? []).map((c) => ({
|
|
161
197
|
id: c.id, company_name: c.company_name, email: c.email, customer_class: c.customer_class, country: c.country, source: "local",
|
|
@@ -311,7 +347,7 @@ export function AanbiedingsmailtjePage() {
|
|
|
311
347
|
const filteredProducts = products.filter((p) => p.product.toLowerCase().includes(productSearch.toLowerCase()));
|
|
312
348
|
if (loading)
|
|
313
349
|
return _jsxs("div", { className: "p-6 space-y-4", children: [_jsx(Skeleton, { className: "h-32 w-full" }), _jsx(Skeleton, { className: "h-64 w-full" })] });
|
|
314
|
-
return (_jsxs("main", { className: "p-6 pb-28 space-y-6", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Tag, { className: "h-6 w-6 text-primary" }), _jsx("h1", { className: "text-2xl font-bold", children: "Acties & Promoties" })] }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Maak promoties en verstuur naar klanten" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => navigate({ to: "/aanbiedingsmailtje/overzicht" }), children: [_jsx(List, { className: "h-4 w-4 mr-2" }), "Overzicht"] }), _jsxs(Button, { variant: "outline", onClick: () => navigate({ to: "/aanbiedingsmailtje/rapportage" }), children: [_jsx(BarChart3, { className: "h-4 w-4 mr-2" }), "Rapportage"] })] })] }), _jsxs("div", { className: "grid gap-6 xl:grid-cols-2", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Package, { className: "h-5 w-5" }), _jsx(CardTitle, { children: "Producten Selecteren" })] }), _jsx(CardDescription, { children: "Kies producten en stel actieprijzen in" })] }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { className: "pl-9", placeholder: "Zoek product...", value: productSearch, onChange: (e) => setProductSearch(e.target.value) })] }), productSearch && (
|
|
350
|
+
return (_jsxs("main", { className: "p-6 pb-28 space-y-6", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 flex-wrap", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Tag, { className: "h-6 w-6 text-primary" }), _jsx("h1", { className: "text-2xl font-bold", children: "Acties & Promoties" })] }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Maak promoties en verstuur naar klanten" })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { variant: "outline", onClick: () => navigate({ to: "/aanbiedingsmailtje/overzicht" }), children: [_jsx(List, { className: "h-4 w-4 mr-2" }), "Overzicht"] }), _jsxs(Button, { variant: "outline", onClick: () => navigate({ to: "/aanbiedingsmailtje/rapportage" }), children: [_jsx(BarChart3, { className: "h-4 w-4 mr-2" }), "Rapportage"] })] })] }), _jsxs("div", { className: "grid gap-6 xl:grid-cols-2", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Package, { className: "h-5 w-5" }), _jsx(CardTitle, { children: "Producten Selecteren" })] }), _jsx(CardDescription, { children: "Kies producten en stel actieprijzen in" })] }), _jsxs(CardContent, { className: "space-y-3", children: [_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { className: "pl-9", placeholder: "Zoek product...", value: productSearch, onChange: (e) => setProductSearch(e.target.value), onFocus: () => setProductSearchFocused(true), onBlur: () => setTimeout(() => setProductSearchFocused(false), 200) })] }), (productSearchFocused || productSearch) && (_jsxs("div", { className: "max-h-80 overflow-y-auto border rounded-md", children: [filteredProducts.map((p) => (_jsxs("button", { onMouseDown: () => addProduct(p), className: "w-full text-left px-3 py-2 hover:bg-muted text-sm", children: [p.product, " ", _jsx("span", { className: "text-muted-foreground", children: p.is_draft ? "· concept" : `· voorraad ${p.quantity ?? 0}` })] }, p.id))), filteredProducts.length === 0 && (_jsx("div", { className: "px-3 py-2 text-sm text-muted-foreground", children: "Geen producten gevonden" }))] })), _jsxs("div", { className: "text-sm font-medium", children: ["Geselecteerde producten (", selProducts.length, ")"] }), selProducts.length === 0 ? (_jsx("div", { className: "text-sm text-muted-foreground py-6", children: "Geen producten geselecteerd" })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex flex-wrap gap-2 items-center", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: "Snelle korting:" }), [10, 15, 20, 25, 30].map((p) => (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => applyDiscount(p), children: [_jsx(Percent, { className: "h-3 w-3 mr-1" }), p, "%"] }, p)))] }), _jsxs("div", { className: "flex flex-wrap gap-2 items-center rounded-md border bg-muted/30 p-2", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: "Stickeropdruk \u2014 \u00E9\u00E9n prijs voor \u00E1lle producten: \u20AC" }), _jsx(Input, { type: "number", step: "0.01", placeholder: "0,00", className: "w-28 h-8", value: bulkPrice, onChange: (e) => setBulkPrice(e.target.value) }), _jsxs("div", { className: "flex items-center gap-2 ml-2", children: [_jsx(Switch, { id: "bulk-sticker-toggle", checked: bulkStickerOn, onCheckedChange: (checked) => {
|
|
315
351
|
setBulkStickerOn(checked);
|
|
316
352
|
if (!checked) {
|
|
317
353
|
setSelProducts((prev) => prev.map((p) => ({ ...p, sticker_cost: null })));
|
|
@@ -326,17 +362,28 @@ export function AanbiedingsmailtjePage() {
|
|
|
326
362
|
const updateTier = (idx, patch) => {
|
|
327
363
|
const tiers = [...p.tiers];
|
|
328
364
|
tiers[idx] = { ...tiers[idx], ...patch };
|
|
329
|
-
|
|
365
|
+
const maxPrice = Math.max(p.crate_price, ...tiers.map((t) => t.price_per_crate));
|
|
366
|
+
updateProd(p.product_id, { tiers, normal_price: Math.max(p.normal_price, maxPrice) });
|
|
330
367
|
};
|
|
331
368
|
const addTier = () => {
|
|
332
369
|
const last = p.tiers[p.tiers.length - 1];
|
|
333
370
|
const nextMin = last ? last.min_crates + 5 : 1;
|
|
334
|
-
|
|
371
|
+
const basePrice = Math.max(p.crate_price, p.normal_price);
|
|
372
|
+
const newTiers = [...p.tiers, { min_crates: nextMin, price_per_crate: basePrice }];
|
|
373
|
+
updateProd(p.product_id, { tiers: newTiers, normal_price: Math.max(p.normal_price, basePrice) });
|
|
335
374
|
};
|
|
336
375
|
const removeTier = (idx) => {
|
|
337
|
-
|
|
376
|
+
const tiers = p.tiers.filter((_, i) => i !== idx);
|
|
377
|
+
const maxPrice = Math.max(p.crate_price, ...tiers.map((t) => t.price_per_crate));
|
|
378
|
+
updateProd(p.product_id, { tiers, normal_price: Math.max(p.normal_price, maxPrice) });
|
|
338
379
|
};
|
|
339
|
-
return (_jsxs("div", { className: "border rounded-md p-3 space-y-3 bg-muted/30", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { children: [_jsx("div", { className: "font-medium text-sm", children: p.product_name }), _jsxs("div", { className: "text-xs text-muted-foreground", children: ["Originele prijs: \u20AC ", p.normal_price.toFixed(2)] })] }), _jsx(Button, { variant: "ghost", size: "icon", onClick: () => removeProduct(p.product_id), children: _jsx(X, { className: "h-4 w-4" }) })] }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { className: "text-xs w-24 shrink-0", children: "Planten/fust:" }), _jsx(Input, { type: "number", value: p.plants_per_crate, onChange: (e) => updateProd(p.product_id, { plants_per_crate: Number(e.target.value) }) })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { className: "text-xs w-20 shrink-0", children: "Belading:" }), _jsx(Input, { placeholder: "AxBxC", value: p.belading, onChange: (e) => updateProd(p.product_id, { belading: e.target.value }) })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { className: "text-xs w-24 shrink-0", children: "Barcode:" }), _jsx(Input, { value: p.barcode, onChange: (e) => updateProd(p.product_id, { barcode: e.target.value }) })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { className: "text-xs w-20 shrink-0", children: "Fustprijs: \u20AC" }), _jsx(Input, { type: "number", step: "0.01", value: p.crate_price, onChange: (e) =>
|
|
380
|
+
return (_jsxs("div", { className: "border rounded-md p-3 space-y-3 bg-muted/30", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { children: [_jsx("div", { className: "font-medium text-sm", children: p.product_name }), _jsxs("div", { className: "text-xs text-muted-foreground", children: ["Originele prijs: \u20AC ", p.normal_price.toFixed(2)] })] }), _jsx(Button, { variant: "ghost", size: "icon", onClick: () => removeProduct(p.product_id), children: _jsx(X, { className: "h-4 w-4" }) })] }), _jsxs("div", { className: "grid grid-cols-2 gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { className: "text-xs w-24 shrink-0", children: "Planten/fust:" }), _jsx(Input, { type: "number", value: p.plants_per_crate, onChange: (e) => updateProd(p.product_id, { plants_per_crate: Number(e.target.value) }) })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { className: "text-xs w-20 shrink-0", children: "Belading:" }), _jsx(Input, { placeholder: "AxBxC", value: p.belading, onChange: (e) => updateProd(p.product_id, { belading: e.target.value }) })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { className: "text-xs w-24 shrink-0", children: "Barcode:" }), _jsx(Input, { value: p.barcode, onChange: (e) => updateProd(p.product_id, { barcode: e.target.value }) })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { className: "text-xs w-20 shrink-0", children: "Fustprijs: \u20AC" }), _jsx(Input, { type: "number", step: "0.01", value: p.crate_price, onChange: (e) => {
|
|
381
|
+
const newPrice = Number(e.target.value);
|
|
382
|
+
const oldPrice = p.crate_price;
|
|
383
|
+
const tiers = p.tiers.map((t) => t.price_per_crate === oldPrice ? { ...t, price_per_crate: newPrice } : t);
|
|
384
|
+
const normalPrice = Math.max(p.normal_price, newPrice, ...tiers.map((t) => t.price_per_crate));
|
|
385
|
+
updateProd(p.product_id, { crate_price: newPrice, tiers, normal_price: normalPrice });
|
|
386
|
+
} })] })] }), _jsxs("div", { className: "flex items-center gap-2 border-t pt-2", children: [_jsx(Label, { className: "text-xs", children: "Stickeropdruk beschikbaar:" }), _jsx(Button, { type: "button", size: "sm", variant: p.sticker_available ? "default" : "outline", onClick: () => updateProd(p.product_id, { sticker_available: true }), children: "Ja" }), _jsx(Button, { type: "button", size: "sm", variant: !p.sticker_available ? "default" : "outline", onClick: () => updateProd(p.product_id, { sticker_available: false }), children: "Nee" })] }), _jsxs("div", { className: "space-y-2 border-t pt-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Label, { className: "text-xs font-medium", children: "Prijs aftuin ex. Fust vanaf .." }), _jsx(Button, { variant: "outline", size: "sm", onClick: addTier, children: "+ Staffel" })] }), p.tiers.map((t, idx) => {
|
|
340
387
|
const pct = p.normal_price > 0 ? Math.round((1 - t.price_per_crate / p.normal_price) * 100) : 0;
|
|
341
388
|
return (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: "Vanaf" }), _jsx(Input, { type: "number", className: "w-20", value: t.min_crates, onChange: (e) => updateTier(idx, { min_crates: Number(e.target.value) }) }), _jsx("span", { className: "text-xs text-muted-foreground", children: "fust(en): \u20AC" }), _jsx(Input, { type: "number", step: "0.01", className: "w-24", value: t.price_per_crate, onChange: (e) => updateTier(idx, { price_per_crate: Number(e.target.value) }) }), _jsxs("span", { className: `text-xs font-medium ${pct > 0 ? "text-destructive" : "text-muted-foreground"}`, children: ["(", pct, "%)"] }), p.tiers.length > 1 && (_jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7", onClick: () => removeTier(idx), children: _jsx(X, { className: "h-3 w-3" }) }))] }, idx));
|
|
342
389
|
})] }), _jsx(Textarea, { placeholder: "Omschrijving (optioneel)", rows: 2, value: p.description, onChange: (e) => updateProd(p.product_id, { description: e.target.value }) })] }, p.product_id));
|
|
@@ -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":"AAoEA,wBAAgB,SAAS,gCAooBxB"}
|
|
@@ -3,19 +3,41 @@ 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 } from "../lib/public-offer.functions";
|
|
7
|
-
import { ShoppingCart, Check, User, Package, Truck, Minus, Plus, Send, Leaf } from "lucide-react";
|
|
8
|
-
|
|
6
|
+
import { getOfferByLinkCode, trackOfferClick, placeOfferOrder, getOfferDeliveryOptions, } from "../lib/public-offer.functions";
|
|
7
|
+
import { ShoppingCart, Check, User, Package, Truck, Minus, Plus, Send, Leaf, MapPin } from "lucide-react";
|
|
8
|
+
// ISO weekday: Ma=1 .. Zo=7
|
|
9
|
+
function isoWeekday(d) {
|
|
10
|
+
const js = d.getDay(); // 0..6, 0=Sun
|
|
11
|
+
return js === 0 ? 7 : js;
|
|
12
|
+
}
|
|
13
|
+
function addDays(start, days) {
|
|
14
|
+
const d = new Date(start);
|
|
15
|
+
d.setDate(d.getDate() + days);
|
|
16
|
+
return d;
|
|
17
|
+
}
|
|
18
|
+
// Voeg N werkdagen toe, waarbij "werkdag" = weekdag in allowedWeekdays (ISO 1..7)
|
|
19
|
+
function addWorkingDays(start, count, allowedWeekdays) {
|
|
20
|
+
const set = new Set(allowedWeekdays.length > 0 ? allowedWeekdays : [1, 2, 3, 4, 5]);
|
|
9
21
|
const d = new Date(start);
|
|
10
22
|
let added = 0;
|
|
11
|
-
while (added <
|
|
23
|
+
while (added < count) {
|
|
12
24
|
d.setDate(d.getDate() + 1);
|
|
13
|
-
|
|
14
|
-
if (day !== 0 && day !== 6)
|
|
25
|
+
if (set.has(isoWeekday(d)))
|
|
15
26
|
added++;
|
|
16
27
|
}
|
|
17
28
|
return d;
|
|
18
29
|
}
|
|
30
|
+
// Verschuif door totdat de dag in allowedWeekdays valt
|
|
31
|
+
function snapToWeekday(start, allowedWeekdays) {
|
|
32
|
+
const set = new Set(allowedWeekdays.length > 0 ? allowedWeekdays : [1, 2, 3, 4, 5]);
|
|
33
|
+
const d = new Date(start);
|
|
34
|
+
for (let i = 0; i < 14; i++) {
|
|
35
|
+
if (set.has(isoWeekday(d)))
|
|
36
|
+
return d;
|
|
37
|
+
d.setDate(d.getDate() + 1);
|
|
38
|
+
}
|
|
39
|
+
return d;
|
|
40
|
+
}
|
|
19
41
|
function toDateInput(d) {
|
|
20
42
|
const y = d.getFullYear();
|
|
21
43
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
@@ -34,12 +56,14 @@ function formatDateNL(iso) {
|
|
|
34
56
|
return iso;
|
|
35
57
|
}
|
|
36
58
|
}
|
|
59
|
+
const WEEKDAY_LABELS = { 1: "ma", 2: "di", 3: "wo", 4: "do", 5: "vr", 6: "za", 7: "zo" };
|
|
37
60
|
const fmtEuro = (n) => `€ ${Number(n ?? 0).toLocaleString("nl-NL", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
38
61
|
export function ActiePage() {
|
|
39
62
|
const { linkCode } = useParams({ strict: false });
|
|
40
63
|
const getFn = useServerFn(getOfferByLinkCode);
|
|
41
64
|
const trackFn = useServerFn(trackOfferClick);
|
|
42
65
|
const orderFn = useServerFn(placeOfferOrder);
|
|
66
|
+
const optionsFn = useServerFn(getOfferDeliveryOptions);
|
|
43
67
|
const [data, setData] = useState(null);
|
|
44
68
|
const [loading, setLoading] = useState(true);
|
|
45
69
|
const [qty, setQty] = useState({});
|
|
@@ -48,10 +72,43 @@ export function ActiePage() {
|
|
|
48
72
|
const [email, setEmail] = useState("");
|
|
49
73
|
const [phone, setPhone] = useState("");
|
|
50
74
|
const [notes, setNotes] = useState("");
|
|
51
|
-
const [deliveryDate, setDeliveryDate] = useState(
|
|
75
|
+
const [deliveryDate, setDeliveryDate] = useState("");
|
|
52
76
|
const [placed, setPlaced] = useState(null);
|
|
53
77
|
const [busy, setBusy] = useState(false);
|
|
54
|
-
|
|
78
|
+
// Logistiek
|
|
79
|
+
const [options, setOptions] = useState(null);
|
|
80
|
+
// afhaallocatie verwijderd uit het bestelformulier
|
|
81
|
+
const [deliveryGardenId, setDeliveryGardenId] = useState("");
|
|
82
|
+
// Auto-carrier: eerste voorkeur (is_default → sort_order → naam) uit de carriers van de aanbieder
|
|
83
|
+
const carrier = useMemo(() => {
|
|
84
|
+
const list = Array.isArray(options?.carriers) ? options.carriers : [];
|
|
85
|
+
return list[0] ?? null;
|
|
86
|
+
}, [options]);
|
|
87
|
+
// Eerst mogelijke leverdatum: alleen berekenen als afhaallocatie + aflevertuin bekend zijn
|
|
88
|
+
const minDeliveryDate = useMemo(() => {
|
|
89
|
+
if (!options || !deliveryGardenId || !carrier)
|
|
90
|
+
return "";
|
|
91
|
+
const pickDays = Math.max(0, Number(options.pick_days_before_transport ?? 1));
|
|
92
|
+
const pickWeekdays = Array.isArray(options.pick_weekdays) && options.pick_weekdays.length > 0
|
|
93
|
+
? options.pick_weekdays.map((n) => Number(n))
|
|
94
|
+
: [1, 2, 3, 4, 5];
|
|
95
|
+
const deliveryWeekdays = Array.isArray(carrier.delivery_weekdays) && carrier.delivery_weekdays.length > 0
|
|
96
|
+
? carrier.delivery_weekdays.map((n) => Number(n))
|
|
97
|
+
: [1, 2, 3, 4, 5];
|
|
98
|
+
const readyForTransport = addWorkingDays(new Date(), pickDays, pickWeekdays);
|
|
99
|
+
const afterTransport = addWorkingDays(readyForTransport, Math.max(0, Number(carrier.delivery_days ?? 0)), deliveryWeekdays);
|
|
100
|
+
const snapped = snapToWeekday(afterTransport, deliveryWeekdays);
|
|
101
|
+
return toDateInput(snapped);
|
|
102
|
+
}, [options, deliveryGardenId, carrier]);
|
|
103
|
+
const allowedDeliveryWeekdays = useMemo(() => (carrier?.delivery_weekdays ?? []).map((n) => Number(n)), [carrier]);
|
|
104
|
+
// Reset / preselect leverdatum bij logistieke wijziging
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!minDeliveryDate) {
|
|
107
|
+
setDeliveryDate("");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
setDeliveryDate(minDeliveryDate);
|
|
111
|
+
}, [minDeliveryDate]);
|
|
55
112
|
useEffect(() => {
|
|
56
113
|
let cancelled = false;
|
|
57
114
|
const load = async (initial) => {
|
|
@@ -64,6 +121,10 @@ export function ActiePage() {
|
|
|
64
121
|
setName(result.recipient.recipient_name);
|
|
65
122
|
setEmail(result.recipient.recipient_email);
|
|
66
123
|
trackFn({ data: { code: linkCode } }).catch(() => { });
|
|
124
|
+
optionsFn({ data: { code: linkCode } })
|
|
125
|
+
.then((opts) => { if (!cancelled)
|
|
126
|
+
setOptions(opts); })
|
|
127
|
+
.catch(() => { });
|
|
67
128
|
}
|
|
68
129
|
}
|
|
69
130
|
finally {
|
|
@@ -127,6 +188,8 @@ export function ActiePage() {
|
|
|
127
188
|
const totalAmount = safeProducts.reduce((s, p) => s + lineFor(p).total, 0);
|
|
128
189
|
const hasItems = safeProducts.some((p) => (qty[p.id] ?? 0) > 0);
|
|
129
190
|
const setQ = (id, value, maxCrates) => setQty({ ...qty, [id]: Math.max(0, Math.min(maxCrates, Math.floor(Number.isFinite(value) ? value : 0))) });
|
|
191
|
+
// pickupLocation verwijderd
|
|
192
|
+
const deliveryGarden = (options?.delivery_gardens ?? []).find((g) => g.id === deliveryGardenId) ?? null;
|
|
130
193
|
const submit = async () => {
|
|
131
194
|
const items = safeProducts
|
|
132
195
|
.filter((p) => (qty[p.id] ?? 0) > 0)
|
|
@@ -145,23 +208,49 @@ export function ActiePage() {
|
|
|
145
208
|
toast.error("Voeg minimaal één product toe");
|
|
146
209
|
return;
|
|
147
210
|
}
|
|
148
|
-
|
|
149
|
-
|
|
211
|
+
// afhaallocatie niet vereist
|
|
212
|
+
if (!deliveryGardenId) {
|
|
213
|
+
toast.error("Kies een aflevertuin");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (!deliveryDate || !minDeliveryDate) {
|
|
217
|
+
toast.error("Er kon geen leverdatum worden berekend");
|
|
150
218
|
return;
|
|
151
219
|
}
|
|
220
|
+
if (deliveryDate < minDeliveryDate) {
|
|
221
|
+
toast.error(`Eerst mogelijke leverdatum is ${formatDateNL(minDeliveryDate)}`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (allowedDeliveryWeekdays.length > 0) {
|
|
225
|
+
const wd = isoWeekday(new Date(deliveryDate + "T00:00:00"));
|
|
226
|
+
if (!allowedDeliveryWeekdays.includes(wd)) {
|
|
227
|
+
toast.error("De vervoerder levert niet op deze dag");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
152
231
|
setBusy(true);
|
|
153
232
|
try {
|
|
233
|
+
const logisticsNote = [
|
|
234
|
+
`Aflevertuin: ${deliveryGarden ? `${deliveryGarden.name}${deliveryGarden.gln_code ? ` (${deliveryGarden.gln_code})` : ""}` : "-"}`,
|
|
235
|
+
`Vervoerder: ${carrier?.name ?? "-"}`,
|
|
236
|
+
`Gewenste leverdatum: ${formatDateNL(deliveryDate)}`,
|
|
237
|
+
].join("\n");
|
|
154
238
|
const r = await orderFn({
|
|
155
239
|
data: {
|
|
156
240
|
code: linkCode,
|
|
157
241
|
customer_name: name,
|
|
158
242
|
customer_email: email,
|
|
159
243
|
customer_phone: phone,
|
|
160
|
-
notes:
|
|
244
|
+
notes: `${logisticsNote}${notes ? `\n\n${notes}` : ""}`,
|
|
245
|
+
delivery_garden_id: deliveryGardenId || undefined,
|
|
246
|
+
delivery_garden_name: deliveryGarden?.name,
|
|
247
|
+
carrier_id: carrier?.id,
|
|
248
|
+
carrier_name: carrier?.name,
|
|
249
|
+
delivery_date: deliveryDate,
|
|
161
250
|
items,
|
|
162
251
|
},
|
|
163
252
|
});
|
|
164
|
-
setPlaced({ id: r.orderId, total: r.total });
|
|
253
|
+
setPlaced({ id: r.orderId, total: r.total, orderNumber: r.orderNumber ?? null });
|
|
165
254
|
}
|
|
166
255
|
catch (e) {
|
|
167
256
|
toast.error(e?.message ?? "Fout");
|
|
@@ -171,7 +260,7 @@ export function ActiePage() {
|
|
|
171
260
|
}
|
|
172
261
|
};
|
|
173
262
|
if (placed) {
|
|
174
|
-
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
|
|
263
|
+
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." })] }) }));
|
|
175
264
|
}
|
|
176
265
|
const fromName = data?.campaign?.from_name ?? "";
|
|
177
266
|
return (_jsxs("div", { className: "min-h-screen bg-[#f0fdf4] flex flex-col", children: [_jsx("header", { className: "bg-gradient-to-r from-green-600 to-green-700 text-white", children: _jsxs("div", { className: "max-w-6xl mx-auto px-6 py-10 text-center", children: [_jsxs("div", { className: "mx-auto mb-4 inline-flex items-center justify-center bg-white rounded-md px-4 py-2 shadow-sm", children: [_jsx(Leaf, { className: "h-5 w-5 text-green-700 mr-2" }), _jsx("span", { className: "font-bold text-slate-900 tracking-tight", children: "Flow Selections" })] }), _jsxs("h1", { className: "text-3xl md:text-4xl font-extrabold tracking-tight", children: ["\uD83C\uDF31 ", data?.campaign?.subject ?? "Speciale Aanbieding"] }), data?.campaign?.intro_message ? (_jsx("p", { className: "mt-3 text-green-50 whitespace-pre-line max-w-2xl mx-auto", children: data.campaign.intro_message })) : (_jsx("p", { className: "mt-3 text-green-50", children: "Wij hebben een mooie aanbieding voor u!" }))] }) }), _jsxs("main", { className: "flex-1 max-w-6xl w-full mx-auto px-4 md:px-6 py-8 space-y-6", children: [_jsxs("div", { className: "bg-green-50 border border-green-200 rounded-xl p-5 flex items-center gap-4", children: [_jsx("div", { className: "rounded-full bg-green-600 p-3 text-white shrink-0", children: _jsx(User, { className: "h-5 w-5" }) }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "font-bold text-slate-900", children: ["Welkom, ", data?.recipient?.recipient_name, "!"] }), _jsx("div", { className: "text-green-700 text-sm truncate", children: data?.recipient?.recipient_email })] })] }), _jsxs("div", { className: "grid gap-6 lg:grid-cols-[minmax(0,1fr)_360px]", children: [_jsxs("section", { children: [_jsxs("h2", { className: "flex items-center gap-2 text-xl font-bold text-slate-900 mb-4", children: [_jsx(Package, { className: "h-5 w-5 text-green-700" }), "Selecteer uw producten"] }), safeProducts.some((p) => p.sticker_available !== false) && (_jsxs("div", { className: "mb-4 flex items-center justify-between gap-3 bg-blue-50 border border-blue-200 rounded-xl px-4 py-3", children: [_jsxs("div", { className: "text-sm text-blue-900", children: [_jsx("strong", { children: "Stickeropdruk voor alle planten" }), _jsx("div", { className: "text-xs text-blue-700", children: "Schakel met \u00E9\u00E9n klik in/uit voor alle producten waar het beschikbaar is." })] }), _jsxs("div", { className: "flex gap-2 shrink-0", children: [_jsx("button", { type: "button", onClick: () => {
|
|
@@ -198,5 +287,24 @@ export function ActiePage() {
|
|
|
198
287
|
}) })] }), _jsx("aside", { className: "lg:sticky lg:top-6 self-start", children: _jsxs("div", { className: "bg-white rounded-xl border border-slate-200 shadow-sm p-5 space-y-5", children: [_jsxs("h3", { className: "flex items-center gap-2 text-lg font-bold text-slate-900", children: [_jsx(ShoppingCart, { className: "h-5 w-5 text-green-700" }), "Uw bestelling"] }), hasItems ? (_jsxs("div", { className: "space-y-2 text-sm", children: [safeProducts.filter((p) => (qty[p.id] ?? 0) > 0).map((p) => {
|
|
199
288
|
const l = lineFor(p);
|
|
200
289
|
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));
|
|
201
|
-
}), _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(
|
|
290
|
+
}), _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) => {
|
|
291
|
+
const checked = deliveryGardenId === g.id;
|
|
292
|
+
return (_jsxs("label", { className: `flex items-start gap-2 px-3 py-2 text-sm cursor-pointer hover:bg-green-50 ${checked ? "bg-green-50" : ""}`, children: [_jsx("input", { type: "radio", name: "delivery-garden", className: "mt-0.5", checked: checked, onChange: () => setDeliveryGardenId(g.id) }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "truncate font-medium text-slate-800", children: [g.name, g.is_primary ? (_jsx("span", { className: "ml-2 text-[10px] uppercase tracking-wide text-green-700", children: "standaard" })) : null] }), _jsxs("div", { className: "text-[11px] text-slate-500", children: [g.city ? _jsx("span", { children: g.city }) : null, g.gln_code ? _jsxs("span", { children: [g.city ? " · " : "", "GLN ", g.gln_code] }) : null] })] })] }, g.id));
|
|
293
|
+
})) }), deliveryGarden && (_jsxs("p", { className: "text-xs text-slate-600", children: ["Gekozen: ", _jsx("strong", { children: deliveryGarden.name })] }))] }), carrier && (_jsxs("div", { className: "rounded-md bg-slate-50 border border-slate-200 px-3 py-2 text-xs text-slate-700", children: [_jsxs("div", { className: "flex items-center gap-2 font-semibold text-slate-800", children: [_jsx(Truck, { className: "h-4 w-4 text-green-700" }), "Vervoerder: ", carrier.name] }), _jsxs("div", { className: "mt-0.5", children: ["Transporttijd: ", carrier.delivery_days, " werkdag", carrier.delivery_days === 1 ? "" : "en", allowedDeliveryWeekdays.length > 0 && (_jsxs(_Fragment, { children: [" \u00B7 Levert op ", allowedDeliveryWeekdays.sort((a, b) => a - b).map((n) => WEEKDAY_LABELS[n]).join(", ")] }))] })] })), _jsxs("div", { className: "space-y-1.5", children: [_jsxs(Label, { className: "flex items-center gap-2 text-slate-800", children: [_jsx(Truck, { className: "h-4 w-4 text-green-700" }), "Gewenste leverdatum"] }), _jsx(Input, { type: "date", min: minDeliveryDate || undefined, value: deliveryDate, disabled: !minDeliveryDate, onChange: (e) => {
|
|
294
|
+
const v = e.target.value;
|
|
295
|
+
if (!v) {
|
|
296
|
+
setDeliveryDate("");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (allowedDeliveryWeekdays.length > 0) {
|
|
300
|
+
const wd = isoWeekday(new Date(v + "T00:00:00"));
|
|
301
|
+
if (!allowedDeliveryWeekdays.includes(wd)) {
|
|
302
|
+
const snapped = snapToWeekday(new Date(v + "T00:00:00"), allowedDeliveryWeekdays);
|
|
303
|
+
setDeliveryDate(toDateInput(snapped));
|
|
304
|
+
toast.message(`Vervoerder levert niet op deze dag — verzet naar ${formatDateNL(toDateInput(snapped))}`);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
setDeliveryDate(v);
|
|
309
|
+
} }), !deliveryGardenId ? (_jsx("p", { className: "text-xs text-amber-700", children: "Kies eerst een aflevertuin om de eerst mogelijke leverdatum te berekenen." })) : minDeliveryDate ? (_jsxs("p", { className: "text-xs text-slate-600", children: ["Eerst mogelijk: ", _jsx("strong", { children: formatDateNL(minDeliveryDate) })] })) : (_jsx("p", { className: "text-xs text-red-700", children: "Er kon geen leverdatum worden berekend \u2014 controleer de vervoerdersinstellingen." }))] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { className: "text-slate-800", children: "Opmerkingen (optioneel)" }), _jsx(Textarea, { placeholder: "Speciale wensen of vragen...", value: notes, onChange: (e) => setNotes(e.target.value), rows: 3 })] }), _jsxs(Button, { onClick: submit, disabled: busy || !hasItems || !name || !email || !deliveryGardenId || !deliveryDate, className: "w-full bg-green-600 hover:bg-green-700 text-white", size: "lg", children: [_jsx(Send, { className: "h-4 w-4 mr-2" }), busy ? "Bezig..." : "Bestelling plaatsen"] })] }) })] })] }), _jsxs("footer", { className: "bg-slate-900 text-slate-300 text-center py-5 text-sm", children: ["\u00A9 ", new Date().getFullYear(), " ", fromName || "Flow Selections"] })] }));
|
|
202
310
|
}
|