@xiwebdesign/xicms-core 5.3.0 → 5.5.0

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.
Files changed (135) hide show
  1. package/dist/XiCMSProvider.d.ts +11 -2
  2. package/dist/XiCMSProvider.js +10 -5
  3. package/dist/XiCMSProvider.js.map +1 -1
  4. package/dist/admin/blockEditors/GenericEditor.js +18 -8
  5. package/dist/admin/blockEditors/GenericEditor.js.map +1 -1
  6. package/dist/admin/customBlocks/CustomBlocksPanel.js +12 -2
  7. package/dist/admin/customBlocks/CustomBlocksPanel.js.map +1 -1
  8. package/dist/admin/lib/pagesClient.js +4 -1
  9. package/dist/admin/lib/pagesClient.js.map +1 -1
  10. package/dist/admin/lib/upload.js +14 -7
  11. package/dist/admin/lib/upload.js.map +1 -1
  12. package/dist/admin/media/MediaManager.js +60 -28
  13. package/dist/admin/media/MediaManager.js.map +1 -1
  14. package/dist/admin/media/MediaPicker.js +25 -15
  15. package/dist/admin/media/MediaPicker.js.map +1 -1
  16. package/dist/admin/pages/PageEditor.d.ts +2 -1
  17. package/dist/admin/pages/PageEditor.js +16 -2
  18. package/dist/admin/pages/PageEditor.js.map +1 -1
  19. package/dist/admin/pages/PagesPanel.js +9 -5
  20. package/dist/admin/pages/PagesPanel.js.map +1 -1
  21. package/dist/admin/panelRegistry.js +3 -0
  22. package/dist/admin/panelRegistry.js.map +1 -1
  23. package/dist/admin/shop/CategoriesTab.d.ts +3 -0
  24. package/dist/admin/shop/CategoriesTab.js +140 -0
  25. package/dist/admin/shop/CategoriesTab.js.map +1 -0
  26. package/dist/admin/shop/ConnectionsTab.d.ts +9 -0
  27. package/dist/admin/shop/ConnectionsTab.js +158 -0
  28. package/dist/admin/shop/ConnectionsTab.js.map +1 -0
  29. package/dist/admin/shop/OrdersTab.d.ts +3 -0
  30. package/dist/admin/shop/OrdersTab.js +236 -0
  31. package/dist/admin/shop/OrdersTab.js.map +1 -0
  32. package/dist/admin/shop/ProductsTab.d.ts +3 -0
  33. package/dist/admin/shop/ProductsTab.js +397 -0
  34. package/dist/admin/shop/ProductsTab.js.map +1 -0
  35. package/dist/admin/shop/ShopPanel.d.ts +4 -0
  36. package/dist/admin/shop/ShopPanel.js +25 -0
  37. package/dist/admin/shop/ShopPanel.js.map +1 -0
  38. package/dist/admin/shop/lib.d.ts +16 -0
  39. package/dist/admin/shop/lib.js +46 -0
  40. package/dist/admin/shop/lib.js.map +1 -0
  41. package/dist/admin/visual/VisualEditor.js +11 -2
  42. package/dist/admin/visual/VisualEditor.js.map +1 -1
  43. package/dist/admin.css +1 -1
  44. package/dist/agentFiles/files.d.ts +15 -3
  45. package/dist/agentFiles/files.js +57 -0
  46. package/dist/agentFiles/files.js.map +1 -1
  47. package/dist/auth/AuthProvider.js +3 -3
  48. package/dist/auth/AuthProvider.js.map +1 -1
  49. package/dist/blocks/builtins/Columns.js +1 -1
  50. package/dist/blocks/builtins/Columns.js.map +1 -1
  51. package/dist/blocks/builtins/Gallery.js +2 -2
  52. package/dist/blocks/builtins/Gallery.js.map +1 -1
  53. package/dist/blocks/builtins/Hero.js +1 -1
  54. package/dist/blocks/builtins/Hero.js.map +1 -1
  55. package/dist/blocks/builtins/SymbolRef.js.map +1 -1
  56. package/dist/components/PageView.js +4 -3
  57. package/dist/components/PageView.js.map +1 -1
  58. package/dist/demo/index.d.ts +7 -0
  59. package/dist/demo/index.js +23 -0
  60. package/dist/demo/index.js.map +1 -0
  61. package/dist/index.d.ts +2 -4
  62. package/dist/index.js +0 -20
  63. package/dist/index.js.map +1 -1
  64. package/dist/modules/builtins.d.ts +1 -0
  65. package/dist/modules/builtins.js +4 -0
  66. package/dist/modules/builtins.js.map +1 -1
  67. package/dist/modules/index.d.ts +4 -0
  68. package/dist/modules/index.js +12 -0
  69. package/dist/modules/index.js.map +1 -1
  70. package/dist/modules/registry.d.ts +3 -4
  71. package/dist/modules/registry.js +2 -3
  72. package/dist/modules/registry.js.map +1 -1
  73. package/dist/modules/shop/blocks/Cart.d.ts +2 -0
  74. package/dist/modules/shop/blocks/Cart.js +91 -0
  75. package/dist/modules/shop/blocks/Cart.js.map +1 -0
  76. package/dist/modules/shop/blocks/Checkout.d.ts +2 -0
  77. package/dist/modules/shop/blocks/Checkout.js +126 -0
  78. package/dist/modules/shop/blocks/Checkout.js.map +1 -0
  79. package/dist/modules/shop/blocks/Donation.d.ts +2 -0
  80. package/dist/modules/shop/blocks/Donation.js +145 -0
  81. package/dist/modules/shop/blocks/Donation.js.map +1 -0
  82. package/dist/modules/shop/blocks/OrderStatus.d.ts +2 -0
  83. package/dist/modules/shop/blocks/OrderStatus.js +40 -0
  84. package/dist/modules/shop/blocks/OrderStatus.js.map +1 -0
  85. package/dist/modules/shop/blocks/ProductDetail.d.ts +2 -0
  86. package/dist/modules/shop/blocks/ProductDetail.js +168 -0
  87. package/dist/modules/shop/blocks/ProductDetail.js.map +1 -0
  88. package/dist/modules/shop/blocks/ProductGrid.d.ts +8 -0
  89. package/dist/modules/shop/blocks/ProductGrid.js +136 -0
  90. package/dist/modules/shop/blocks/ProductGrid.js.map +1 -0
  91. package/dist/modules/shop/blocks/ShopCategories.d.ts +2 -0
  92. package/dist/modules/shop/blocks/ShopCategories.js +69 -0
  93. package/dist/modules/shop/blocks/ShopCategories.js.map +1 -0
  94. package/dist/modules/shop/cartStore.d.ts +22 -0
  95. package/dist/modules/shop/cartStore.js +85 -0
  96. package/dist/modules/shop/cartStore.js.map +1 -0
  97. package/dist/modules/shop/lib.d.ts +25 -0
  98. package/dist/modules/shop/lib.js +61 -0
  99. package/dist/modules/shop/lib.js.map +1 -0
  100. package/dist/modules/shop/module.d.ts +1 -0
  101. package/dist/modules/shop/module.js +39 -0
  102. package/dist/modules/shop/module.js.map +1 -0
  103. package/dist/modules/shop/settings.d.ts +41 -0
  104. package/dist/modules/shop/settings.js +18 -0
  105. package/dist/modules/shop/settings.js.map +1 -0
  106. package/dist/schema/blocks.d.ts +27 -12
  107. package/dist/schema/blocks.js +3 -0
  108. package/dist/schema/blocks.js.map +1 -1
  109. package/dist/schema/index.d.ts +1 -0
  110. package/dist/schema/index.js +1 -0
  111. package/dist/schema/index.js.map +1 -1
  112. package/dist/schema/settings.d.ts +1 -34
  113. package/dist/schema/settings.js +2 -8
  114. package/dist/schema/settings.js.map +1 -1
  115. package/dist/schema/shop.d.ts +939 -0
  116. package/dist/schema/shop.js +209 -0
  117. package/dist/schema/shop.js.map +1 -0
  118. package/dist/schema/siteConfig.d.ts +3 -71
  119. package/dist/schema/siteConfig.js +3 -8
  120. package/dist/schema/siteConfig.js.map +1 -1
  121. package/dist/schema/symbols.d.ts +2 -2
  122. package/dist/schema/symbols.js.map +1 -1
  123. package/dist/server/index.d.ts +1 -1
  124. package/dist/server/index.js +2 -0
  125. package/dist/server/index.js.map +1 -1
  126. package/dist/server/pages.d.ts +23 -3
  127. package/dist/server/pages.js +37 -3
  128. package/dist/server/pages.js.map +1 -1
  129. package/dist/server/seo.js +5 -3
  130. package/dist/server/seo.js.map +1 -1
  131. package/dist/settings/useSiteSettings.d.ts +15 -3
  132. package/dist/settings/useSiteSettings.js +26 -5
  133. package/dist/settings/useSiteSettings.js.map +1 -1
  134. package/dist/styles.css +14 -0
  135. package/package.json +6 -2
