@xiwebdesign/xicms-core 5.5.3 → 5.6.1
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/admin/shop/ConnectionsTab.js +1 -1
- package/dist/admin/shop/ConnectionsTab.js.map +1 -1
- package/dist/admin/shop/DiscountsTab.d.ts +4 -0
- package/dist/admin/shop/DiscountsTab.js +168 -0
- package/dist/admin/shop/DiscountsTab.js.map +1 -0
- package/dist/admin/shop/EmailsTab.d.ts +8 -0
- package/dist/admin/shop/EmailsTab.js +88 -0
- package/dist/admin/shop/EmailsTab.js.map +1 -0
- package/dist/admin/shop/OrdersTab.js +271 -139
- package/dist/admin/shop/OrdersTab.js.map +1 -1
- package/dist/admin/shop/ProductsTab.js +152 -28
- package/dist/admin/shop/ProductsTab.js.map +1 -1
- package/dist/admin/shop/ShopPanel.d.ts +4 -1
- package/dist/admin/shop/ShopPanel.js +9 -0
- package/dist/admin/shop/ShopPanel.js.map +1 -1
- package/dist/admin/shop/ShopSettingsTab.d.ts +3 -0
- package/dist/admin/shop/ShopSettingsTab.js +86 -0
- package/dist/admin/shop/ShopSettingsTab.js.map +1 -0
- package/dist/admin.css +1 -1
- package/dist/blocks/context.d.ts +6 -0
- package/dist/blocks/context.js +15 -1
- package/dist/blocks/context.js.map +1 -1
- package/dist/modules/shop/blocks/Cart.js +22 -14
- package/dist/modules/shop/blocks/Cart.js.map +1 -1
- package/dist/modules/shop/blocks/Checkout.js +75 -23
- package/dist/modules/shop/blocks/Checkout.js.map +1 -1
- package/dist/modules/shop/blocks/Donation.js +8 -7
- package/dist/modules/shop/blocks/Donation.js.map +1 -1
- package/dist/modules/shop/blocks/ProductDetail.js +296 -109
- package/dist/modules/shop/blocks/ProductDetail.js.map +1 -1
- package/dist/modules/shop/blocks/ProductGrid.d.ts +9 -0
- package/dist/modules/shop/blocks/ProductGrid.js +144 -65
- package/dist/modules/shop/blocks/ProductGrid.js.map +1 -1
- package/dist/modules/shop/blocks/ShopCategories.js +7 -2
- package/dist/modules/shop/blocks/ShopCategories.js.map +1 -1
- package/dist/modules/shop/cartStore.d.ts +13 -2
- package/dist/modules/shop/cartStore.js +10 -5
- package/dist/modules/shop/cartStore.js.map +1 -1
- package/dist/modules/shop/lib.d.ts +18 -5
- package/dist/modules/shop/lib.js +23 -0
- package/dist/modules/shop/lib.js.map +1 -1
- package/dist/modules/shop/module.js +6 -1
- package/dist/modules/shop/module.js.map +1 -1
- package/dist/modules/shop/settings.d.ts +18 -0
- package/dist/modules/shop/settings.js +8 -1
- package/dist/modules/shop/settings.js.map +1 -1
- package/dist/modules/shop/strings.d.ts +65 -0
- package/dist/modules/shop/strings.js +136 -0
- package/dist/modules/shop/strings.js.map +1 -0
- package/dist/modules/shop/types.d.ts +18 -0
- package/dist/modules/shop/types.js +1 -0
- package/dist/modules/shop/types.js.map +1 -0
- package/dist/render/PublishedPage.d.ts +6 -1
- package/dist/render/PublishedPage.js +4 -3
- package/dist/render/PublishedPage.js.map +1 -1
- package/dist/schema/shop.d.ts +413 -2
- package/dist/schema/shop.js +95 -6
- package/dist/schema/shop.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +14 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/shop.d.ts +46 -0
- package/dist/server/shop.js +181 -0
- package/dist/server/shop.js.map +1 -0
- package/dist/styles.css +147 -0
- package/package.json +1 -1
|
@@ -123,7 +123,7 @@ function ConnectionsTab({ siteId }) {
|
|
|
123
123
|
/* @__PURE__ */ jsxs("p", { className: "text-sm text-slate-600", children: [
|
|
124
124
|
"Mail wordt verstuurd via je eigen mailbox (SMTP): bv. het e-mailadres bij je hostingpartij, of Gmail (smtp.gmail.com, poort 465, met een ",
|
|
125
125
|
/* @__PURE__ */ jsx("a", { className: "underline", href: "https://myaccount.google.com/apppasswords", target: "_blank", rel: "noreferrer", children: "app-wachtwoord" }),
|
|
126
|
-
"). Zonder SMTP-instellingen valt de mail terug op de centrale XiWebdesign-afzender."
|
|
126
|
+
"). Deze instelling geldt site-breed: \xF3\xF3k formulier-mails lopen erover. Zonder SMTP-instellingen valt de mail terug op de centrale XiWebdesign-afzender. De teksten van de mails pas je aan onder het tabblad E-mails."
|
|
127
127
|
] }),
|
|
128
128
|
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-[1fr_110px]", children: [
|
|
129
129
|
/* @__PURE__ */ jsx(Field, { label: "SMTP-server", children: /* @__PURE__ */ jsx(Input, { value: smtp.host, onChange: (e) => setSmtp((p) => ({ ...p, host: e.target.value })), placeholder: "smtp.gmail.com" }) }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/shop/ConnectionsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Badge, Button, Card, Field, Input } from \"../ui/primitives\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\nimport { shopCallable } from \"./lib\";\n\ninterface ShopStatus {\n stripe: { configured: boolean; account?: string; webhookConfigured: boolean };\n smtp: { configured: boolean; host?: string; port?: number; user?: string; fromName?: string; fromEmail?: string; notifyTo?: string };\n}\n\n/**\n * Koppelingen — Stripe (eigen account van de klant) en e-mail (eigen SMTP, bv. Gmail of de\n * mailbox bij de hostingpartij). Geheimen gaan uitsluitend via callables naar\n * `sites/{siteId}/private/shop` (deny-all rules); dit paneel toont alleen gemaskeerde status.\n * Alleen een siteadmin kan opslaan (de callables dwingen dat af).\n */\nexport function ConnectionsTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [status, setStatus] = useState<ShopStatus | null>(null);\n const [busy, setBusy] = useState(false);\n\n const [stripeKey, setStripeKey] = useState(\"\");\n const [smtp, setSmtp] = useState({ host: \"\", port: \"465\", secure: true, user: \"\", pass: \"\", fromName: \"\", fromEmail: \"\", notifyTo: \"\" });\n\n const refresh = useCallback(async () => {\n try {\n const s = await shopCallable<{ siteId: string }, ShopStatus>(\"getShopStatus\")({ siteId });\n setStatus(s);\n setSmtp((p) => ({\n ...p,\n host: s.smtp.host ?? p.host,\n port: String(s.smtp.port ?? 465),\n user: s.smtp.user ?? p.user,\n fromName: s.smtp.fromName ?? p.fromName,\n fromEmail: s.smtp.fromEmail ?? p.fromEmail,\n notifyTo: s.smtp.notifyTo ?? p.notifyTo,\n }));\n } catch {\n setStatus(null);\n }\n }, [siteId]);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n async function saveStripe() {\n if (!stripeKey.trim().startsWith(\"rk_\") && !stripeKey.trim().startsWith(\"sk_\")) {\n toast({ title: \"Ongeldige sleutel\", description: \"Plak een geheime sleutel (sk_…) of restricted key (rk_…).\", tone: \"error\" });\n return;\n }\n setBusy(true);\n try {\n const res = await shopCallable<{ siteId: string; stripe: { secretKey: string } }, { account?: string }>(\"saveShopSecrets\")({\n siteId,\n stripe: { secretKey: stripeKey.trim() },\n });\n toast({ title: \"Stripe gekoppeld\", description: res.account ? `Account: ${res.account}` : undefined, tone: \"success\" });\n setStripeKey(\"\");\n await refresh();\n } catch (e) {\n toast({ title: \"Stripe koppelen mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n async function saveSmtp() {\n setBusy(true);\n try {\n await shopCallable<{ siteId: string; smtp: Record<string, unknown> }, { ok: boolean }>(\"saveShopSecrets\")({\n siteId,\n smtp: {\n host: smtp.host.trim(),\n port: Number(smtp.port) || 465,\n secure: smtp.secure,\n user: smtp.user.trim(),\n // Leeg laten = bestaand wachtwoord behouden.\n ...(smtp.pass ? { pass: smtp.pass } : {}),\n fromName: smtp.fromName.trim(),\n fromEmail: smtp.fromEmail.trim(),\n notifyTo: smtp.notifyTo.trim(),\n },\n });\n toast({ title: \"E-mailinstellingen opgeslagen\", tone: \"success\" });\n setSmtp((p) => ({ ...p, pass: \"\" }));\n await refresh();\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n async function sendTest() {\n setBusy(true);\n try {\n const res = await shopCallable<{ siteId: string }, { to: string }>(\"sendShopTestMail\")({ siteId });\n toast({ title: \"Testmail verstuurd\", description: `Naar ${res.to} — check de inbox (en spam).`, tone: \"success\" });\n } catch (e) {\n toast({ title: \"Testmail mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n return (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">Stripe (betalingen)</h3>\n {status && (\n <Badge tone={status.stripe.configured ? \"green\" : \"amber\"}>\n {status.stripe.configured ? `Gekoppeld${status.stripe.account ? ` — ${status.stripe.account}` : \"\"}` : \"Nog niet gekoppeld\"}\n </Badge>\n )}\n </div>\n <ol className=\"grid list-decimal gap-1 pl-5 text-sm text-slate-600\">\n <li>Maak een gratis account op <a className=\"underline\" href=\"https://dashboard.stripe.com/register\" target=\"_blank\" rel=\"noreferrer\">stripe.com</a> en doorloop de activatie (IBAN, KVK).</li>\n <li>Zet in Stripe onder <em>Settings → Payment methods</em> iDEAL aan.</li>\n <li>Maak onder <em>Developers → API keys</em> een sleutel aan en plak hem hieronder. De webhook wordt automatisch voor je aangemaakt.</li>\n </ol>\n <Field label=\"Geheime sleutel (sk_live_… of rk_live_…)\" hint=\"Wordt veilig opgeslagen; nooit zichtbaar in de browser\">\n <Input type=\"password\" value={stripeKey} onChange={(e) => setStripeKey(e.target.value)} placeholder=\"sk_live_…\" />\n </Field>\n <div>\n <Button onClick={saveStripe} disabled={busy || !stripeKey.trim()}>Koppel Stripe</Button>\n </div>\n {status?.stripe.configured && !status.stripe.webhookConfigured && (\n <p className=\"rounded-lg bg-amber-50 px-3 py-2 text-sm text-amber-700\">\n Webhook nog niet geregistreerd — koppel opnieuw met een sleutel die webhook-rechten heeft.\n </p>\n )}\n </div>\n </Card>\n\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">E-mail (bestelbevestigingen)</h3>\n {status && (\n <Badge tone={status.smtp.configured ? \"green\" : \"amber\"}>\n {status.smtp.configured ? \"Ingesteld\" : \"Nog niet ingesteld\"}\n </Badge>\n )}\n </div>\n <p className=\"text-sm text-slate-600\">\n Mail wordt verstuurd via je eigen mailbox (SMTP): bv. het e-mailadres bij je hostingpartij, of Gmail\n (smtp.gmail.com, poort 465, met een <a className=\"underline\" href=\"https://myaccount.google.com/apppasswords\" target=\"_blank\" rel=\"noreferrer\">app-wachtwoord</a>).\n Zonder SMTP-instellingen valt de mail terug op de centrale XiWebdesign-afzender.\n </p>\n <div className=\"grid gap-4 md:grid-cols-[1fr_110px]\">\n <Field label=\"SMTP-server\">\n <Input value={smtp.host} onChange={(e) => setSmtp((p) => ({ ...p, host: e.target.value }))} placeholder=\"smtp.gmail.com\" />\n </Field>\n <Field label=\"Poort\">\n <Input value={smtp.port} onChange={(e) => setSmtp((p) => ({ ...p, port: e.target.value }))} placeholder=\"465\" />\n </Field>\n </div>\n <Switch checked={smtp.secure} onCheckedChange={(v) => setSmtp((p) => ({ ...p, secure: v }))} label=\"SSL (poort 465). Uit = STARTTLS (poort 587)\" />\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Field label=\"Gebruikersnaam\">\n <Input value={smtp.user} onChange={(e) => setSmtp((p) => ({ ...p, user: e.target.value }))} placeholder=\"info@jouwdomein.nl\" />\n </Field>\n <Field label=\"Wachtwoord\" hint={status?.smtp.configured ? \"Leeg laten = ongewijzigd\" : undefined}>\n <Input type=\"password\" value={smtp.pass} onChange={(e) => setSmtp((p) => ({ ...p, pass: e.target.value }))} />\n </Field>\n </div>\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Field label=\"Afzendernaam\">\n <Input value={smtp.fromName} onChange={(e) => setSmtp((p) => ({ ...p, fromName: e.target.value }))} placeholder=\"Naam van je winkel\" />\n </Field>\n <Field label=\"Afzenderadres\">\n <Input value={smtp.fromEmail} onChange={(e) => setSmtp((p) => ({ ...p, fromEmail: e.target.value }))} placeholder=\"info@jouwdomein.nl\" />\n </Field>\n </div>\n <Field label=\"Notificatie-adres\" hint=\"Hier ontvang je zelf een mail bij elke bestelling\">\n <Input value={smtp.notifyTo} onChange={(e) => setSmtp((p) => ({ ...p, notifyTo: e.target.value }))} placeholder=\"bestellingen@jouwdomein.nl\" />\n </Field>\n <div className=\"flex items-center gap-2\">\n <Button onClick={saveSmtp} disabled={busy || !smtp.host.trim()}>Opslaan</Button>\n <Button variant=\"secondary\" onClick={sendTest} disabled={busy || !status?.smtp.configured}>Stuur testmail</Button>\n </div>\n </div>\n </Card>\n\n <Card className=\"lg:col-span-2\">\n <p className=\"text-sm text-slate-600\">\n Verzendkosten, valuta, ordernummer-prefix en de winkel-paden stel je in onder{\" \"}\n <strong>Instellingen → Modules → Webwinkel</strong>.\n </p>\n </Card>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/shop/ConnectionsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Badge, Button, Card, Field, Input } from \"../ui/primitives\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\nimport { shopCallable } from \"./lib\";\n\ninterface ShopStatus {\n stripe: { configured: boolean; account?: string; webhookConfigured: boolean };\n smtp: { configured: boolean; host?: string; port?: number; user?: string; fromName?: string; fromEmail?: string; notifyTo?: string };\n}\n\n/**\n * Koppelingen — Stripe (eigen account van de klant) en e-mail (eigen SMTP, bv. Gmail of de\n * mailbox bij de hostingpartij). Geheimen gaan uitsluitend via callables naar\n * `sites/{siteId}/private/shop` (deny-all rules); dit paneel toont alleen gemaskeerde status.\n * Alleen een siteadmin kan opslaan (de callables dwingen dat af).\n */\nexport function ConnectionsTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [status, setStatus] = useState<ShopStatus | null>(null);\n const [busy, setBusy] = useState(false);\n\n const [stripeKey, setStripeKey] = useState(\"\");\n const [smtp, setSmtp] = useState({ host: \"\", port: \"465\", secure: true, user: \"\", pass: \"\", fromName: \"\", fromEmail: \"\", notifyTo: \"\" });\n\n const refresh = useCallback(async () => {\n try {\n const s = await shopCallable<{ siteId: string }, ShopStatus>(\"getShopStatus\")({ siteId });\n setStatus(s);\n setSmtp((p) => ({\n ...p,\n host: s.smtp.host ?? p.host,\n port: String(s.smtp.port ?? 465),\n user: s.smtp.user ?? p.user,\n fromName: s.smtp.fromName ?? p.fromName,\n fromEmail: s.smtp.fromEmail ?? p.fromEmail,\n notifyTo: s.smtp.notifyTo ?? p.notifyTo,\n }));\n } catch {\n setStatus(null);\n }\n }, [siteId]);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n async function saveStripe() {\n if (!stripeKey.trim().startsWith(\"rk_\") && !stripeKey.trim().startsWith(\"sk_\")) {\n toast({ title: \"Ongeldige sleutel\", description: \"Plak een geheime sleutel (sk_…) of restricted key (rk_…).\", tone: \"error\" });\n return;\n }\n setBusy(true);\n try {\n const res = await shopCallable<{ siteId: string; stripe: { secretKey: string } }, { account?: string }>(\"saveShopSecrets\")({\n siteId,\n stripe: { secretKey: stripeKey.trim() },\n });\n toast({ title: \"Stripe gekoppeld\", description: res.account ? `Account: ${res.account}` : undefined, tone: \"success\" });\n setStripeKey(\"\");\n await refresh();\n } catch (e) {\n toast({ title: \"Stripe koppelen mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n async function saveSmtp() {\n setBusy(true);\n try {\n await shopCallable<{ siteId: string; smtp: Record<string, unknown> }, { ok: boolean }>(\"saveShopSecrets\")({\n siteId,\n smtp: {\n host: smtp.host.trim(),\n port: Number(smtp.port) || 465,\n secure: smtp.secure,\n user: smtp.user.trim(),\n // Leeg laten = bestaand wachtwoord behouden.\n ...(smtp.pass ? { pass: smtp.pass } : {}),\n fromName: smtp.fromName.trim(),\n fromEmail: smtp.fromEmail.trim(),\n notifyTo: smtp.notifyTo.trim(),\n },\n });\n toast({ title: \"E-mailinstellingen opgeslagen\", tone: \"success\" });\n setSmtp((p) => ({ ...p, pass: \"\" }));\n await refresh();\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n async function sendTest() {\n setBusy(true);\n try {\n const res = await shopCallable<{ siteId: string }, { to: string }>(\"sendShopTestMail\")({ siteId });\n toast({ title: \"Testmail verstuurd\", description: `Naar ${res.to} — check de inbox (en spam).`, tone: \"success\" });\n } catch (e) {\n toast({ title: \"Testmail mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n return (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">Stripe (betalingen)</h3>\n {status && (\n <Badge tone={status.stripe.configured ? \"green\" : \"amber\"}>\n {status.stripe.configured ? `Gekoppeld${status.stripe.account ? ` — ${status.stripe.account}` : \"\"}` : \"Nog niet gekoppeld\"}\n </Badge>\n )}\n </div>\n <ol className=\"grid list-decimal gap-1 pl-5 text-sm text-slate-600\">\n <li>Maak een gratis account op <a className=\"underline\" href=\"https://dashboard.stripe.com/register\" target=\"_blank\" rel=\"noreferrer\">stripe.com</a> en doorloop de activatie (IBAN, KVK).</li>\n <li>Zet in Stripe onder <em>Settings → Payment methods</em> iDEAL aan.</li>\n <li>Maak onder <em>Developers → API keys</em> een sleutel aan en plak hem hieronder. De webhook wordt automatisch voor je aangemaakt.</li>\n </ol>\n <Field label=\"Geheime sleutel (sk_live_… of rk_live_…)\" hint=\"Wordt veilig opgeslagen; nooit zichtbaar in de browser\">\n <Input type=\"password\" value={stripeKey} onChange={(e) => setStripeKey(e.target.value)} placeholder=\"sk_live_…\" />\n </Field>\n <div>\n <Button onClick={saveStripe} disabled={busy || !stripeKey.trim()}>Koppel Stripe</Button>\n </div>\n {status?.stripe.configured && !status.stripe.webhookConfigured && (\n <p className=\"rounded-lg bg-amber-50 px-3 py-2 text-sm text-amber-700\">\n Webhook nog niet geregistreerd — koppel opnieuw met een sleutel die webhook-rechten heeft.\n </p>\n )}\n </div>\n </Card>\n\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">E-mail (bestelbevestigingen)</h3>\n {status && (\n <Badge tone={status.smtp.configured ? \"green\" : \"amber\"}>\n {status.smtp.configured ? \"Ingesteld\" : \"Nog niet ingesteld\"}\n </Badge>\n )}\n </div>\n <p className=\"text-sm text-slate-600\">\n Mail wordt verstuurd via je eigen mailbox (SMTP): bv. het e-mailadres bij je hostingpartij, of Gmail\n (smtp.gmail.com, poort 465, met een <a className=\"underline\" href=\"https://myaccount.google.com/apppasswords\" target=\"_blank\" rel=\"noreferrer\">app-wachtwoord</a>).\n Deze instelling geldt site-breed: óók formulier-mails lopen erover. Zonder SMTP-instellingen\n valt de mail terug op de centrale XiWebdesign-afzender. De teksten van de mails pas je aan\n onder het tabblad E-mails.\n </p>\n <div className=\"grid gap-4 md:grid-cols-[1fr_110px]\">\n <Field label=\"SMTP-server\">\n <Input value={smtp.host} onChange={(e) => setSmtp((p) => ({ ...p, host: e.target.value }))} placeholder=\"smtp.gmail.com\" />\n </Field>\n <Field label=\"Poort\">\n <Input value={smtp.port} onChange={(e) => setSmtp((p) => ({ ...p, port: e.target.value }))} placeholder=\"465\" />\n </Field>\n </div>\n <Switch checked={smtp.secure} onCheckedChange={(v) => setSmtp((p) => ({ ...p, secure: v }))} label=\"SSL (poort 465). Uit = STARTTLS (poort 587)\" />\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Field label=\"Gebruikersnaam\">\n <Input value={smtp.user} onChange={(e) => setSmtp((p) => ({ ...p, user: e.target.value }))} placeholder=\"info@jouwdomein.nl\" />\n </Field>\n <Field label=\"Wachtwoord\" hint={status?.smtp.configured ? \"Leeg laten = ongewijzigd\" : undefined}>\n <Input type=\"password\" value={smtp.pass} onChange={(e) => setSmtp((p) => ({ ...p, pass: e.target.value }))} />\n </Field>\n </div>\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Field label=\"Afzendernaam\">\n <Input value={smtp.fromName} onChange={(e) => setSmtp((p) => ({ ...p, fromName: e.target.value }))} placeholder=\"Naam van je winkel\" />\n </Field>\n <Field label=\"Afzenderadres\">\n <Input value={smtp.fromEmail} onChange={(e) => setSmtp((p) => ({ ...p, fromEmail: e.target.value }))} placeholder=\"info@jouwdomein.nl\" />\n </Field>\n </div>\n <Field label=\"Notificatie-adres\" hint=\"Hier ontvang je zelf een mail bij elke bestelling\">\n <Input value={smtp.notifyTo} onChange={(e) => setSmtp((p) => ({ ...p, notifyTo: e.target.value }))} placeholder=\"bestellingen@jouwdomein.nl\" />\n </Field>\n <div className=\"flex items-center gap-2\">\n <Button onClick={saveSmtp} disabled={busy || !smtp.host.trim()}>Opslaan</Button>\n <Button variant=\"secondary\" onClick={sendTest} disabled={busy || !status?.smtp.configured}>Stuur testmail</Button>\n </div>\n </div>\n </Card>\n\n <Card className=\"lg:col-span-2\">\n <p className=\"text-sm text-slate-600\">\n Verzendkosten, valuta, ordernummer-prefix en de winkel-paden stel je in onder{\" \"}\n <strong>Instellingen → Modules → Webwinkel</strong>.\n </p>\n </Card>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { collection, deleteDoc, doc, onSnapshot, setDoc } from "firebase/firestore";
|
|
5
|
+
import { getDb } from "../../firebase/client";
|
|
6
|
+
import { DiscountCodeSchema, formatCents } from "../../schema/shop";
|
|
7
|
+
import { Badge, Button, Card, Field, Input } from "../ui/primitives";
|
|
8
|
+
import { Select } from "../ui/select";
|
|
9
|
+
import { Switch } from "../ui/switch";
|
|
10
|
+
import { useToast } from "../ui/toast";
|
|
11
|
+
import { useConfirm } from "../ui/ConfirmDialog";
|
|
12
|
+
import { centsToEuroInput, eurosToCents } from "./lib";
|
|
13
|
+
function DiscountsTab({ siteId }) {
|
|
14
|
+
const { toast } = useToast();
|
|
15
|
+
const confirm = useConfirm();
|
|
16
|
+
const [codes, setCodes] = useState([]);
|
|
17
|
+
const [selected, setSelected] = useState(null);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const db = getDb();
|
|
20
|
+
if (!db) return;
|
|
21
|
+
return onSnapshot(
|
|
22
|
+
collection(db, "sites", siteId, "discountCodes"),
|
|
23
|
+
(snap) => setCodes(snap.docs.map((d) => ({ id: d.id, ...d.data() })).sort((a, b) => a.code.localeCompare(b.code)))
|
|
24
|
+
);
|
|
25
|
+
}, [siteId]);
|
|
26
|
+
const current = codes.find((c) => c.id === selected) ?? null;
|
|
27
|
+
return /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-[280px_1fr]", children: [
|
|
28
|
+
/* @__PURE__ */ jsxs(Card, { className: "h-fit", children: [
|
|
29
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between", children: [
|
|
30
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "Kortingscodes" }),
|
|
31
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => setSelected("__new__"), children: "+ Nieuw" })
|
|
32
|
+
] }),
|
|
33
|
+
/* @__PURE__ */ jsxs("ul", { className: "grid gap-1", children: [
|
|
34
|
+
codes.map((c) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
35
|
+
"button",
|
|
36
|
+
{
|
|
37
|
+
onClick: () => setSelected(c.id),
|
|
38
|
+
className: `flex w-full items-center justify-between gap-2 rounded-lg px-3 py-2 text-left text-sm ${c.id === selected ? "bg-brand-50 text-brand-700" : "hover:bg-slate-100"}`,
|
|
39
|
+
children: [
|
|
40
|
+
/* @__PURE__ */ jsx("span", { className: "font-mono font-semibold", children: c.code }),
|
|
41
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1", children: [
|
|
42
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-slate-500", children: c.type === "percent" ? `${c.value}%` : formatCents(c.value) }),
|
|
43
|
+
/* @__PURE__ */ jsx(Badge, { tone: c.active !== false ? "green" : "slate", children: c.active !== false ? "actief" : "uit" })
|
|
44
|
+
] })
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
) }, c.id)),
|
|
48
|
+
codes.length === 0 && /* @__PURE__ */ jsx("li", { className: "px-3 py-2 text-sm text-slate-500", children: "Nog geen kortingscodes." })
|
|
49
|
+
] })
|
|
50
|
+
] }),
|
|
51
|
+
current || selected === "__new__" ? /* @__PURE__ */ jsx(
|
|
52
|
+
DiscountDetail,
|
|
53
|
+
{
|
|
54
|
+
siteId,
|
|
55
|
+
existing: current,
|
|
56
|
+
onDone: () => setSelected(null),
|
|
57
|
+
onToast: toast,
|
|
58
|
+
confirm
|
|
59
|
+
},
|
|
60
|
+
selected
|
|
61
|
+
) : /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Selecteer of maak een kortingscode." }) })
|
|
62
|
+
] });
|
|
63
|
+
}
|
|
64
|
+
function DiscountDetail({
|
|
65
|
+
siteId,
|
|
66
|
+
existing,
|
|
67
|
+
onDone,
|
|
68
|
+
onToast,
|
|
69
|
+
confirm
|
|
70
|
+
}) {
|
|
71
|
+
const [def, setDef] = useState(
|
|
72
|
+
existing ?? { code: "", type: "percent", value: 10, minOrderCents: 0, freeShipping: false, active: true, maxUses: 0, usedCount: 0 }
|
|
73
|
+
);
|
|
74
|
+
const [fixedInput, setFixedInput] = useState(existing?.type === "fixed" ? centsToEuroInput(existing.value) : "");
|
|
75
|
+
const [minInput, setMinInput] = useState(existing?.minOrderCents ? centsToEuroInput(existing.minOrderCents) : "");
|
|
76
|
+
async function save() {
|
|
77
|
+
const db = getDb();
|
|
78
|
+
if (!db) return;
|
|
79
|
+
const value = def.type === "fixed" ? eurosToCents(fixedInput) ?? 0 : def.value;
|
|
80
|
+
const parsed = DiscountCodeSchema.safeParse({
|
|
81
|
+
...def,
|
|
82
|
+
code: def.code.toUpperCase(),
|
|
83
|
+
value,
|
|
84
|
+
minOrderCents: minInput.trim() === "" ? 0 : eurosToCents(minInput) ?? 0
|
|
85
|
+
});
|
|
86
|
+
if (!parsed.success) {
|
|
87
|
+
onToast({ title: "Ongeldige code", description: parsed.error.issues[0]?.message, tone: "error" });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await setDoc(doc(db, "sites", siteId, "discountCodes", parsed.data.code), parsed.data);
|
|
91
|
+
onToast({ title: "Kortingscode opgeslagen", tone: "success" });
|
|
92
|
+
if (!existing) onDone();
|
|
93
|
+
}
|
|
94
|
+
async function remove() {
|
|
95
|
+
const db = getDb();
|
|
96
|
+
if (!db || !existing) return;
|
|
97
|
+
const ok = await confirm({ title: `Code ${existing.code} verwijderen?`, tone: "danger" });
|
|
98
|
+
if (!ok) return;
|
|
99
|
+
await deleteDoc(doc(db, "sites", siteId, "discountCodes", existing.id));
|
|
100
|
+
onDone();
|
|
101
|
+
}
|
|
102
|
+
return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
|
|
103
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-3", children: [
|
|
104
|
+
/* @__PURE__ */ jsx(Field, { label: "Code", hint: "Hoofdletters, cijfers, streepjes", children: /* @__PURE__ */ jsx(
|
|
105
|
+
Input,
|
|
106
|
+
{
|
|
107
|
+
value: def.code,
|
|
108
|
+
disabled: Boolean(existing),
|
|
109
|
+
onChange: (e) => setDef((p) => ({ ...p, code: e.target.value.toUpperCase().replace(/[^A-Z0-9-]/g, "") })),
|
|
110
|
+
placeholder: "WELKOM10"
|
|
111
|
+
}
|
|
112
|
+
) }),
|
|
113
|
+
/* @__PURE__ */ jsx(Field, { label: "Soort", children: /* @__PURE__ */ jsx(
|
|
114
|
+
Select,
|
|
115
|
+
{
|
|
116
|
+
value: def.type,
|
|
117
|
+
onValueChange: (v) => setDef((p) => ({ ...p, type: v })),
|
|
118
|
+
options: [
|
|
119
|
+
{ value: "percent", label: "Percentage" },
|
|
120
|
+
{ value: "fixed", label: "Vast bedrag" }
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
) }),
|
|
124
|
+
def.type === "percent" ? /* @__PURE__ */ jsx(Field, { label: "Percentage", children: /* @__PURE__ */ jsx(
|
|
125
|
+
Input,
|
|
126
|
+
{
|
|
127
|
+
type: "number",
|
|
128
|
+
min: 1,
|
|
129
|
+
max: 100,
|
|
130
|
+
value: def.value,
|
|
131
|
+
onChange: (e) => setDef((p) => ({ ...p, value: Math.max(1, Math.min(100, Number(e.target.value) || 0)) })),
|
|
132
|
+
className: "w-24"
|
|
133
|
+
}
|
|
134
|
+
) }) : /* @__PURE__ */ jsx(Field, { label: "Bedrag (\u20AC)", children: /* @__PURE__ */ jsx(Input, { value: fixedInput, onChange: (e) => setFixedInput(e.target.value), placeholder: "5,00", className: "w-24" }) })
|
|
135
|
+
] }),
|
|
136
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-3", children: [
|
|
137
|
+
/* @__PURE__ */ jsx(Field, { label: "Minimaal orderbedrag (\u20AC)", hint: "Leeg = geen minimum", children: /* @__PURE__ */ jsx(Input, { value: minInput, onChange: (e) => setMinInput(e.target.value), placeholder: "25,00", className: "w-28" }) }),
|
|
138
|
+
/* @__PURE__ */ jsx(Field, { label: "Geldig van", hint: "Leeg = direct", children: /* @__PURE__ */ jsx(Input, { type: "date", value: def.startsAt ?? "", onChange: (e) => setDef((p) => ({ ...p, startsAt: e.target.value || void 0 })) }) }),
|
|
139
|
+
/* @__PURE__ */ jsx(Field, { label: "Geldig t/m", hint: "Leeg = onbeperkt", children: /* @__PURE__ */ jsx(Input, { type: "date", value: def.endsAt ?? "", onChange: (e) => setDef((p) => ({ ...p, endsAt: e.target.value || void 0 })) }) })
|
|
140
|
+
] }),
|
|
141
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-6", children: [
|
|
142
|
+
/* @__PURE__ */ jsx(Field, { label: "Max. aantal keer te gebruiken", hint: "0 = onbeperkt", children: /* @__PURE__ */ jsx(
|
|
143
|
+
Input,
|
|
144
|
+
{
|
|
145
|
+
type: "number",
|
|
146
|
+
min: 0,
|
|
147
|
+
value: def.maxUses ?? 0,
|
|
148
|
+
onChange: (e) => setDef((p) => ({ ...p, maxUses: Math.max(0, Number(e.target.value) || 0) })),
|
|
149
|
+
className: "w-24"
|
|
150
|
+
}
|
|
151
|
+
) }),
|
|
152
|
+
/* @__PURE__ */ jsx(Switch, { checked: def.freeShipping === true, onCheckedChange: (v) => setDef((p) => ({ ...p, freeShipping: v })), label: "Gratis verzending" }),
|
|
153
|
+
/* @__PURE__ */ jsx(Switch, { checked: def.active !== false, onCheckedChange: (v) => setDef((p) => ({ ...p, active: v })), label: "Actief" }),
|
|
154
|
+
existing && /* @__PURE__ */ jsxs("span", { className: "text-sm text-slate-500", children: [
|
|
155
|
+
existing.usedCount ?? 0,
|
|
156
|
+
"\xD7 gebruikt"
|
|
157
|
+
] })
|
|
158
|
+
] }),
|
|
159
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-t border-slate-100 pt-4", children: [
|
|
160
|
+
/* @__PURE__ */ jsx(Button, { onClick: save, disabled: !def.code, children: "Opslaan" }),
|
|
161
|
+
existing && /* @__PURE__ */ jsx(Button, { variant: "danger", onClick: remove, children: "Verwijderen" })
|
|
162
|
+
] })
|
|
163
|
+
] }) });
|
|
164
|
+
}
|
|
165
|
+
export {
|
|
166
|
+
DiscountsTab
|
|
167
|
+
};
|
|
168
|
+
//# sourceMappingURL=DiscountsTab.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/shop/DiscountsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { collection, deleteDoc, doc, onSnapshot, setDoc } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport { DiscountCodeSchema, formatCents, type DiscountCode } from \"../../schema/shop\";\nimport { Badge, Button, Card, Field, Input } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { centsToEuroInput, eurosToCents } from \"./lib\";\n\ntype Stored = DiscountCode & { id: string };\n\n/** Kortingscodes — doc-id = code; de webhook telt het gebruik op. */\nexport function DiscountsTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [codes, setCodes] = useState<Stored[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n return onSnapshot(collection(db, \"sites\", siteId, \"discountCodes\"), (snap) =>\n setCodes(snap.docs.map((d) => ({ id: d.id, ...(d.data() as DiscountCode) })).sort((a, b) => a.code.localeCompare(b.code))),\n );\n }, [siteId]);\n\n const current = codes.find((c) => c.id === selected) ?? null;\n\n return (\n <div className=\"grid gap-4 md:grid-cols-[280px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">Kortingscodes</h3>\n <Button size=\"sm\" onClick={() => setSelected(\"__new__\")}>+ Nieuw</Button>\n </div>\n <ul className=\"grid gap-1\">\n {codes.map((c) => (\n <li key={c.id}>\n <button\n onClick={() => setSelected(c.id)}\n className={`flex w-full items-center justify-between gap-2 rounded-lg px-3 py-2 text-left text-sm ${\n c.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n <span className=\"font-mono font-semibold\">{c.code}</span>\n <span className=\"flex items-center gap-1\">\n <span className=\"text-xs text-slate-500\">\n {c.type === \"percent\" ? `${c.value}%` : formatCents(c.value)}\n </span>\n <Badge tone={c.active !== false ? \"green\" : \"slate\"}>{c.active !== false ? \"actief\" : \"uit\"}</Badge>\n </span>\n </button>\n </li>\n ))}\n {codes.length === 0 && <li className=\"px-3 py-2 text-sm text-slate-500\">Nog geen kortingscodes.</li>}\n </ul>\n </Card>\n {current || selected === \"__new__\" ? (\n <DiscountDetail\n key={selected}\n siteId={siteId}\n existing={current}\n onDone={() => setSelected(null)}\n onToast={toast}\n confirm={confirm}\n />\n ) : (\n <Card><p className=\"text-sm text-slate-500\">Selecteer of maak een kortingscode.</p></Card>\n )}\n </div>\n );\n}\n\nfunction DiscountDetail({\n siteId,\n existing,\n onDone,\n onToast,\n confirm,\n}: {\n siteId: string;\n existing: Stored | null;\n onDone: () => void;\n onToast: (t: { title: string; description?: string; tone?: \"default\" | \"success\" | \"error\" }) => void;\n confirm: (opts: { title: string; description?: string; tone?: \"default\" | \"danger\" }) => Promise<boolean>;\n}) {\n const [def, setDef] = useState<DiscountCode>(\n existing ?? { code: \"\", type: \"percent\", value: 10, minOrderCents: 0, freeShipping: false, active: true, maxUses: 0, usedCount: 0 },\n );\n const [fixedInput, setFixedInput] = useState(existing?.type === \"fixed\" ? centsToEuroInput(existing.value) : \"\");\n const [minInput, setMinInput] = useState(existing?.minOrderCents ? centsToEuroInput(existing.minOrderCents) : \"\");\n\n async function save() {\n const db = getDb();\n if (!db) return;\n const value = def.type === \"fixed\" ? (eurosToCents(fixedInput) ?? 0) : def.value;\n const parsed = DiscountCodeSchema.safeParse({\n ...def,\n code: def.code.toUpperCase(),\n value,\n minOrderCents: minInput.trim() === \"\" ? 0 : (eurosToCents(minInput) ?? 0),\n });\n if (!parsed.success) {\n onToast({ title: \"Ongeldige code\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n await setDoc(doc(db, \"sites\", siteId, \"discountCodes\", parsed.data.code), parsed.data);\n onToast({ title: \"Kortingscode opgeslagen\", tone: \"success\" });\n if (!existing) onDone();\n }\n\n async function remove() {\n const db = getDb();\n if (!db || !existing) return;\n const ok = await confirm({ title: `Code ${existing.code} verwijderen?`, tone: \"danger\" });\n if (!ok) return;\n await deleteDoc(doc(db, \"sites\", siteId, \"discountCodes\", existing.id));\n onDone();\n }\n\n return (\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"grid gap-4 md:grid-cols-3\">\n <Field label=\"Code\" hint=\"Hoofdletters, cijfers, streepjes\">\n <Input\n value={def.code}\n disabled={Boolean(existing)}\n onChange={(e) => setDef((p) => ({ ...p, code: e.target.value.toUpperCase().replace(/[^A-Z0-9-]/g, \"\") }))}\n placeholder=\"WELKOM10\"\n />\n </Field>\n <Field label=\"Soort\">\n <Select\n value={def.type}\n onValueChange={(v) => setDef((p) => ({ ...p, type: v as \"percent\" | \"fixed\" }))}\n options={[\n { value: \"percent\", label: \"Percentage\" },\n { value: \"fixed\", label: \"Vast bedrag\" },\n ]}\n />\n </Field>\n {def.type === \"percent\" ? (\n <Field label=\"Percentage\">\n <Input\n type=\"number\"\n min={1}\n max={100}\n value={def.value}\n onChange={(e) => setDef((p) => ({ ...p, value: Math.max(1, Math.min(100, Number(e.target.value) || 0)) }))}\n className=\"w-24\"\n />\n </Field>\n ) : (\n <Field label=\"Bedrag (€)\">\n <Input value={fixedInput} onChange={(e) => setFixedInput(e.target.value)} placeholder=\"5,00\" className=\"w-24\" />\n </Field>\n )}\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-3\">\n <Field label=\"Minimaal orderbedrag (€)\" hint=\"Leeg = geen minimum\">\n <Input value={minInput} onChange={(e) => setMinInput(e.target.value)} placeholder=\"25,00\" className=\"w-28\" />\n </Field>\n <Field label=\"Geldig van\" hint=\"Leeg = direct\">\n <Input type=\"date\" value={def.startsAt ?? \"\"} onChange={(e) => setDef((p) => ({ ...p, startsAt: e.target.value || undefined }))} />\n </Field>\n <Field label=\"Geldig t/m\" hint=\"Leeg = onbeperkt\">\n <Input type=\"date\" value={def.endsAt ?? \"\"} onChange={(e) => setDef((p) => ({ ...p, endsAt: e.target.value || undefined }))} />\n </Field>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-6\">\n <Field label=\"Max. aantal keer te gebruiken\" hint=\"0 = onbeperkt\">\n <Input\n type=\"number\"\n min={0}\n value={def.maxUses ?? 0}\n onChange={(e) => setDef((p) => ({ ...p, maxUses: Math.max(0, Number(e.target.value) || 0) }))}\n className=\"w-24\"\n />\n </Field>\n <Switch checked={def.freeShipping === true} onCheckedChange={(v) => setDef((p) => ({ ...p, freeShipping: v }))} label=\"Gratis verzending\" />\n <Switch checked={def.active !== false} onCheckedChange={(v) => setDef((p) => ({ ...p, active: v }))} label=\"Actief\" />\n {existing && (\n <span className=\"text-sm text-slate-500\">{existing.usedCount ?? 0}× gebruikt</span>\n )}\n </div>\n\n <div className=\"flex items-center justify-between border-t border-slate-100 pt-4\">\n <Button onClick={save} disabled={!def.code}>Opslaan</Button>\n {existing && <Button variant=\"danger\" onClick={remove}>Verwijderen</Button>}\n </div>\n </div>\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E-mails — bewerkbare templates in `settings/mailTemplates` (siteadmin). Leeg veld = de
|
|
3
|
+
* ingebouwde standaardtekst; er gaat dus altijd een correcte mail uit. Platte tekst met
|
|
4
|
+
* placeholders; de opmaak (afzender, huisstijl) regelt de engine.
|
|
5
|
+
*/
|
|
6
|
+
export declare function EmailsTab({ siteId }: {
|
|
7
|
+
siteId: string;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { doc, getDoc, setDoc } from "firebase/firestore";
|
|
5
|
+
import { getDb } from "../../firebase/client";
|
|
6
|
+
import { MAIL_TEMPLATE_KEYS } from "../../schema/shop";
|
|
7
|
+
import { Button, Card, Field, Input, Textarea } from "../ui/primitives";
|
|
8
|
+
import { useToast } from "../ui/toast";
|
|
9
|
+
import { shopCallable } from "./lib";
|
|
10
|
+
const TEMPLATE_INFO = {
|
|
11
|
+
orderConfirmation: { label: "Bestelbevestiging", hint: "Naar de klant zodra de betaling binnen is." },
|
|
12
|
+
orderShipped: { label: "Bestelling verzonden", hint: "Naar de klant wanneer je de status op \u2018verzonden\u2019 zet." },
|
|
13
|
+
orderCancelled: { label: "Bestelling geannuleerd", hint: "Naar de klant bij annulering." },
|
|
14
|
+
orderRefunded: { label: "Terugbetaald", hint: "Naar de klant na een terugbetaling." },
|
|
15
|
+
donationThanks: { label: "Donatie-bedankje", hint: "Naar de gever na een donatie." },
|
|
16
|
+
formConfirmation: { label: "Formulier-bevestiging", hint: "Ontvangstbevestiging van formulieren (site-breed)." }
|
|
17
|
+
};
|
|
18
|
+
const PLACEHOLDERS = "{{naam}} {{ordernummer}} {{regels}} {{totaal}} {{verzendadres}} {{trackingUrl}} {{notitie}} {{sitenaam}} {{formulier}}";
|
|
19
|
+
function EmailsTab({ siteId }) {
|
|
20
|
+
const { toast } = useToast();
|
|
21
|
+
const [templates, setTemplates] = useState(null);
|
|
22
|
+
const [busy, setBusy] = useState(null);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const db = getDb();
|
|
25
|
+
if (!db) return;
|
|
26
|
+
void getDoc(doc(db, "sites", siteId, "settings", "mailTemplates")).then(
|
|
27
|
+
(snap) => setTemplates(snap.data() ?? {})
|
|
28
|
+
);
|
|
29
|
+
}, [siteId]);
|
|
30
|
+
async function save() {
|
|
31
|
+
const db = getDb();
|
|
32
|
+
if (!db || !templates) return;
|
|
33
|
+
setBusy("save");
|
|
34
|
+
try {
|
|
35
|
+
await setDoc(doc(db, "sites", siteId, "settings", "mailTemplates"), templates);
|
|
36
|
+
toast({ title: "Templates opgeslagen", tone: "success" });
|
|
37
|
+
} catch (e) {
|
|
38
|
+
toast({ title: "Opslaan mislukt", description: e instanceof Error ? e.message : void 0, tone: "error" });
|
|
39
|
+
} finally {
|
|
40
|
+
setBusy(null);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function preview(key) {
|
|
44
|
+
setBusy(key);
|
|
45
|
+
try {
|
|
46
|
+
const res = await shopCallable("sendTemplatePreview")({
|
|
47
|
+
siteId,
|
|
48
|
+
template: key
|
|
49
|
+
});
|
|
50
|
+
toast({ title: "Voorbeeld verstuurd", description: `Naar ${res.to} (met voorbeeldgegevens).`, tone: "success" });
|
|
51
|
+
} catch (e) {
|
|
52
|
+
toast({ title: "Voorbeeld mislukt", description: e instanceof Error ? e.message : void 0, tone: "error" });
|
|
53
|
+
} finally {
|
|
54
|
+
setBusy(null);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!templates) return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Laden\u2026" }) });
|
|
58
|
+
return /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
|
|
59
|
+
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs("p", { className: "text-sm text-slate-600", children: [
|
|
60
|
+
"Laat een veld leeg om de standaardtekst te gebruiken. Beschikbare invulvelden:",
|
|
61
|
+
" ",
|
|
62
|
+
/* @__PURE__ */ jsx("code", { className: "rounded bg-slate-100 px-1 py-0.5 text-xs", children: PLACEHOLDERS })
|
|
63
|
+
] }) }),
|
|
64
|
+
MAIL_TEMPLATE_KEYS.map((key) => {
|
|
65
|
+
const info = TEMPLATE_INFO[key];
|
|
66
|
+
const tpl = templates[key] ?? {};
|
|
67
|
+
const set = (patch) => setTemplates((p) => ({ ...p, [key]: { ...p?.[key], ...patch } }));
|
|
68
|
+
return /* @__PURE__ */ jsxs(Card, { children: [
|
|
69
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between gap-2", children: [
|
|
70
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
71
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: info.label }),
|
|
72
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-slate-500", children: info.hint })
|
|
73
|
+
] }),
|
|
74
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", disabled: busy !== null, onClick: () => preview(key), children: busy === key ? "Versturen\u2026" : "Stuur voorbeeld naar mij" })
|
|
75
|
+
] }),
|
|
76
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
|
|
77
|
+
/* @__PURE__ */ jsx(Field, { label: "Onderwerp", hint: "Leeg = standaard", children: /* @__PURE__ */ jsx(Input, { value: tpl.subject ?? "", onChange: (e) => set({ subject: e.target.value }) }) }),
|
|
78
|
+
/* @__PURE__ */ jsx(Field, { label: "Tekst", hint: "Leeg = standaard", children: /* @__PURE__ */ jsx(Textarea, { rows: 5, value: tpl.bodyText ?? "", onChange: (e) => set({ bodyText: e.target.value }) }) })
|
|
79
|
+
] })
|
|
80
|
+
] }, key);
|
|
81
|
+
}),
|
|
82
|
+
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, { onClick: save, disabled: busy !== null, children: "Alles opslaan" }) })
|
|
83
|
+
] });
|
|
84
|
+
}
|
|
85
|
+
export {
|
|
86
|
+
EmailsTab
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=EmailsTab.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/shop/EmailsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { doc, getDoc, setDoc } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport { MAIL_TEMPLATE_KEYS, type MailTemplateKey, type MailTemplates } from \"../../schema/shop\";\nimport { Button, Card, Field, Input, Textarea } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { shopCallable } from \"./lib\";\n\nconst TEMPLATE_INFO: Record<MailTemplateKey, { label: string; hint: string }> = {\n orderConfirmation: { label: \"Bestelbevestiging\", hint: \"Naar de klant zodra de betaling binnen is.\" },\n orderShipped: { label: \"Bestelling verzonden\", hint: \"Naar de klant wanneer je de status op ‘verzonden’ zet.\" },\n orderCancelled: { label: \"Bestelling geannuleerd\", hint: \"Naar de klant bij annulering.\" },\n orderRefunded: { label: \"Terugbetaald\", hint: \"Naar de klant na een terugbetaling.\" },\n donationThanks: { label: \"Donatie-bedankje\", hint: \"Naar de gever na een donatie.\" },\n formConfirmation: { label: \"Formulier-bevestiging\", hint: \"Ontvangstbevestiging van formulieren (site-breed).\" },\n};\n\nconst PLACEHOLDERS =\n \"{{naam}} {{ordernummer}} {{regels}} {{totaal}} {{verzendadres}} {{trackingUrl}} {{notitie}} {{sitenaam}} {{formulier}}\";\n\n/**\n * E-mails — bewerkbare templates in `settings/mailTemplates` (siteadmin). Leeg veld = de\n * ingebouwde standaardtekst; er gaat dus altijd een correcte mail uit. Platte tekst met\n * placeholders; de opmaak (afzender, huisstijl) regelt de engine.\n */\nexport function EmailsTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [templates, setTemplates] = useState<MailTemplates | null>(null);\n const [busy, setBusy] = useState<string | null>(null);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n void getDoc(doc(db, \"sites\", siteId, \"settings\", \"mailTemplates\")).then((snap) =>\n setTemplates((snap.data() as MailTemplates | undefined) ?? {}),\n );\n }, [siteId]);\n\n async function save() {\n const db = getDb();\n if (!db || !templates) return;\n setBusy(\"save\");\n try {\n await setDoc(doc(db, \"sites\", siteId, \"settings\", \"mailTemplates\"), templates);\n toast({ title: \"Templates opgeslagen\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(null);\n }\n }\n\n async function preview(key: MailTemplateKey) {\n setBusy(key);\n try {\n const res = await shopCallable<{ siteId: string; template: MailTemplateKey }, { to: string }>(\"sendTemplatePreview\")({\n siteId,\n template: key,\n });\n toast({ title: \"Voorbeeld verstuurd\", description: `Naar ${res.to} (met voorbeeldgegevens).`, tone: \"success\" });\n } catch (e) {\n toast({ title: \"Voorbeeld mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(null);\n }\n }\n\n if (!templates) return <Card><p className=\"text-sm text-slate-500\">Laden…</p></Card>;\n\n return (\n <div className=\"grid gap-4\">\n <Card>\n <p className=\"text-sm text-slate-600\">\n Laat een veld leeg om de standaardtekst te gebruiken. Beschikbare invulvelden:{\" \"}\n <code className=\"rounded bg-slate-100 px-1 py-0.5 text-xs\">{PLACEHOLDERS}</code>\n </p>\n </Card>\n\n {MAIL_TEMPLATE_KEYS.map((key) => {\n const info = TEMPLATE_INFO[key];\n const tpl = templates[key] ?? {};\n const set = (patch: { subject?: string; bodyText?: string }) =>\n setTemplates((p) => ({ ...p, [key]: { ...p?.[key], ...patch } }));\n return (\n <Card key={key}>\n <div className=\"mb-2 flex items-center justify-between gap-2\">\n <div>\n <h3 className=\"text-sm font-semibold\">{info.label}</h3>\n <p className=\"text-xs text-slate-500\">{info.hint}</p>\n </div>\n <Button size=\"sm\" variant=\"secondary\" disabled={busy !== null} onClick={() => preview(key)}>\n {busy === key ? \"Versturen…\" : \"Stuur voorbeeld naar mij\"}\n </Button>\n </div>\n <div className=\"grid gap-3\">\n <Field label=\"Onderwerp\" hint=\"Leeg = standaard\">\n <Input value={tpl.subject ?? \"\"} onChange={(e) => set({ subject: e.target.value })} />\n </Field>\n <Field label=\"Tekst\" hint=\"Leeg = standaard\">\n <Textarea rows={5} value={tpl.bodyText ?? \"\"} onChange={(e) => set({ bodyText: e.target.value })} />\n </Field>\n </div>\n </Card>\n );\n })}\n\n <div>\n <Button onClick={save} disabled={busy !== null}>Alles opslaan</Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|