@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.
@@ -1 +1 @@
1
- {"version":3,"file":"AanbiedingsmailtjePage.d.ts","sourceRoot":"","sources":["../../src/components/AanbiedingsmailtjePage.tsx"],"names":[],"mappings":"AA0CA,wBAAgB,sBAAsB,gCAylBrC"}
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
- ...published.sort((a, b) => a.product.localeCompare(b.product)),
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 && (_jsx("div", { className: "max-h-40 overflow-y-auto border rounded-md", children: filteredProducts.slice(0, 20).map((p) => (_jsxs("button", { onClick: () => 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))) })), _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) => {
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
- updateProd(p.product_id, { tiers });
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
- updateProd(p.product_id, { tiers: [...p.tiers, { min_crates: nextMin, price_per_crate: p.normal_price }] });
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
- updateProd(p.product_id, { tiers: p.tiers.filter((_, i) => i !== idx) });
376
+ const tiers = p.tiers.filter((_, i) => i !== idx);
377
+ const maxPrice = Math.max(p.crate_price, ...tiers.map((t) => t.price_per_crate));
378
+ updateProd(p.product_id, { tiers, normal_price: Math.max(p.normal_price, maxPrice) });
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) => updateProd(p.product_id, { crate_price: Number(e.target.value) }) })] })] }), _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) => {
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":"AAqCA,wBAAgB,SAAS,gCAoexB"}
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
- function addBusinessDays(start, days) {
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 < days) {
23
+ while (added < count) {
12
24
  d.setDate(d.getDate() + 1);
13
- const day = d.getDay();
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(() => toDateInput(addBusinessDays(new Date(), 3)));
75
+ const [deliveryDate, setDeliveryDate] = useState("");
52
76
  const [placed, setPlaced] = useState(null);
53
77
  const [busy, setBusy] = useState(false);
54
- const minDate = useMemo(() => toDateInput(addBusinessDays(new Date(), 3)), []);
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
- if (!deliveryDate) {
149
- toast.error("Kies een leverdatum");
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: `Gewenste leverdatum: ${formatDateNL(deliveryDate)}${notes ? `\n\n${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 #", 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." })] }) }));
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(Truck, { className: "h-4 w-4 text-green-700" }), "Gewenste leverdatum"] }), _jsx(Input, { type: "date", min: minDate, value: deliveryDate, onChange: (e) => setDeliveryDate(e.target.value) }), _jsx("p", { className: "text-xs text-slate-500", children: "Minimaal 3 werkdagen van tevoren" })] }), _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, 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"] })] }));
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
  }