@@ -0,0 +1,140 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import { addDoc, collection, deleteDoc, doc, onSnapshot, setDoc } from "firebase/firestore";
5
+ import { getDb } from "../../firebase/client";
6
+ import { ShopCategorySchema } from "../../schema/shop";
7
+ import { Button, Card, Field, Input, Textarea } from "../ui/primitives";
8
+ import { Switch } from "../ui/switch";
9
+ import { useToast } from "../ui/toast";
10
+ import { useConfirm } from "../ui/ConfirmDialog";
11
+ import { MediaPickerButton } from "../media/MediaPicker";
12
+ import { slugify } from "../lib/slugify";
13
+ function CategoriesTab({ siteId }) {
14
+ const { toast } = useToast();
15
+ const confirm = useConfirm();
16
+ const [cats, setCats] = 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, "shopCategories"),
23
+ (snap) => setCats(
24
+ snap.docs.map((d) => ({ id: d.id, ...d.data() })).sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
25
+ )
26
+ );
27
+ }, [siteId]);
28
+ async function createCategory() {
29
+ const db = getDb();
30
+ if (!db) return;
31
+ const ref = await addDoc(collection(db, "sites", siteId, "shopCategories"), {
32
+ name: "Nieuwe categorie",
33
+ slug: `categorie-${cats.length + 1}`,
34
+ order: cats.length,
35
+ visible: true
36
+ });
37
+ setSelected(ref.id);
38
+ }
39
+ const current = cats.find((c) => c.id === selected) ?? null;
40
+ return /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-[260px_1fr]", children: [
41
+ /* @__PURE__ */ jsxs(Card, { className: "h-fit", children: [
42
+ /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between", children: [
43
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "Categorie\xEBn" }),
44
+ /* @__PURE__ */ jsx(Button, { size: "sm", onClick: createCategory, children: "+ Nieuw" })
45
+ ] }),
46
+ /* @__PURE__ */ jsxs("ul", { className: "grid gap-1", children: [
47
+ cats.map((c) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
48
+ "button",
49
+ {
50
+ onClick: () => setSelected(c.id),
51
+ className: `w-full truncate rounded-lg px-3 py-2 text-left text-sm ${c.id === selected ? "bg-brand-50 text-brand-700" : "hover:bg-slate-100"}`,
52
+ children: [
53
+ c.name,
54
+ c.visible === false && /* @__PURE__ */ jsx("span", { className: "ml-1 text-xs text-slate-400", children: "(verborgen)" })
55
+ ]
56
+ }
57
+ ) }, c.id)),
58
+ cats.length === 0 && /* @__PURE__ */ jsx("li", { className: "px-3 py-2 text-sm text-slate-500", children: "Nog geen categorie\xEBn." })
59
+ ] })
60
+ ] }),
61
+ current ? /* @__PURE__ */ jsx(
62
+ CategoryDetail,
63
+ {
64
+ siteId,
65
+ category: current,
66
+ onDeleted: () => setSelected(null),
67
+ onToast: toast,
68
+ confirm
69
+ },
70
+ current.id
71
+ ) : /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Selecteer of maak een categorie." }) })
72
+ ] });
73
+ }
74
+ function CategoryDetail({
75
+ siteId,
76
+ category,
77
+ onDeleted,
78
+ onToast,
79
+ confirm
80
+ }) {
81
+ const [def, setDef] = useState(category);
82
+ async function save() {
83
+ const db = getDb();
84
+ if (!db) return;
85
+ const parsed = ShopCategorySchema.safeParse(def);
86
+ if (!parsed.success) {
87
+ onToast({ title: "Ongeldige categorie", description: parsed.error.issues[0]?.message, tone: "error" });
88
+ return;
89
+ }
90
+ await setDoc(doc(db, "sites", siteId, "shopCategories", category.id), parsed.data);
91
+ onToast({ title: "Categorie opgeslagen", tone: "success" });
92
+ }
93
+ async function remove() {
94
+ const db = getDb();
95
+ if (!db) return;
96
+ const ok = await confirm({
97
+ title: `"${category.name}" verwijderen?`,
98
+ description: "Producten in deze categorie blijven bestaan (alleen de koppeling vervalt)."
99
+ });
100
+ if (!ok) return;
101
+ await deleteDoc(doc(db, "sites", siteId, "shopCategories", category.id));
102
+ onDeleted();
103
+ }
104
+ return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
105
+ /* @__PURE__ */ jsx(Field, { label: "Naam", children: /* @__PURE__ */ jsx(
106
+ Input,
107
+ {
108
+ value: def.name,
109
+ onChange: (e) => setDef((p) => ({ ...p, name: e.target.value, slug: slugify(e.target.value) || p.slug }))
110
+ }
111
+ ) }),
112
+ /* @__PURE__ */ jsx(Field, { label: "Slug", hint: "Wordt gebruikt in de winkel-URL (?cat=\u2026)", children: /* @__PURE__ */ jsx(Input, { value: def.slug, onChange: (e) => setDef((p) => ({ ...p, slug: slugify(e.target.value) })) }) }),
113
+ /* @__PURE__ */ jsx(Field, { label: "Omschrijving (optioneel)", children: /* @__PURE__ */ jsx(Textarea, { rows: 2, value: def.description ?? "", onChange: (e) => setDef((p) => ({ ...p, description: e.target.value })) }) }),
114
+ /* @__PURE__ */ jsx(Field, { label: "Afbeelding", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
115
+ def.imageUrl && /* @__PURE__ */ jsx("img", { src: def.imageUrl, alt: "", className: "h-14 w-14 rounded-lg object-cover" }),
116
+ /* @__PURE__ */ jsx(MediaPickerButton, { siteId, onSelect: (m) => setDef((p) => ({ ...p, imageUrl: m.url })) }),
117
+ def.imageUrl && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: () => setDef((p) => ({ ...p, imageUrl: void 0 })), children: "Verwijder" })
118
+ ] }) }),
119
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-6", children: [
120
+ /* @__PURE__ */ jsx(Field, { label: "Volgorde", children: /* @__PURE__ */ jsx(
121
+ Input,
122
+ {
123
+ type: "number",
124
+ className: "w-24",
125
+ value: def.order ?? 0,
126
+ onChange: (e) => setDef((p) => ({ ...p, order: Math.max(0, Number(e.target.value) || 0) }))
127
+ }
128
+ ) }),
129
+ /* @__PURE__ */ jsx(Switch, { checked: def.visible !== false, onCheckedChange: (v) => setDef((p) => ({ ...p, visible: v })), label: "Zichtbaar in de winkel" })
130
+ ] }),
131
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
132
+ /* @__PURE__ */ jsx(Button, { onClick: save, children: "Opslaan" }),
133
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: remove, children: "Verwijderen" })
134
+ ] })
135
+ ] }) });
136
+ }
137
+ export {
138
+ CategoriesTab
139
+ };
140
+ //# sourceMappingURL=CategoriesTab.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/shop/CategoriesTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { addDoc, collection, deleteDoc, doc, onSnapshot, setDoc } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport { ShopCategorySchema, type ShopCategory } from \"../../schema/shop\";\nimport { Button, Card, Field, Input, Textarea } from \"../ui/primitives\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { MediaPickerButton } from \"../media/MediaPicker\";\nimport { slugify } from \"../lib/slugify\";\n\ntype StoredCategory = ShopCategory & { id: string };\n\nexport function CategoriesTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [cats, setCats] = useState<StoredCategory[]>([]);\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, \"shopCategories\"), (snap) =>\n setCats(\n snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as ShopCategory) }))\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),\n ),\n );\n }, [siteId]);\n\n async function createCategory() {\n const db = getDb();\n if (!db) return;\n const ref = await addDoc(collection(db, \"sites\", siteId, \"shopCategories\"), {\n name: \"Nieuwe categorie\",\n slug: `categorie-${cats.length + 1}`,\n order: cats.length,\n visible: true,\n } satisfies ShopCategory);\n setSelected(ref.id);\n }\n\n const current = cats.find((c) => c.id === selected) ?? null;\n\n return (\n <div className=\"grid gap-4 md:grid-cols-[260px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">Categorieën</h3>\n <Button size=\"sm\" onClick={createCategory}>+ Nieuw</Button>\n </div>\n <ul className=\"grid gap-1\">\n {cats.map((c) => (\n <li key={c.id}>\n <button\n onClick={() => setSelected(c.id)}\n className={`w-full truncate 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 {c.name}\n {c.visible === false && <span className=\"ml-1 text-xs text-slate-400\">(verborgen)</span>}\n </button>\n </li>\n ))}\n {cats.length === 0 && <li className=\"px-3 py-2 text-sm text-slate-500\">Nog geen categorieën.</li>}\n </ul>\n </Card>\n {current ? (\n <CategoryDetail\n key={current.id}\n siteId={siteId}\n category={current}\n onDeleted={() => setSelected(null)}\n onToast={toast}\n confirm={confirm}\n />\n ) : (\n <Card>\n <p className=\"text-sm text-slate-500\">Selecteer of maak een categorie.</p>\n </Card>\n )}\n </div>\n );\n}\n\nfunction CategoryDetail({\n siteId,\n category,\n onDeleted,\n onToast,\n confirm,\n}: {\n siteId: string;\n category: StoredCategory;\n onDeleted: () => void;\n onToast: (t: { title: string; description?: string; tone?: \"default\" | \"success\" | \"error\" }) => void;\n confirm: (opts: { title: string; description?: string }) => Promise<boolean>;\n}) {\n const [def, setDef] = useState<ShopCategory>(category);\n\n async function save() {\n const db = getDb();\n if (!db) return;\n const parsed = ShopCategorySchema.safeParse(def);\n if (!parsed.success) {\n onToast({ title: \"Ongeldige categorie\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n await setDoc(doc(db, \"sites\", siteId, \"shopCategories\", category.id), parsed.data);\n onToast({ title: \"Categorie opgeslagen\", tone: \"success\" });\n }\n\n async function remove() {\n const db = getDb();\n if (!db) return;\n const ok = await confirm({\n title: `\"${category.name}\" verwijderen?`,\n description: \"Producten in deze categorie blijven bestaan (alleen de koppeling vervalt).\",\n });\n if (!ok) return;\n await deleteDoc(doc(db, \"sites\", siteId, \"shopCategories\", category.id));\n onDeleted();\n }\n\n return (\n <Card>\n <div className=\"grid gap-4\">\n <Field label=\"Naam\">\n <Input\n value={def.name}\n onChange={(e) => setDef((p) => ({ ...p, name: e.target.value, slug: slugify(e.target.value) || p.slug }))}\n />\n </Field>\n <Field label=\"Slug\" hint=\"Wordt gebruikt in de winkel-URL (?cat=…)\">\n <Input value={def.slug} onChange={(e) => setDef((p) => ({ ...p, slug: slugify(e.target.value) }))} />\n </Field>\n <Field label=\"Omschrijving (optioneel)\">\n <Textarea rows={2} value={def.description ?? \"\"} onChange={(e) => setDef((p) => ({ ...p, description: e.target.value }))} />\n </Field>\n <Field label=\"Afbeelding\">\n <div className=\"flex items-center gap-3\">\n {def.imageUrl && <img src={def.imageUrl} alt=\"\" className=\"h-14 w-14 rounded-lg object-cover\" />}\n <MediaPickerButton siteId={siteId} onSelect={(m) => setDef((p) => ({ ...p, imageUrl: m.url }))} />\n {def.imageUrl && (\n <Button size=\"sm\" variant=\"ghost\" onClick={() => setDef((p) => ({ ...p, imageUrl: undefined }))}>\n Verwijder\n </Button>\n )}\n </div>\n </Field>\n <div className=\"flex items-center gap-6\">\n <Field label=\"Volgorde\">\n <Input\n type=\"number\"\n className=\"w-24\"\n value={def.order ?? 0}\n onChange={(e) => setDef((p) => ({ ...p, order: Math.max(0, Number(e.target.value) || 0) }))}\n />\n </Field>\n <Switch checked={def.visible !== false} onCheckedChange={(v) => setDef((p) => ({ ...p, visible: v }))} label=\"Zichtbaar in de winkel\" />\n </div>\n <div className=\"flex items-center justify-between\">\n <Button onClick={save}>Opslaan</Button>\n <Button variant=\"ghost\" onClick={remove}>Verwijderen</Button>\n </div>\n </div>\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Koppelingen — Stripe (eigen account van de klant) en e-mail (eigen SMTP, bv. Gmail of de
3
+ * mailbox bij de hostingpartij). Geheimen gaan uitsluitend via callables naar
4
+ * `sites/{siteId}/private/shop` (deny-all rules); dit paneel toont alleen gemaskeerde status.
5
+ * Alleen een siteadmin kan opslaan (de callables dwingen dat af).
6
+ */
7
+ export declare function ConnectionsTab({ siteId }: {
8
+ siteId: string;
9
+ }): import("react").JSX.Element;
@@ -0,0 +1,158 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useState } from "react";
4
+ import { Badge, Button, Card, Field, Input } from "../ui/primitives";
5
+ import { Switch } from "../ui/switch";
6
+ import { useToast } from "../ui/toast";
7
+ import { shopCallable } from "./lib";
8
+ function ConnectionsTab({ siteId }) {
9
+ const { toast } = useToast();
10
+ const [status, setStatus] = useState(null);
11
+ const [busy, setBusy] = useState(false);
12
+ const [stripeKey, setStripeKey] = useState("");
13
+ const [smtp, setSmtp] = useState({ host: "", port: "465", secure: true, user: "", pass: "", fromName: "", fromEmail: "", notifyTo: "" });
14
+ const refresh = useCallback(async () => {
15
+ try {
16
+ const s = await shopCallable("getShopStatus")({ siteId });
17
+ setStatus(s);
18
+ setSmtp((p) => ({
19
+ ...p,
20
+ host: s.smtp.host ?? p.host,
21
+ port: String(s.smtp.port ?? 465),
22
+ user: s.smtp.user ?? p.user,
23
+ fromName: s.smtp.fromName ?? p.fromName,
24
+ fromEmail: s.smtp.fromEmail ?? p.fromEmail,
25
+ notifyTo: s.smtp.notifyTo ?? p.notifyTo
26
+ }));
27
+ } catch {
28
+ setStatus(null);
29
+ }
30
+ }, [siteId]);
31
+ useEffect(() => {
32
+ void refresh();
33
+ }, [refresh]);
34
+ async function saveStripe() {
35
+ if (!stripeKey.trim().startsWith("rk_") && !stripeKey.trim().startsWith("sk_")) {
36
+ toast({ title: "Ongeldige sleutel", description: "Plak een geheime sleutel (sk_\u2026) of restricted key (rk_\u2026).", tone: "error" });
37
+ return;
38
+ }
39
+ setBusy(true);
40
+ try {
41
+ const res = await shopCallable("saveShopSecrets")({
42
+ siteId,
43
+ stripe: { secretKey: stripeKey.trim() }
44
+ });
45
+ toast({ title: "Stripe gekoppeld", description: res.account ? `Account: ${res.account}` : void 0, tone: "success" });
46
+ setStripeKey("");
47
+ await refresh();
48
+ } catch (e) {
49
+ toast({ title: "Stripe koppelen mislukt", description: e instanceof Error ? e.message : void 0, tone: "error" });
50
+ } finally {
51
+ setBusy(false);
52
+ }
53
+ }
54
+ async function saveSmtp() {
55
+ setBusy(true);
56
+ try {
57
+ await shopCallable("saveShopSecrets")({
58
+ siteId,
59
+ smtp: {
60
+ host: smtp.host.trim(),
61
+ port: Number(smtp.port) || 465,
62
+ secure: smtp.secure,
63
+ user: smtp.user.trim(),
64
+ // Leeg laten = bestaand wachtwoord behouden.
65
+ ...smtp.pass ? { pass: smtp.pass } : {},
66
+ fromName: smtp.fromName.trim(),
67
+ fromEmail: smtp.fromEmail.trim(),
68
+ notifyTo: smtp.notifyTo.trim()
69
+ }
70
+ });
71
+ toast({ title: "E-mailinstellingen opgeslagen", tone: "success" });
72
+ setSmtp((p) => ({ ...p, pass: "" }));
73
+ await refresh();
74
+ } catch (e) {
75
+ toast({ title: "Opslaan mislukt", description: e instanceof Error ? e.message : void 0, tone: "error" });
76
+ } finally {
77
+ setBusy(false);
78
+ }
79
+ }
80
+ async function sendTest() {
81
+ setBusy(true);
82
+ try {
83
+ const res = await shopCallable("sendShopTestMail")({ siteId });
84
+ toast({ title: "Testmail verstuurd", description: `Naar ${res.to} \u2014 check de inbox (en spam).`, tone: "success" });
85
+ } catch (e) {
86
+ toast({ title: "Testmail mislukt", description: e instanceof Error ? e.message : void 0, tone: "error" });
87
+ } finally {
88
+ setBusy(false);
89
+ }
90
+ }
91
+ return /* @__PURE__ */ jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [
92
+ /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
93
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
94
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "Stripe (betalingen)" }),
95
+ status && /* @__PURE__ */ jsx(Badge, { tone: status.stripe.configured ? "green" : "amber", children: status.stripe.configured ? `Gekoppeld${status.stripe.account ? ` \u2014 ${status.stripe.account}` : ""}` : "Nog niet gekoppeld" })
96
+ ] }),
97
+ /* @__PURE__ */ jsxs("ol", { className: "grid list-decimal gap-1 pl-5 text-sm text-slate-600", children: [
98
+ /* @__PURE__ */ jsxs("li", { children: [
99
+ "Maak een gratis account op ",
100
+ /* @__PURE__ */ jsx("a", { className: "underline", href: "https://dashboard.stripe.com/register", target: "_blank", rel: "noreferrer", children: "stripe.com" }),
101
+ " en doorloop de activatie (IBAN, KVK)."
102
+ ] }),
103
+ /* @__PURE__ */ jsxs("li", { children: [
104
+ "Zet in Stripe onder ",
105
+ /* @__PURE__ */ jsx("em", { children: "Settings \u2192 Payment methods" }),
106
+ " iDEAL aan."
107
+ ] }),
108
+ /* @__PURE__ */ jsxs("li", { children: [
109
+ "Maak onder ",
110
+ /* @__PURE__ */ jsx("em", { children: "Developers \u2192 API keys" }),
111
+ " een sleutel aan en plak hem hieronder. De webhook wordt automatisch voor je aangemaakt."
112
+ ] })
113
+ ] }),
114
+ /* @__PURE__ */ jsx(Field, { label: "Geheime sleutel (sk_live_\u2026 of rk_live_\u2026)", hint: "Wordt veilig opgeslagen; nooit zichtbaar in de browser", children: /* @__PURE__ */ jsx(Input, { type: "password", value: stripeKey, onChange: (e) => setStripeKey(e.target.value), placeholder: "sk_live_\u2026" }) }),
115
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, { onClick: saveStripe, disabled: busy || !stripeKey.trim(), children: "Koppel Stripe" }) }),
116
+ status?.stripe.configured && !status.stripe.webhookConfigured && /* @__PURE__ */ jsx("p", { className: "rounded-lg bg-amber-50 px-3 py-2 text-sm text-amber-700", children: "Webhook nog niet geregistreerd \u2014 koppel opnieuw met een sleutel die webhook-rechten heeft." })
117
+ ] }) }),
118
+ /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
119
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
120
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "E-mail (bestelbevestigingen)" }),
121
+ status && /* @__PURE__ */ jsx(Badge, { tone: status.smtp.configured ? "green" : "amber", children: status.smtp.configured ? "Ingesteld" : "Nog niet ingesteld" })
122
+ ] }),
123
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-slate-600", children: [
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
+ /* @__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."
127
+ ] }),
128
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-[1fr_110px]", children: [
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" }) }),
130
+ /* @__PURE__ */ jsx(Field, { label: "Poort", children: /* @__PURE__ */ jsx(Input, { value: smtp.port, onChange: (e) => setSmtp((p) => ({ ...p, port: e.target.value })), placeholder: "465" }) })
131
+ ] }),
132
+ /* @__PURE__ */ jsx(Switch, { checked: smtp.secure, onCheckedChange: (v) => setSmtp((p) => ({ ...p, secure: v })), label: "SSL (poort 465). Uit = STARTTLS (poort 587)" }),
133
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [
134
+ /* @__PURE__ */ jsx(Field, { label: "Gebruikersnaam", children: /* @__PURE__ */ jsx(Input, { value: smtp.user, onChange: (e) => setSmtp((p) => ({ ...p, user: e.target.value })), placeholder: "info@jouwdomein.nl" }) }),
135
+ /* @__PURE__ */ jsx(Field, { label: "Wachtwoord", hint: status?.smtp.configured ? "Leeg laten = ongewijzigd" : void 0, children: /* @__PURE__ */ jsx(Input, { type: "password", value: smtp.pass, onChange: (e) => setSmtp((p) => ({ ...p, pass: e.target.value })) }) })
136
+ ] }),
137
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [
138
+ /* @__PURE__ */ jsx(Field, { label: "Afzendernaam", children: /* @__PURE__ */ jsx(Input, { value: smtp.fromName, onChange: (e) => setSmtp((p) => ({ ...p, fromName: e.target.value })), placeholder: "Christelijk Kraamcadeau" }) }),
139
+ /* @__PURE__ */ jsx(Field, { label: "Afzenderadres", children: /* @__PURE__ */ jsx(Input, { value: smtp.fromEmail, onChange: (e) => setSmtp((p) => ({ ...p, fromEmail: e.target.value })), placeholder: "info@jouwdomein.nl" }) })
140
+ ] }),
141
+ /* @__PURE__ */ jsx(Field, { label: "Notificatie-adres", hint: "Hier ontvang je zelf een mail bij elke bestelling", children: /* @__PURE__ */ jsx(Input, { value: smtp.notifyTo, onChange: (e) => setSmtp((p) => ({ ...p, notifyTo: e.target.value })), placeholder: "bestellingen@jouwdomein.nl" }) }),
142
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
143
+ /* @__PURE__ */ jsx(Button, { onClick: saveSmtp, disabled: busy || !smtp.host.trim(), children: "Opslaan" }),
144
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: sendTest, disabled: busy || !status?.smtp.configured, children: "Stuur testmail" })
145
+ ] })
146
+ ] }) }),
147
+ /* @__PURE__ */ jsx(Card, { className: "lg:col-span-2", children: /* @__PURE__ */ jsxs("p", { className: "text-sm text-slate-600", children: [
148
+ "Verzendkosten, valuta, ordernummer-prefix en de winkel-paden stel je in onder",
149
+ " ",
150
+ /* @__PURE__ */ jsx("strong", { children: "Instellingen \u2192 Modules \u2192 Webwinkel" }),
151
+ "."
152
+ ] }) })
153
+ ] });
154
+ }
155
+ export {
156
+ ConnectionsTab
157
+ };
158
+ //# sourceMappingURL=ConnectionsTab.js.map
@@ -0,0 +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=\"Christelijk Kraamcadeau\" />\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":";AAiHU,SACE,KADF;AA/GV,SAAS,aAAa,WAAW,gBAAgB;AACjD,SAAS,OAAO,QAAQ,MAAM,OAAO,aAAa;AAClD,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,oBAAoB;AAatB,SAAS,eAAe,EAAE,OAAO,GAAuB;AAC7D,QAAM,EAAE,MAAM,IAAI,SAAS;AAC3B,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA4B,IAAI;AAC5D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AAEtC,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,EAAE;AAC7C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,EAAE,MAAM,IAAI,MAAM,OAAO,QAAQ,MAAM,MAAM,IAAI,MAAM,IAAI,UAAU,IAAI,WAAW,IAAI,UAAU,GAAG,CAAC;AAEvI,QAAM,UAAU,YAAY,YAAY;AACtC,QAAI;AACF,YAAM,IAAI,MAAM,aAA6C,eAAe,EAAE,EAAE,OAAO,CAAC;AACxF,gBAAU,CAAC;AACX,cAAQ,CAAC,OAAO;AAAA,QACd,GAAG;AAAA,QACH,MAAM,EAAE,KAAK,QAAQ,EAAE;AAAA,QACvB,MAAM,OAAO,EAAE,KAAK,QAAQ,GAAG;AAAA,QAC/B,MAAM,EAAE,KAAK,QAAQ,EAAE;AAAA,QACvB,UAAU,EAAE,KAAK,YAAY,EAAE;AAAA,QAC/B,WAAW,EAAE,KAAK,aAAa,EAAE;AAAA,QACjC,UAAU,EAAE,KAAK,YAAY,EAAE;AAAA,MACjC,EAAE;AAAA,IACJ,QAAQ;AACN,gBAAU,IAAI;AAAA,IAChB;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,YAAU,MAAM;AACd,SAAK,QAAQ;AAAA,EACf,GAAG,CAAC,OAAO,CAAC;AAEZ,iBAAe,aAAa;AAC1B,QAAI,CAAC,UAAU,KAAK,EAAE,WAAW,KAAK,KAAK,CAAC,UAAU,KAAK,EAAE,WAAW,KAAK,GAAG;AAC9E,YAAM,EAAE,OAAO,qBAAqB,aAAa,uEAA6D,MAAM,QAAQ,CAAC;AAC7H;AAAA,IACF;AACA,YAAQ,IAAI;AACZ,QAAI;AACF,YAAM,MAAM,MAAM,aAAsF,iBAAiB,EAAE;AAAA,QACzH;AAAA,QACA,QAAQ,EAAE,WAAW,UAAU,KAAK,EAAE;AAAA,MACxC,CAAC;AACD,YAAM,EAAE,OAAO,oBAAoB,aAAa,IAAI,UAAU,YAAY,IAAI,OAAO,KAAK,QAAW,MAAM,UAAU,CAAC;AACtH,mBAAa,EAAE;AACf,YAAM,QAAQ;AAAA,IAChB,SAAS,GAAG;AACV,YAAM,EAAE,OAAO,2BAA2B,aAAa,aAAa,QAAQ,EAAE,UAAU,QAAW,MAAM,QAAQ,CAAC;AAAA,IACpH,UAAE;AACA,cAAQ,KAAK;AAAA,IACf;AAAA,EACF;AAEA,iBAAe,WAAW;AACxB,YAAQ,IAAI;AACZ,QAAI;AACF,YAAM,aAAiF,iBAAiB,EAAE;AAAA,QACxG;AAAA,QACA,MAAM;AAAA,UACJ,MAAM,KAAK,KAAK,KAAK;AAAA,UACrB,MAAM,OAAO,KAAK,IAAI,KAAK;AAAA,UAC3B,QAAQ,KAAK;AAAA,UACb,MAAM,KAAK,KAAK,KAAK;AAAA;AAAA,UAErB,GAAI,KAAK,OAAO,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;AAAA,UACvC,UAAU,KAAK,SAAS,KAAK;AAAA,UAC7B,WAAW,KAAK,UAAU,KAAK;AAAA,UAC/B,UAAU,KAAK,SAAS,KAAK;AAAA,QAC/B;AAAA,MACF,CAAC;AACD,YAAM,EAAE,OAAO,iCAAiC,MAAM,UAAU,CAAC;AACjE,cAAQ,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,GAAG,EAAE;AACnC,YAAM,QAAQ;AAAA,IAChB,SAAS,GAAG;AACV,YAAM,EAAE,OAAO,mBAAmB,aAAa,aAAa,QAAQ,EAAE,UAAU,QAAW,MAAM,QAAQ,CAAC;AAAA,IAC5G,UAAE;AACA,cAAQ,KAAK;AAAA,IACf;AAAA,EACF;AAEA,iBAAe,WAAW;AACxB,YAAQ,IAAI;AACZ,QAAI;AACF,YAAM,MAAM,MAAM,aAAiD,kBAAkB,EAAE,EAAE,OAAO,CAAC;AACjG,YAAM,EAAE,OAAO,sBAAsB,aAAa,QAAQ,IAAI,EAAE,qCAAgC,MAAM,UAAU,CAAC;AAAA,IACnH,SAAS,GAAG;AACV,YAAM,EAAE,OAAO,oBAAoB,aAAa,aAAa,QAAQ,EAAE,UAAU,QAAW,MAAM,QAAQ,CAAC;AAAA,IAC7G,UAAE;AACA,cAAQ,KAAK;AAAA,IACf;AAAA,EACF;AAEA,SACE,qBAAC,SAAI,WAAU,6BACb;AAAA,wBAAC,QACC,+BAAC,SAAI,WAAU,cACb;AAAA,2BAAC,SAAI,WAAU,qCACb;AAAA,4BAAC,QAAG,WAAU,yBAAwB,iCAAmB;AAAA,QACxD,UACC,oBAAC,SAAM,MAAM,OAAO,OAAO,aAAa,UAAU,SAC/C,iBAAO,OAAO,aAAa,YAAY,OAAO,OAAO,UAAU,WAAM,OAAO,OAAO,OAAO,KAAK,EAAE,KAAK,sBACzG;AAAA,SAEJ;AAAA,MACA,qBAAC,QAAG,WAAU,uDACZ;AAAA,6BAAC,QAAG;AAAA;AAAA,UAA2B,oBAAC,OAAE,WAAU,aAAY,MAAK,yCAAwC,QAAO,UAAS,KAAI,cAAa,wBAAU;AAAA,UAAI;AAAA,WAAsC;AAAA,QAC1L,qBAAC,QAAG;AAAA;AAAA,UAAoB,oBAAC,QAAG,6CAA0B;AAAA,UAAK;AAAA,WAAW;AAAA,QACtE,qBAAC,QAAG;AAAA;AAAA,UAAW,oBAAC,QAAG,wCAAqB;AAAA,UAAK;AAAA,WAAwF;AAAA,SACvI;AAAA,MACA,oBAAC,SAAM,OAAM,sDAA2C,MAAK,0DAC3D,8BAAC,SAAM,MAAK,YAAW,OAAO,WAAW,UAAU,CAAC,MAAM,aAAa,EAAE,OAAO,KAAK,GAAG,aAAY,kBAAY,GAClH;AAAA,MACA,oBAAC,SACC,8BAAC,UAAO,SAAS,YAAY,UAAU,QAAQ,CAAC,UAAU,KAAK,GAAG,2BAAa,GACjF;AAAA,MACC,QAAQ,OAAO,cAAc,CAAC,OAAO,OAAO,qBAC3C,oBAAC,OAAE,WAAU,2DAA0D,6GAEvE;AAAA,OAEJ,GACF;AAAA,IAEA,oBAAC,QACC,+BAAC,SAAI,WAAU,cACb;AAAA,2BAAC,SAAI,WAAU,qCACb;AAAA,4BAAC,QAAG,WAAU,yBAAwB,0CAA4B;AAAA,QACjE,UACC,oBAAC,SAAM,MAAM,OAAO,KAAK,aAAa,UAAU,SAC7C,iBAAO,KAAK,aAAa,cAAc,sBAC1C;AAAA,SAEJ;AAAA,MACA,qBAAC,OAAE,WAAU,0BAAyB;AAAA;AAAA,QAEA,oBAAC,OAAE,WAAU,aAAY,MAAK,6CAA4C,QAAO,UAAS,KAAI,cAAa,4BAAc;AAAA,QAAI;AAAA,SAEnK;AAAA,MACA,qBAAC,SAAI,WAAU,uCACb;AAAA,4BAAC,SAAM,OAAM,eACX,8BAAC,SAAM,OAAO,KAAK,MAAM,UAAU,CAAC,MAAM,QAAQ,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,EAAE,OAAO,MAAM,EAAE,GAAG,aAAY,kBAAiB,GAC3H;AAAA,QACA,oBAAC,SAAM,OAAM,SACX,8BAAC,SAAM,OAAO,KAAK,MAAM,UAAU,CAAC,MAAM,QAAQ,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,EAAE,OAAO,MAAM,EAAE,GAAG,aAAY,OAAM,GAChH;AAAA,SACF;AAAA,MACA,oBAAC,UAAO,SAAS,KAAK,QAAQ,iBAAiB,CAAC,MAAM,QAAQ,CAAC,OAAO,EAAE,GAAG,GAAG,QAAQ,EAAE,EAAE,GAAG,OAAM,+CAA8C;AAAA,MACjJ,qBAAC,SAAI,WAAU,6BACb;AAAA,4BAAC,SAAM,OAAM,kBACX,8BAAC,SAAM,OAAO,KAAK,MAAM,UAAU,CAAC,MAAM,QAAQ,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,EAAE,OAAO,MAAM,EAAE,GAAG,aAAY,sBAAqB,GAC/H;AAAA,QACA,oBAAC,SAAM,OAAM,cAAa,MAAM,QAAQ,KAAK,aAAa,6BAA6B,QACrF,8BAAC,SAAM,MAAK,YAAW,OAAO,KAAK,MAAM,UAAU,CAAC,MAAM,QAAQ,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,EAAE,OAAO,MAAM,EAAE,GAAG,GAC9G;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,6BACb;AAAA,4BAAC,SAAM,OAAM,gBACX,8BAAC,SAAM,OAAO,KAAK,UAAU,UAAU,CAAC,MAAM,QAAQ,CAAC,OAAO,EAAE,GAAG,GAAG,UAAU,EAAE,OAAO,MAAM,EAAE,GAAG,aAAY,2BAA0B,GAC5I;AAAA,QACA,oBAAC,SAAM,OAAM,iBACX,8BAAC,SAAM,OAAO,KAAK,WAAW,UAAU,CAAC,MAAM,QAAQ,CAAC,OAAO,EAAE,GAAG,GAAG,WAAW,EAAE,OAAO,MAAM,EAAE,GAAG,aAAY,sBAAqB,GACzI;AAAA,SACF;AAAA,MACA,oBAAC,SAAM,OAAM,qBAAoB,MAAK,qDACpC,8BAAC,SAAM,OAAO,KAAK,UAAU,UAAU,CAAC,MAAM,QAAQ,CAAC,OAAO,EAAE,GAAG,GAAG,UAAU,EAAE,OAAO,MAAM,EAAE,GAAG,aAAY,8BAA6B,GAC/I;AAAA,MACA,qBAAC,SAAI,WAAU,2BACb;AAAA,4BAAC,UAAO,SAAS,UAAU,UAAU,QAAQ,CAAC,KAAK,KAAK,KAAK,GAAG,qBAAO;AAAA,QACvE,oBAAC,UAAO,SAAQ,aAAY,SAAS,UAAU,UAAU,QAAQ,CAAC,QAAQ,KAAK,YAAY,4BAAc;AAAA,SAC3G;AAAA,OACF,GACF;AAAA,IAEA,oBAAC,QAAK,WAAU,iBACd,+BAAC,OAAE,WAAU,0BAAyB;AAAA;AAAA,MAC0C;AAAA,MAC9E,oBAAC,YAAO,0DAAkC;AAAA,MAAS;AAAA,OACrD,GACF;AAAA,KACF;AAEJ;","names":[]}
@@ -0,0 +1,3 @@
1
+ export declare function OrdersTab({ siteId }: {
2
+ siteId: string;
3
+ }): import("react").JSX.Element;
@@ -0,0 +1,236 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import { collection, doc, limit, onSnapshot, orderBy, query, serverTimestamp, updateDoc } from "firebase/firestore";
5
+ import { getDb } from "../../firebase/client";
6
+ import { formatCents } from "../../schema/shop";
7
+ import { Badge, Button, Card } from "../ui/primitives";
8
+ import { Select } from "../ui/select";
9
+ import { useToast } from "../ui/toast";
10
+ import { useConfirm } from "../ui/ConfirmDialog";
11
+ import { ORDER_STATUS_LABELS, shopCallable } from "./lib";
12
+ const FILTERS = [
13
+ { value: "all", label: "Alles" },
14
+ { value: "pending", label: "Wacht op betaling" },
15
+ { value: "paid", label: "Betaald" },
16
+ { value: "shipped", label: "Verzonden" },
17
+ { value: "cancelled", label: "Geannuleerd" },
18
+ { value: "refunded", label: "Terugbetaald" },
19
+ { value: "donation", label: "Donaties" }
20
+ ];
21
+ function fmtDate(t) {
22
+ if (!t?.toDate) return "\u2014";
23
+ return t.toDate().toLocaleString("nl-NL", { dateStyle: "short", timeStyle: "short" });
24
+ }
25
+ function OrdersTab({ siteId }) {
26
+ const { toast } = useToast();
27
+ const confirm = useConfirm();
28
+ const [orders, setOrders] = useState([]);
29
+ const [selected, setSelected] = useState(null);
30
+ const [filter, setFilter] = useState("all");
31
+ const [busy, setBusy] = useState(false);
32
+ useEffect(() => {
33
+ const db = getDb();
34
+ if (!db) return;
35
+ return onSnapshot(
36
+ query(collection(db, "sites", siteId, "orders"), orderBy("createdAt", "desc"), limit(200)),
37
+ (snap) => setOrders(snap.docs.map((d) => ({ id: d.id, ...d.data() })))
38
+ );
39
+ }, [siteId]);
40
+ const visible = useMemo(() => {
41
+ if (filter === "all") return orders;
42
+ if (filter === "donation") return orders.filter((o) => o.items.some((i) => i.kind === "donation"));
43
+ return orders.filter((o) => o.status === filter);
44
+ }, [orders, filter]);
45
+ const current = orders.find((o) => o.id === selected) ?? null;
46
+ async function setStatus(order, status) {
47
+ const db = getDb();
48
+ if (!db) return;
49
+ const patch = { status };
50
+ if (status === "shipped") patch.shippedAt = serverTimestamp();
51
+ await updateDoc(doc(db, "sites", siteId, "orders", order.id), patch);
52
+ toast({ title: status === "shipped" ? "Gemarkeerd als verzonden" : "Bestelling geannuleerd", tone: "success" });
53
+ }
54
+ async function refund(order) {
55
+ const ok = await confirm({
56
+ title: `Bestelling ${order.number} terugbetalen?`,
57
+ description: `Het volledige bedrag (${formatCents(order.amount.totalCents)}) wordt via Stripe teruggestort.`,
58
+ tone: "danger",
59
+ confirmLabel: "Terugbetalen"
60
+ });
61
+ if (!ok) return;
62
+ setBusy(true);
63
+ try {
64
+ await shopCallable("refundOrder")({ siteId, orderId: order.id });
65
+ toast({ title: "Terugbetaling gestart", tone: "success" });
66
+ } catch (e) {
67
+ toast({ title: "Terugbetalen mislukt", description: e instanceof Error ? e.message : void 0, tone: "error" });
68
+ } finally {
69
+ setBusy(false);
70
+ }
71
+ }
72
+ async function resendMail(order) {
73
+ setBusy(true);
74
+ try {
75
+ await shopCallable("resendOrderMail")({ siteId, orderId: order.id });
76
+ toast({ title: "Bevestigingsmail opnieuw verstuurd", tone: "success" });
77
+ } catch (e) {
78
+ toast({ title: "Mail versturen mislukt", description: e instanceof Error ? e.message : void 0, tone: "error" });
79
+ } finally {
80
+ setBusy(false);
81
+ }
82
+ }
83
+ function exportCsv() {
84
+ const rows = [
85
+ ["nummer", "datum", "status", "naam", "email", "items", "subtotaal", "verzendkosten", "totaal"],
86
+ ...orders.map((o) => [
87
+ o.number,
88
+ fmtDate(o.createdAt),
89
+ o.status,
90
+ o.customer.name,
91
+ o.customer.email,
92
+ o.items.map((i) => `${i.qty}x ${i.name}${i.variantKey ? ` (${i.variantKey})` : ""}`).join(" | "),
93
+ (o.amount.itemsCents / 100).toFixed(2),
94
+ (o.amount.shippingCents / 100).toFixed(2),
95
+ (o.amount.totalCents / 100).toFixed(2)
96
+ ])
97
+ ];
98
+ const csv = rows.map((r) => r.map((c) => `"${String(c).replaceAll('"', '""')}"`).join(";")).join("\n");
99
+ const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8" });
100
+ const a = document.createElement("a");
101
+ a.href = URL.createObjectURL(blob);
102
+ a.download = `bestellingen-${siteId}.csv`;
103
+ a.click();
104
+ URL.revokeObjectURL(a.href);
105
+ }
106
+ return /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-[340px_1fr]", children: [
107
+ /* @__PURE__ */ jsxs(Card, { className: "h-fit", children: [
108
+ /* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between gap-2", children: [
109
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "Bestellingen" }),
110
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: exportCsv, disabled: orders.length === 0, children: "CSV" })
111
+ ] }),
112
+ /* @__PURE__ */ jsx(Select, { value: filter, onValueChange: setFilter, options: FILTERS, className: "mb-2" }),
113
+ /* @__PURE__ */ jsxs("ul", { className: "grid max-h-[65vh] gap-1 overflow-y-auto", children: [
114
+ visible.map((o) => {
115
+ const st = ORDER_STATUS_LABELS[o.status] ?? { label: o.status, tone: "slate" };
116
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
117
+ "button",
118
+ {
119
+ onClick: () => setSelected(o.id),
120
+ className: `grid w-full gap-0.5 rounded-lg px-3 py-2 text-left text-sm ${o.id === selected ? "bg-brand-50 text-brand-700" : "hover:bg-slate-100"}`,
121
+ children: [
122
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center justify-between gap-2", children: [
123
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: o.number }),
124
+ /* @__PURE__ */ jsx(Badge, { tone: st.tone, children: st.label })
125
+ ] }),
126
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center justify-between gap-2 text-xs text-slate-500", children: [
127
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: o.customer.name }),
128
+ /* @__PURE__ */ jsx("span", { children: formatCents(o.amount.totalCents) })
129
+ ] }),
130
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-slate-400", children: fmtDate(o.createdAt) })
131
+ ]
132
+ }
133
+ ) }, o.id);
134
+ }),
135
+ visible.length === 0 && /* @__PURE__ */ jsx("li", { className: "px-3 py-2 text-sm text-slate-500", children: "Geen bestellingen." })
136
+ ] })
137
+ ] }),
138
+ current ? /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
139
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
140
+ /* @__PURE__ */ jsxs("h3", { className: "text-base font-semibold", children: [
141
+ current.number,
142
+ " ",
143
+ /* @__PURE__ */ jsx(Badge, { tone: (ORDER_STATUS_LABELS[current.status] ?? { tone: "slate" }).tone, children: (ORDER_STATUS_LABELS[current.status] ?? { label: current.status }).label })
144
+ ] }),
145
+ /* @__PURE__ */ jsxs("span", { className: "text-sm text-slate-500", children: [
146
+ "Geplaatst: ",
147
+ fmtDate(current.createdAt)
148
+ ] })
149
+ ] }),
150
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [
151
+ /* @__PURE__ */ jsxs("div", { children: [
152
+ /* @__PURE__ */ jsx("h4", { className: "mb-1 text-sm font-semibold", children: "Klant" }),
153
+ /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
154
+ current.customer.name,
155
+ /* @__PURE__ */ jsx("br", {}),
156
+ /* @__PURE__ */ jsx("a", { className: "text-brand-700 underline", href: `mailto:${current.customer.email}`, children: current.customer.email }),
157
+ current.customer.phone && /* @__PURE__ */ jsxs(Fragment, { children: [
158
+ /* @__PURE__ */ jsx("br", {}),
159
+ current.customer.phone
160
+ ] })
161
+ ] })
162
+ ] }),
163
+ /* @__PURE__ */ jsxs("div", { children: [
164
+ /* @__PURE__ */ jsx("h4", { className: "mb-1 text-sm font-semibold", children: "Verzendadres" }),
165
+ current.shipping ? /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
166
+ current.shipping.address,
167
+ /* @__PURE__ */ jsx("br", {}),
168
+ current.shipping.zip,
169
+ " ",
170
+ current.shipping.city,
171
+ " (",
172
+ current.shipping.country,
173
+ ")"
174
+ ] }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Geen verzending (donatie)." })
175
+ ] })
176
+ ] }),
177
+ current.note && /* @__PURE__ */ jsxs("div", { children: [
178
+ /* @__PURE__ */ jsx("h4", { className: "mb-1 text-sm font-semibold", children: "Opmerking van de klant" }),
179
+ /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap text-sm", children: current.note })
180
+ ] }),
181
+ /* @__PURE__ */ jsxs("div", { children: [
182
+ /* @__PURE__ */ jsx("h4", { className: "mb-1 text-sm font-semibold", children: "Regels" }),
183
+ /* @__PURE__ */ jsx("table", { className: "w-full text-sm", children: /* @__PURE__ */ jsxs("tbody", { children: [
184
+ current.items.map((i, idx) => /* @__PURE__ */ jsxs("tr", { className: "border-t border-slate-100", children: [
185
+ /* @__PURE__ */ jsxs("td", { className: "py-1 pr-2", children: [
186
+ i.qty,
187
+ "\xD7 ",
188
+ i.name,
189
+ i.variantKey && /* @__PURE__ */ jsxs("span", { className: "text-slate-500", children: [
190
+ " (",
191
+ i.variantKey.split("|").join(" \xB7 "),
192
+ ")"
193
+ ] }),
194
+ i.kind === "donation" && /* @__PURE__ */ jsx(Badge, { tone: "amber", children: "donatie" })
195
+ ] }),
196
+ /* @__PURE__ */ jsx("td", { className: "py-1 text-right", children: formatCents(i.unitPriceCents * i.qty) })
197
+ ] }, idx)),
198
+ /* @__PURE__ */ jsxs("tr", { className: "border-t border-slate-100 text-slate-500", children: [
199
+ /* @__PURE__ */ jsx("td", { className: "py-1 pr-2", children: "Verzendkosten" }),
200
+ /* @__PURE__ */ jsx("td", { className: "py-1 text-right", children: current.amount.shippingCents === 0 ? "gratis" : formatCents(current.amount.shippingCents) })
201
+ ] }),
202
+ /* @__PURE__ */ jsxs("tr", { className: "border-t border-slate-200 font-semibold", children: [
203
+ /* @__PURE__ */ jsx("td", { className: "py-1 pr-2", children: "Totaal" }),
204
+ /* @__PURE__ */ jsx("td", { className: "py-1 text-right", children: formatCents(current.amount.totalCents) })
205
+ ] })
206
+ ] }) })
207
+ ] }),
208
+ current.mail?.error && /* @__PURE__ */ jsxs("p", { className: "rounded-lg bg-amber-50 px-3 py-2 text-sm text-amber-700", children: [
209
+ "Bevestigingsmail mislukt: ",
210
+ current.mail.error
211
+ ] }),
212
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 border-t border-slate-100 pt-4", children: [
213
+ current.status === "paid" && /* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => setStatus(current, "shipped"), children: "Markeer als verzonden" }),
214
+ current.status === "pending" && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: () => setStatus(current, "cancelled"), children: "Annuleer" }),
215
+ (current.status === "paid" || current.status === "shipped") && /* @__PURE__ */ jsxs(Fragment, { children: [
216
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", disabled: busy, onClick: () => resendMail(current), children: "Mail opnieuw versturen" }),
217
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "danger", disabled: busy, onClick: () => refund(current), children: "Terugbetalen" })
218
+ ] }),
219
+ current.stripe?.paymentIntentId && /* @__PURE__ */ jsx(
220
+ "a",
221
+ {
222
+ className: "ml-auto text-sm text-brand-700 underline",
223
+ href: `https://dashboard.stripe.com/payments/${current.stripe.paymentIntentId}`,
224
+ target: "_blank",
225
+ rel: "noreferrer",
226
+ children: "Bekijk in Stripe \u2197"
227
+ }
228
+ )
229
+ ] })
230
+ ] }) }) : /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Selecteer een bestelling." }) })
231
+ ] });
232
+ }
233
+ export {
234
+ OrdersTab
235
+ };
236
+ //# sourceMappingURL=OrdersTab.js.map