@xiwebdesign/xicms-core 5.4.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.
- package/dist/admin/panelRegistry.js +3 -0
- package/dist/admin/panelRegistry.js.map +1 -1
- package/dist/admin/shop/CategoriesTab.d.ts +3 -0
- package/dist/admin/shop/CategoriesTab.js +140 -0
- package/dist/admin/shop/CategoriesTab.js.map +1 -0
- package/dist/admin/shop/ConnectionsTab.d.ts +9 -0
- package/dist/admin/shop/ConnectionsTab.js +158 -0
- package/dist/admin/shop/ConnectionsTab.js.map +1 -0
- package/dist/admin/shop/OrdersTab.d.ts +3 -0
- package/dist/admin/shop/OrdersTab.js +236 -0
- package/dist/admin/shop/OrdersTab.js.map +1 -0
- package/dist/admin/shop/ProductsTab.d.ts +3 -0
- package/dist/admin/shop/ProductsTab.js +397 -0
- package/dist/admin/shop/ProductsTab.js.map +1 -0
- package/dist/admin/shop/ShopPanel.d.ts +4 -0
- package/dist/admin/shop/ShopPanel.js +25 -0
- package/dist/admin/shop/ShopPanel.js.map +1 -0
- package/dist/admin/shop/lib.d.ts +16 -0
- package/dist/admin/shop/lib.js +46 -0
- package/dist/admin/shop/lib.js.map +1 -0
- package/dist/admin.css +1 -1
- package/dist/modules/builtins.d.ts +1 -0
- package/dist/modules/builtins.js +4 -0
- package/dist/modules/builtins.js.map +1 -1
- package/dist/modules/index.d.ts +4 -0
- package/dist/modules/index.js +12 -0
- package/dist/modules/index.js.map +1 -1
- package/dist/modules/shop/blocks/Cart.d.ts +2 -0
- package/dist/modules/shop/blocks/Cart.js +91 -0
- package/dist/modules/shop/blocks/Cart.js.map +1 -0
- package/dist/modules/shop/blocks/Checkout.d.ts +2 -0
- package/dist/modules/shop/blocks/Checkout.js +126 -0
- package/dist/modules/shop/blocks/Checkout.js.map +1 -0
- package/dist/modules/shop/blocks/Donation.d.ts +2 -0
- package/dist/modules/shop/blocks/Donation.js +145 -0
- package/dist/modules/shop/blocks/Donation.js.map +1 -0
- package/dist/modules/shop/blocks/OrderStatus.d.ts +2 -0
- package/dist/modules/shop/blocks/OrderStatus.js +40 -0
- package/dist/modules/shop/blocks/OrderStatus.js.map +1 -0
- package/dist/modules/shop/blocks/ProductDetail.d.ts +2 -0
- package/dist/modules/shop/blocks/ProductDetail.js +168 -0
- package/dist/modules/shop/blocks/ProductDetail.js.map +1 -0
- package/dist/modules/shop/blocks/ProductGrid.d.ts +8 -0
- package/dist/modules/shop/blocks/ProductGrid.js +136 -0
- package/dist/modules/shop/blocks/ProductGrid.js.map +1 -0
- package/dist/modules/shop/blocks/ShopCategories.d.ts +2 -0
- package/dist/modules/shop/blocks/ShopCategories.js +69 -0
- package/dist/modules/shop/blocks/ShopCategories.js.map +1 -0
- package/dist/modules/shop/cartStore.d.ts +22 -0
- package/dist/modules/shop/cartStore.js +85 -0
- package/dist/modules/shop/cartStore.js.map +1 -0
- package/dist/modules/shop/lib.d.ts +25 -0
- package/dist/modules/shop/lib.js +61 -0
- package/dist/modules/shop/lib.js.map +1 -0
- package/dist/modules/shop/module.d.ts +1 -0
- package/dist/modules/shop/module.js +39 -0
- package/dist/modules/shop/module.js.map +1 -0
- package/dist/modules/shop/settings.d.ts +41 -0
- package/dist/modules/shop/settings.js +18 -0
- package/dist/modules/shop/settings.js.map +1 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/shop.d.ts +939 -0
- package/dist/schema/shop.js +209 -0
- package/dist/schema/shop.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { addDoc, collection, deleteDoc, doc, onSnapshot, setDoc } from "firebase/firestore";
|
|
5
|
+
import { getDb } from "../../firebase/client";
|
|
6
|
+
import {
|
|
7
|
+
ProductSchema,
|
|
8
|
+
formatCents
|
|
9
|
+
} from "../../schema/shop";
|
|
10
|
+
import { Badge, Button, Card, Field, Input, Textarea } from "../ui/primitives";
|
|
11
|
+
import { Select } from "../ui/select";
|
|
12
|
+
import { Switch } from "../ui/switch";
|
|
13
|
+
import { useToast } from "../ui/toast";
|
|
14
|
+
import { useConfirm } from "../ui/ConfirmDialog";
|
|
15
|
+
import { MediaPickerButton } from "../media/MediaPicker";
|
|
16
|
+
import { slugify } from "../lib/slugify";
|
|
17
|
+
import { allVariantKeys, centsToEuroInput, eurosToCents } from "./lib";
|
|
18
|
+
const STATUS_OPTIONS = [
|
|
19
|
+
{ value: "draft", label: "Concept" },
|
|
20
|
+
{ value: "active", label: "Actief" },
|
|
21
|
+
{ value: "archived", label: "Gearchiveerd" }
|
|
22
|
+
];
|
|
23
|
+
function ProductsTab({ siteId }) {
|
|
24
|
+
const { toast } = useToast();
|
|
25
|
+
const confirm = useConfirm();
|
|
26
|
+
const [products, setProducts] = useState([]);
|
|
27
|
+
const [cats, setCats] = useState([]);
|
|
28
|
+
const [selected, setSelected] = useState(null);
|
|
29
|
+
const [q, setQ] = useState("");
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const db = getDb();
|
|
32
|
+
if (!db) return;
|
|
33
|
+
const un1 = onSnapshot(
|
|
34
|
+
collection(db, "sites", siteId, "products"),
|
|
35
|
+
(snap) => setProducts(
|
|
36
|
+
snap.docs.map((d) => ({ id: d.id, ...d.data() })).sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, "nl"))
|
|
37
|
+
)
|
|
38
|
+
);
|
|
39
|
+
const un2 = onSnapshot(
|
|
40
|
+
collection(db, "sites", siteId, "shopCategories"),
|
|
41
|
+
(snap) => setCats(snap.docs.map((d) => ({ id: d.id, ...d.data() })))
|
|
42
|
+
);
|
|
43
|
+
return () => {
|
|
44
|
+
un1();
|
|
45
|
+
un2();
|
|
46
|
+
};
|
|
47
|
+
}, [siteId]);
|
|
48
|
+
async function createProduct() {
|
|
49
|
+
const db = getDb();
|
|
50
|
+
if (!db) return;
|
|
51
|
+
const ref = await addDoc(collection(db, "sites", siteId, "products"), {
|
|
52
|
+
name: "Nieuw product",
|
|
53
|
+
slug: `product-${products.length + 1}`,
|
|
54
|
+
description: "",
|
|
55
|
+
categoryIds: [],
|
|
56
|
+
images: [],
|
|
57
|
+
priceCents: 0,
|
|
58
|
+
vatRate: 21,
|
|
59
|
+
options: [],
|
|
60
|
+
variants: [],
|
|
61
|
+
trackStock: false,
|
|
62
|
+
status: "draft",
|
|
63
|
+
order: products.length,
|
|
64
|
+
featured: false
|
|
65
|
+
});
|
|
66
|
+
setSelected(ref.id);
|
|
67
|
+
}
|
|
68
|
+
const filtered = useMemo(() => {
|
|
69
|
+
const needle = q.trim().toLowerCase();
|
|
70
|
+
if (!needle) return products;
|
|
71
|
+
return products.filter((p) => p.name.toLowerCase().includes(needle) || p.slug.includes(needle));
|
|
72
|
+
}, [products, q]);
|
|
73
|
+
const current = products.find((p) => p.id === selected) ?? null;
|
|
74
|
+
return /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-[300px_1fr]", children: [
|
|
75
|
+
/* @__PURE__ */ jsxs(Card, { className: "h-fit", children: [
|
|
76
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between", children: [
|
|
77
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "Producten" }),
|
|
78
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", onClick: createProduct, children: "+ Nieuw" })
|
|
79
|
+
] }),
|
|
80
|
+
/* @__PURE__ */ jsx(Input, { placeholder: "Zoeken\u2026", value: q, onChange: (e) => setQ(e.target.value), className: "mb-2" }),
|
|
81
|
+
/* @__PURE__ */ jsxs("ul", { className: "grid max-h-[60vh] gap-1 overflow-y-auto", children: [
|
|
82
|
+
filtered.map((p) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
83
|
+
"button",
|
|
84
|
+
{
|
|
85
|
+
onClick: () => setSelected(p.id),
|
|
86
|
+
className: `flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm ${p.id === selected ? "bg-brand-50 text-brand-700" : "hover:bg-slate-100"}`,
|
|
87
|
+
children: [
|
|
88
|
+
p.images?.[0] && /* @__PURE__ */ jsx("img", { src: p.images[0], alt: "", className: "h-8 w-8 rounded object-cover" }),
|
|
89
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: p.name }),
|
|
90
|
+
/* @__PURE__ */ jsx(Badge, { tone: p.status === "active" ? "green" : p.status === "draft" ? "amber" : "slate", children: STATUS_OPTIONS.find((s) => s.value === p.status)?.label ?? p.status })
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
) }, p.id)),
|
|
94
|
+
filtered.length === 0 && /* @__PURE__ */ jsx("li", { className: "px-3 py-2 text-sm text-slate-500", children: "Geen producten." })
|
|
95
|
+
] })
|
|
96
|
+
] }),
|
|
97
|
+
current ? /* @__PURE__ */ jsx(
|
|
98
|
+
ProductDetail,
|
|
99
|
+
{
|
|
100
|
+
siteId,
|
|
101
|
+
product: current,
|
|
102
|
+
categories: cats,
|
|
103
|
+
onDeleted: () => setSelected(null),
|
|
104
|
+
onToast: toast,
|
|
105
|
+
confirm
|
|
106
|
+
},
|
|
107
|
+
current.id
|
|
108
|
+
) : /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Selecteer of maak een product." }) })
|
|
109
|
+
] });
|
|
110
|
+
}
|
|
111
|
+
function ProductDetail({
|
|
112
|
+
siteId,
|
|
113
|
+
product,
|
|
114
|
+
categories,
|
|
115
|
+
onDeleted,
|
|
116
|
+
onToast,
|
|
117
|
+
confirm
|
|
118
|
+
}) {
|
|
119
|
+
const [def, setDef] = useState(product);
|
|
120
|
+
const [priceInput, setPriceInput] = useState(centsToEuroInput(product.priceCents));
|
|
121
|
+
const [compareInput, setCompareInput] = useState(centsToEuroInput(product.compareAtCents));
|
|
122
|
+
const variantKeys = useMemo(() => allVariantKeys(def.options ?? []), [def.options]);
|
|
123
|
+
function setOption(i, patch) {
|
|
124
|
+
setDef((p) => {
|
|
125
|
+
const options = [...p.options ?? []];
|
|
126
|
+
options[i] = { ...options[i], ...patch };
|
|
127
|
+
return { ...p, options };
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function variantFor(key) {
|
|
131
|
+
return (def.variants ?? []).find((v) => v.key === key);
|
|
132
|
+
}
|
|
133
|
+
function patchVariant(key, patch) {
|
|
134
|
+
setDef((p) => {
|
|
135
|
+
const variants = [...p.variants ?? []];
|
|
136
|
+
const idx = variants.findIndex((v) => v.key === key);
|
|
137
|
+
if (idx >= 0) variants[idx] = { ...variants[idx], ...patch };
|
|
138
|
+
else variants.push({ key, active: true, ...patch });
|
|
139
|
+
return { ...p, variants };
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function save() {
|
|
143
|
+
const db = getDb();
|
|
144
|
+
if (!db) return;
|
|
145
|
+
const validKeys = new Set(variantKeys);
|
|
146
|
+
const cleanVariants = (def.variants ?? []).filter(
|
|
147
|
+
(v) => validKeys.has(v.key) && (v.priceCents !== void 0 || v.stock !== void 0 || v.sku !== void 0 || v.active === false)
|
|
148
|
+
);
|
|
149
|
+
const parsed = ProductSchema.safeParse({ ...def, variants: cleanVariants });
|
|
150
|
+
if (!parsed.success) {
|
|
151
|
+
onToast({ title: "Ongeldig product", description: parsed.error.issues[0]?.message, tone: "error" });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
await setDoc(doc(db, "sites", siteId, "products", product.id), parsed.data);
|
|
155
|
+
onToast({ title: "Product opgeslagen", tone: "success" });
|
|
156
|
+
}
|
|
157
|
+
async function remove() {
|
|
158
|
+
const db = getDb();
|
|
159
|
+
if (!db) return;
|
|
160
|
+
const ok = await confirm({
|
|
161
|
+
title: `"${product.name}" verwijderen?`,
|
|
162
|
+
description: "Bestaande bestellingen behouden hun eigen kopie van naam en prijs.",
|
|
163
|
+
tone: "danger"
|
|
164
|
+
});
|
|
165
|
+
if (!ok) return;
|
|
166
|
+
await deleteDoc(doc(db, "sites", siteId, "products", product.id));
|
|
167
|
+
onDeleted();
|
|
168
|
+
}
|
|
169
|
+
return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
|
|
170
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-2", children: [
|
|
171
|
+
/* @__PURE__ */ jsx(Field, { label: "Naam", children: /* @__PURE__ */ jsx(
|
|
172
|
+
Input,
|
|
173
|
+
{
|
|
174
|
+
value: def.name,
|
|
175
|
+
onChange: (e) => setDef((p) => ({ ...p, name: e.target.value, slug: slugify(e.target.value) || p.slug }))
|
|
176
|
+
}
|
|
177
|
+
) }),
|
|
178
|
+
/* @__PURE__ */ jsx(Field, { label: "Slug", hint: "Product-URL: /product?p=<slug>", children: /* @__PURE__ */ jsx(Input, { value: def.slug, onChange: (e) => setDef((p) => ({ ...p, slug: slugify(e.target.value) })) }) })
|
|
179
|
+
] }),
|
|
180
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-3", children: [
|
|
181
|
+
/* @__PURE__ */ jsx(Field, { label: "Prijs (\u20AC, incl. btw)", children: /* @__PURE__ */ jsx(
|
|
182
|
+
Input,
|
|
183
|
+
{
|
|
184
|
+
value: priceInput,
|
|
185
|
+
onChange: (e) => {
|
|
186
|
+
setPriceInput(e.target.value);
|
|
187
|
+
const c = eurosToCents(e.target.value);
|
|
188
|
+
if (c !== null) setDef((p) => ({ ...p, priceCents: c }));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
) }),
|
|
192
|
+
/* @__PURE__ */ jsx(Field, { label: "Van-prijs (\u20AC, optioneel)", hint: "Doorgestreept naast de prijs", children: /* @__PURE__ */ jsx(
|
|
193
|
+
Input,
|
|
194
|
+
{
|
|
195
|
+
value: compareInput,
|
|
196
|
+
onChange: (e) => {
|
|
197
|
+
setCompareInput(e.target.value);
|
|
198
|
+
const c = eurosToCents(e.target.value);
|
|
199
|
+
setDef((p) => ({ ...p, compareAtCents: c ?? void 0 }));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
) }),
|
|
203
|
+
/* @__PURE__ */ jsx(Field, { label: "Btw-tarief", children: /* @__PURE__ */ jsx(
|
|
204
|
+
Select,
|
|
205
|
+
{
|
|
206
|
+
value: String(def.vatRate ?? 21),
|
|
207
|
+
onValueChange: (v) => setDef((p) => ({ ...p, vatRate: Number(v) })),
|
|
208
|
+
options: [
|
|
209
|
+
{ value: "21", label: "21%" },
|
|
210
|
+
{ value: "9", label: "9%" },
|
|
211
|
+
{ value: "0", label: "0%" }
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
) })
|
|
215
|
+
] }),
|
|
216
|
+
/* @__PURE__ */ jsx(Field, { label: "Omschrijving", children: /* @__PURE__ */ jsx(Textarea, { rows: 4, value: def.description ?? "", onChange: (e) => setDef((p) => ({ ...p, description: e.target.value })) }) }),
|
|
217
|
+
/* @__PURE__ */ jsx(Field, { label: "Afbeeldingen", hint: "Eerste afbeelding is het hoofdbeeld", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
|
|
218
|
+
(def.images ?? []).map((url, i) => /* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
219
|
+
/* @__PURE__ */ jsx("img", { src: url, alt: "", className: "h-16 w-16 rounded-lg object-cover" }),
|
|
220
|
+
/* @__PURE__ */ jsx(
|
|
221
|
+
"button",
|
|
222
|
+
{
|
|
223
|
+
type: "button",
|
|
224
|
+
"aria-label": "Verwijder afbeelding",
|
|
225
|
+
className: "absolute -right-2 -top-2 grid h-5 w-5 place-items-center rounded-full bg-slate-700 text-xs text-white",
|
|
226
|
+
onClick: () => setDef((p) => ({ ...p, images: (p.images ?? []).filter((_, j) => j !== i) })),
|
|
227
|
+
children: "\u2715"
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
] }, url + i)),
|
|
231
|
+
/* @__PURE__ */ jsx(
|
|
232
|
+
MediaPickerButton,
|
|
233
|
+
{
|
|
234
|
+
siteId,
|
|
235
|
+
label: "+ Afbeelding",
|
|
236
|
+
onSelect: (m) => setDef((p) => ({ ...p, images: [...p.images ?? [], m.url] }))
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
] }) }),
|
|
240
|
+
/* @__PURE__ */ jsx(Field, { label: "Categorie\xEBn", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
241
|
+
categories.map((c) => {
|
|
242
|
+
const on = (def.categoryIds ?? []).includes(c.id);
|
|
243
|
+
return /* @__PURE__ */ jsxs(
|
|
244
|
+
"button",
|
|
245
|
+
{
|
|
246
|
+
type: "button",
|
|
247
|
+
onClick: () => setDef((p) => ({
|
|
248
|
+
...p,
|
|
249
|
+
categoryIds: on ? (p.categoryIds ?? []).filter((x) => x !== c.id) : [...p.categoryIds ?? [], c.id]
|
|
250
|
+
})),
|
|
251
|
+
className: `rounded-full border px-3 py-1 text-sm ${on ? "border-brand-500 bg-brand-50 text-brand-700" : "border-slate-200 hover:bg-slate-50"}`,
|
|
252
|
+
children: [
|
|
253
|
+
on ? "\u2713 " : "",
|
|
254
|
+
c.name
|
|
255
|
+
]
|
|
256
|
+
},
|
|
257
|
+
c.id
|
|
258
|
+
);
|
|
259
|
+
}),
|
|
260
|
+
categories.length === 0 && /* @__PURE__ */ jsx("span", { className: "text-sm text-slate-500", children: "Maak eerst een categorie aan." })
|
|
261
|
+
] }) }),
|
|
262
|
+
/* @__PURE__ */ jsx(
|
|
263
|
+
Field,
|
|
264
|
+
{
|
|
265
|
+
label: "Opties (bv. Maat, Kleur)",
|
|
266
|
+
hint: "Max. 3. Waarden gescheiden door komma's. Combinaties kun je hieronder per stuk aanpassen.",
|
|
267
|
+
children: /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
|
|
268
|
+
(def.options ?? []).map((opt, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
269
|
+
/* @__PURE__ */ jsx(
|
|
270
|
+
Input,
|
|
271
|
+
{
|
|
272
|
+
className: "w-36",
|
|
273
|
+
placeholder: "Naam (bv. Maat)",
|
|
274
|
+
value: opt.name,
|
|
275
|
+
onChange: (e) => setOption(i, { name: e.target.value })
|
|
276
|
+
}
|
|
277
|
+
),
|
|
278
|
+
/* @__PURE__ */ jsx(
|
|
279
|
+
Input,
|
|
280
|
+
{
|
|
281
|
+
placeholder: "Waarden, bv: 56, 62, 68, 74",
|
|
282
|
+
value: opt.values.join(", "),
|
|
283
|
+
onChange: (e) => setOption(i, { values: e.target.value.split(",").map((s) => s.trim()).filter(Boolean) })
|
|
284
|
+
}
|
|
285
|
+
),
|
|
286
|
+
/* @__PURE__ */ jsx(
|
|
287
|
+
Button,
|
|
288
|
+
{
|
|
289
|
+
size: "sm",
|
|
290
|
+
variant: "ghost",
|
|
291
|
+
onClick: () => setDef((p) => ({ ...p, options: (p.options ?? []).filter((_, j) => j !== i) })),
|
|
292
|
+
children: "\u2715"
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
] }, i)),
|
|
296
|
+
(def.options ?? []).length < 3 && /* @__PURE__ */ jsx(
|
|
297
|
+
Button,
|
|
298
|
+
{
|
|
299
|
+
size: "sm",
|
|
300
|
+
variant: "secondary",
|
|
301
|
+
className: "w-fit",
|
|
302
|
+
onClick: () => setDef((p) => ({ ...p, options: [...p.options ?? [], { name: "", values: [] }] })),
|
|
303
|
+
children: "+ Optie"
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
] })
|
|
307
|
+
}
|
|
308
|
+
),
|
|
309
|
+
variantKeys.length > 0 && variantKeys.length <= 60 && /* @__PURE__ */ jsx(Field, { label: "Varianten", hint: "Leeg = standaardprijs / onbeperkte voorraad", children: /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-sm", children: [
|
|
310
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "text-left text-xs text-slate-500", children: [
|
|
311
|
+
/* @__PURE__ */ jsx("th", { className: "py-1 pr-2", children: "Combinatie" }),
|
|
312
|
+
/* @__PURE__ */ jsx("th", { className: "py-1 pr-2", children: "Prijs (\u20AC)" }),
|
|
313
|
+
def.trackStock && /* @__PURE__ */ jsx("th", { className: "py-1 pr-2", children: "Voorraad" }),
|
|
314
|
+
/* @__PURE__ */ jsx("th", { className: "py-1 pr-2", children: "Leverbaar" })
|
|
315
|
+
] }) }),
|
|
316
|
+
/* @__PURE__ */ jsx("tbody", { children: variantKeys.map((key) => {
|
|
317
|
+
const v = variantFor(key);
|
|
318
|
+
return /* @__PURE__ */ jsxs("tr", { className: "border-t border-slate-100", children: [
|
|
319
|
+
/* @__PURE__ */ jsx("td", { className: "py-1 pr-2", children: key.split("|").join(" \xB7 ") }),
|
|
320
|
+
/* @__PURE__ */ jsx("td", { className: "py-1 pr-2", children: /* @__PURE__ */ jsx(
|
|
321
|
+
"input",
|
|
322
|
+
{
|
|
323
|
+
className: "w-24 rounded-lg border border-slate-200 px-2 py-1",
|
|
324
|
+
placeholder: centsToEuroInput(def.priceCents),
|
|
325
|
+
defaultValue: centsToEuroInput(v?.priceCents),
|
|
326
|
+
onChange: (e) => {
|
|
327
|
+
const c = eurosToCents(e.target.value);
|
|
328
|
+
patchVariant(key, { priceCents: c ?? void 0 });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
) }),
|
|
332
|
+
def.trackStock && /* @__PURE__ */ jsx("td", { className: "py-1 pr-2", children: /* @__PURE__ */ jsx(
|
|
333
|
+
"input",
|
|
334
|
+
{
|
|
335
|
+
className: "w-20 rounded-lg border border-slate-200 px-2 py-1",
|
|
336
|
+
type: "number",
|
|
337
|
+
min: 0,
|
|
338
|
+
placeholder: "\u221E",
|
|
339
|
+
defaultValue: v?.stock ?? "",
|
|
340
|
+
onChange: (e) => {
|
|
341
|
+
const n = e.target.value === "" ? void 0 : Math.max(0, Number(e.target.value) || 0);
|
|
342
|
+
patchVariant(key, { stock: n });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
) }),
|
|
346
|
+
/* @__PURE__ */ jsx("td", { className: "py-1 pr-2", children: /* @__PURE__ */ jsx(Switch, { checked: v?.active !== false, onCheckedChange: (on) => patchVariant(key, { active: on }) }) })
|
|
347
|
+
] }, key);
|
|
348
|
+
}) })
|
|
349
|
+
] }) }) }),
|
|
350
|
+
variantKeys.length > 60 && /* @__PURE__ */ jsxs("p", { className: "text-sm text-amber-600", children: [
|
|
351
|
+
variantKeys.length,
|
|
352
|
+
" combinaties \u2014 te veel om per stuk te tonen. Beperk het aantal optie-waarden."
|
|
353
|
+
] }),
|
|
354
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-6", children: [
|
|
355
|
+
/* @__PURE__ */ jsx(Field, { label: "Status", children: /* @__PURE__ */ jsx(
|
|
356
|
+
Select,
|
|
357
|
+
{
|
|
358
|
+
value: def.status ?? "draft",
|
|
359
|
+
onValueChange: (v) => setDef((p) => ({ ...p, status: v })),
|
|
360
|
+
options: STATUS_OPTIONS
|
|
361
|
+
}
|
|
362
|
+
) }),
|
|
363
|
+
/* @__PURE__ */ jsx(Field, { label: "Volgorde", children: /* @__PURE__ */ jsx(
|
|
364
|
+
Input,
|
|
365
|
+
{
|
|
366
|
+
type: "number",
|
|
367
|
+
className: "w-24",
|
|
368
|
+
value: def.order ?? 0,
|
|
369
|
+
onChange: (e) => setDef((p) => ({ ...p, order: Math.max(0, Number(e.target.value) || 0) }))
|
|
370
|
+
}
|
|
371
|
+
) }),
|
|
372
|
+
/* @__PURE__ */ jsx(Switch, { checked: def.featured === true, onCheckedChange: (v) => setDef((p) => ({ ...p, featured: v })), label: "Uitgelicht" }),
|
|
373
|
+
/* @__PURE__ */ jsx(Switch, { checked: def.trackStock === true, onCheckedChange: (v) => setDef((p) => ({ ...p, trackStock: v })), label: "Voorraad bijhouden" }),
|
|
374
|
+
def.trackStock && (def.options ?? []).length === 0 && /* @__PURE__ */ jsx(Field, { label: "Voorraad", children: /* @__PURE__ */ jsx(
|
|
375
|
+
Input,
|
|
376
|
+
{
|
|
377
|
+
type: "number",
|
|
378
|
+
className: "w-24",
|
|
379
|
+
placeholder: "\u221E",
|
|
380
|
+
value: def.stock ?? "",
|
|
381
|
+
onChange: (e) => setDef((p) => ({ ...p, stock: e.target.value === "" ? void 0 : Math.max(0, Number(e.target.value) || 0) }))
|
|
382
|
+
}
|
|
383
|
+
) })
|
|
384
|
+
] }),
|
|
385
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-t border-slate-100 pt-4", children: [
|
|
386
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
387
|
+
/* @__PURE__ */ jsx(Button, { onClick: save, children: "Opslaan" }),
|
|
388
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm text-slate-500", children: formatCents(def.priceCents) })
|
|
389
|
+
] }),
|
|
390
|
+
/* @__PURE__ */ jsx(Button, { variant: "danger", onClick: remove, children: "Verwijderen" })
|
|
391
|
+
] })
|
|
392
|
+
] }) });
|
|
393
|
+
}
|
|
394
|
+
export {
|
|
395
|
+
ProductsTab
|
|
396
|
+
};
|
|
397
|
+
//# sourceMappingURL=ProductsTab.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/shop/ProductsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport { addDoc, collection, deleteDoc, doc, onSnapshot, setDoc } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport {\n ProductSchema,\n formatCents,\n type Product,\n type ProductOption,\n type ProductVariant,\n type ShopCategory,\n} from \"../../schema/shop\";\nimport { Badge, Button, Card, Field, Input, Textarea } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm, type ConfirmOptions } from \"../ui/ConfirmDialog\";\nimport { MediaPickerButton } from \"../media/MediaPicker\";\nimport { slugify } from \"../lib/slugify\";\nimport { allVariantKeys, centsToEuroInput, eurosToCents } from \"./lib\";\n\ntype StoredProduct = Product & { id: string };\ntype StoredCategory = ShopCategory & { id: string };\n\nconst STATUS_OPTIONS = [\n { value: \"draft\", label: \"Concept\" },\n { value: \"active\", label: \"Actief\" },\n { value: \"archived\", label: \"Gearchiveerd\" },\n];\n\nexport function ProductsTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [products, setProducts] = useState<StoredProduct[]>([]);\n const [cats, setCats] = useState<StoredCategory[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n const [q, setQ] = useState(\"\");\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n const un1 = onSnapshot(collection(db, \"sites\", siteId, \"products\"), (snap) =>\n setProducts(\n snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as Product) }))\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\")),\n ),\n );\n const un2 = onSnapshot(collection(db, \"sites\", siteId, \"shopCategories\"), (snap) =>\n setCats(snap.docs.map((d) => ({ id: d.id, ...(d.data() as ShopCategory) }))),\n );\n return () => {\n un1();\n un2();\n };\n }, [siteId]);\n\n async function createProduct() {\n const db = getDb();\n if (!db) return;\n const ref = await addDoc(collection(db, \"sites\", siteId, \"products\"), {\n name: \"Nieuw product\",\n slug: `product-${products.length + 1}`,\n description: \"\",\n categoryIds: [],\n images: [],\n priceCents: 0,\n vatRate: 21,\n options: [],\n variants: [],\n trackStock: false,\n status: \"draft\",\n order: products.length,\n featured: false,\n } satisfies Product);\n setSelected(ref.id);\n }\n\n const filtered = useMemo(() => {\n const needle = q.trim().toLowerCase();\n if (!needle) return products;\n return products.filter((p) => p.name.toLowerCase().includes(needle) || p.slug.includes(needle));\n }, [products, q]);\n\n const current = products.find((p) => p.id === selected) ?? null;\n\n return (\n <div className=\"grid gap-4 md:grid-cols-[300px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">Producten</h3>\n <Button size=\"sm\" onClick={createProduct}>+ Nieuw</Button>\n </div>\n <Input placeholder=\"Zoeken…\" value={q} onChange={(e) => setQ(e.target.value)} className=\"mb-2\" />\n <ul className=\"grid max-h-[60vh] gap-1 overflow-y-auto\">\n {filtered.map((p) => (\n <li key={p.id}>\n <button\n onClick={() => setSelected(p.id)}\n className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm ${\n p.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n {p.images?.[0] && <img src={p.images[0]} alt=\"\" className=\"h-8 w-8 rounded object-cover\" />}\n <span className=\"min-w-0 flex-1 truncate\">{p.name}</span>\n <Badge tone={p.status === \"active\" ? \"green\" : p.status === \"draft\" ? \"amber\" : \"slate\"}>\n {STATUS_OPTIONS.find((s) => s.value === p.status)?.label ?? p.status}\n </Badge>\n </button>\n </li>\n ))}\n {filtered.length === 0 && <li className=\"px-3 py-2 text-sm text-slate-500\">Geen producten.</li>}\n </ul>\n </Card>\n {current ? (\n <ProductDetail\n key={current.id}\n siteId={siteId}\n product={current}\n categories={cats}\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 product.</p>\n </Card>\n )}\n </div>\n );\n}\n\nfunction ProductDetail({\n siteId,\n product,\n categories,\n onDeleted,\n onToast,\n confirm,\n}: {\n siteId: string;\n product: StoredProduct;\n categories: StoredCategory[];\n onDeleted: () => void;\n onToast: (t: { title: string; description?: string; tone?: \"default\" | \"success\" | \"error\" }) => void;\n confirm: (opts: ConfirmOptions) => Promise<boolean>;\n}) {\n const [def, setDef] = useState<Product>(product);\n const [priceInput, setPriceInput] = useState(centsToEuroInput(product.priceCents));\n const [compareInput, setCompareInput] = useState(centsToEuroInput(product.compareAtCents));\n\n const variantKeys = useMemo(() => allVariantKeys(def.options ?? []), [def.options]);\n\n function setOption(i: number, patch: Partial<ProductOption>) {\n setDef((p) => {\n const options = [...(p.options ?? [])];\n options[i] = { ...options[i], ...patch };\n return { ...p, options };\n });\n }\n\n function variantFor(key: string): ProductVariant | undefined {\n return (def.variants ?? []).find((v) => v.key === key);\n }\n\n function patchVariant(key: string, patch: Partial<ProductVariant>) {\n setDef((p) => {\n const variants = [...(p.variants ?? [])];\n const idx = variants.findIndex((v) => v.key === key);\n if (idx >= 0) variants[idx] = { ...variants[idx], ...patch };\n else variants.push({ key, active: true, ...patch });\n return { ...p, variants };\n });\n }\n\n async function save() {\n const db = getDb();\n if (!db) return;\n // Alleen varianten bewaren die iets toevoegen (afwijkende prijs/voorraad/sku of inactief),\n // en geen wees-varianten van verwijderde optiewaarden.\n const validKeys = new Set(variantKeys);\n const cleanVariants = (def.variants ?? []).filter(\n (v) =>\n validKeys.has(v.key) &&\n (v.priceCents !== undefined || v.stock !== undefined || v.sku !== undefined || v.active === false),\n );\n const parsed = ProductSchema.safeParse({ ...def, variants: cleanVariants });\n if (!parsed.success) {\n onToast({ title: \"Ongeldig product\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n await setDoc(doc(db, \"sites\", siteId, \"products\", product.id), parsed.data);\n onToast({ title: \"Product opgeslagen\", tone: \"success\" });\n }\n\n async function remove() {\n const db = getDb();\n if (!db) return;\n const ok = await confirm({\n title: `\"${product.name}\" verwijderen?`,\n description: \"Bestaande bestellingen behouden hun eigen kopie van naam en prijs.\",\n tone: \"danger\",\n });\n if (!ok) return;\n await deleteDoc(doc(db, \"sites\", siteId, \"products\", product.id));\n onDeleted();\n }\n\n return (\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"grid gap-4 md:grid-cols-2\">\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=\"Product-URL: /product?p=<slug>\">\n <Input value={def.slug} onChange={(e) => setDef((p) => ({ ...p, slug: slugify(e.target.value) }))} />\n </Field>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-3\">\n <Field label=\"Prijs (€, incl. btw)\">\n <Input\n value={priceInput}\n onChange={(e) => {\n setPriceInput(e.target.value);\n const c = eurosToCents(e.target.value);\n if (c !== null) setDef((p) => ({ ...p, priceCents: c }));\n }}\n />\n </Field>\n <Field label=\"Van-prijs (€, optioneel)\" hint=\"Doorgestreept naast de prijs\">\n <Input\n value={compareInput}\n onChange={(e) => {\n setCompareInput(e.target.value);\n const c = eurosToCents(e.target.value);\n setDef((p) => ({ ...p, compareAtCents: c ?? undefined }));\n }}\n />\n </Field>\n <Field label=\"Btw-tarief\">\n <Select\n value={String(def.vatRate ?? 21)}\n onValueChange={(v) => setDef((p) => ({ ...p, vatRate: Number(v) as 21 | 9 | 0 }))}\n options={[\n { value: \"21\", label: \"21%\" },\n { value: \"9\", label: \"9%\" },\n { value: \"0\", label: \"0%\" },\n ]}\n />\n </Field>\n </div>\n\n <Field label=\"Omschrijving\">\n <Textarea rows={4} value={def.description ?? \"\"} onChange={(e) => setDef((p) => ({ ...p, description: e.target.value }))} />\n </Field>\n\n <Field label=\"Afbeeldingen\" hint=\"Eerste afbeelding is het hoofdbeeld\">\n <div className=\"flex flex-wrap items-center gap-3\">\n {(def.images ?? []).map((url, i) => (\n <div key={url + i} className=\"relative\">\n <img src={url} alt=\"\" className=\"h-16 w-16 rounded-lg object-cover\" />\n <button\n type=\"button\"\n aria-label=\"Verwijder afbeelding\"\n className=\"absolute -right-2 -top-2 grid h-5 w-5 place-items-center rounded-full bg-slate-700 text-xs text-white\"\n onClick={() => setDef((p) => ({ ...p, images: (p.images ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </button>\n </div>\n ))}\n <MediaPickerButton\n siteId={siteId}\n label=\"+ Afbeelding\"\n onSelect={(m) => setDef((p) => ({ ...p, images: [...(p.images ?? []), m.url] }))}\n />\n </div>\n </Field>\n\n <Field label=\"Categorieën\">\n <div className=\"flex flex-wrap gap-2\">\n {categories.map((c) => {\n const on = (def.categoryIds ?? []).includes(c.id);\n return (\n <button\n key={c.id}\n type=\"button\"\n onClick={() =>\n setDef((p) => ({\n ...p,\n categoryIds: on ? (p.categoryIds ?? []).filter((x) => x !== c.id) : [...(p.categoryIds ?? []), c.id],\n }))\n }\n className={`rounded-full border px-3 py-1 text-sm ${\n on ? \"border-brand-500 bg-brand-50 text-brand-700\" : \"border-slate-200 hover:bg-slate-50\"\n }`}\n >\n {on ? \"✓ \" : \"\"}\n {c.name}\n </button>\n );\n })}\n {categories.length === 0 && <span className=\"text-sm text-slate-500\">Maak eerst een categorie aan.</span>}\n </div>\n </Field>\n\n {/* Opties & varianten */}\n <Field\n label=\"Opties (bv. Maat, Kleur)\"\n hint=\"Max. 3. Waarden gescheiden door komma's. Combinaties kun je hieronder per stuk aanpassen.\"\n >\n <div className=\"grid gap-2\">\n {(def.options ?? []).map((opt, i) => (\n <div key={i} className=\"flex items-center gap-2\">\n <Input\n className=\"w-36\"\n placeholder=\"Naam (bv. Maat)\"\n value={opt.name}\n onChange={(e) => setOption(i, { name: e.target.value })}\n />\n <Input\n placeholder=\"Waarden, bv: 56, 62, 68, 74\"\n value={opt.values.join(\", \")}\n onChange={(e) =>\n setOption(i, { values: e.target.value.split(\",\").map((s) => s.trim()).filter(Boolean) })\n }\n />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setDef((p) => ({ ...p, options: (p.options ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </Button>\n </div>\n ))}\n {(def.options ?? []).length < 3 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n onClick={() => setDef((p) => ({ ...p, options: [...(p.options ?? []), { name: \"\", values: [] }] }))}\n >\n + Optie\n </Button>\n )}\n </div>\n </Field>\n\n {variantKeys.length > 0 && variantKeys.length <= 60 && (\n <Field label=\"Varianten\" hint=\"Leeg = standaardprijs / onbeperkte voorraad\">\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"text-left text-xs text-slate-500\">\n <th className=\"py-1 pr-2\">Combinatie</th>\n <th className=\"py-1 pr-2\">Prijs (€)</th>\n {def.trackStock && <th className=\"py-1 pr-2\">Voorraad</th>}\n <th className=\"py-1 pr-2\">Leverbaar</th>\n </tr>\n </thead>\n <tbody>\n {variantKeys.map((key) => {\n const v = variantFor(key);\n return (\n <tr key={key} className=\"border-t border-slate-100\">\n <td className=\"py-1 pr-2\">{key.split(\"|\").join(\" · \")}</td>\n <td className=\"py-1 pr-2\">\n {/* Native uncontrolled inputs: geen cursor-gedoe bij \"13,\" tijdens typen. */}\n <input\n className=\"w-24 rounded-lg border border-slate-200 px-2 py-1\"\n placeholder={centsToEuroInput(def.priceCents)}\n defaultValue={centsToEuroInput(v?.priceCents)}\n onChange={(e) => {\n const c = eurosToCents(e.target.value);\n patchVariant(key, { priceCents: c ?? undefined });\n }}\n />\n </td>\n {def.trackStock && (\n <td className=\"py-1 pr-2\">\n <input\n className=\"w-20 rounded-lg border border-slate-200 px-2 py-1\"\n type=\"number\"\n min={0}\n placeholder=\"∞\"\n defaultValue={v?.stock ?? \"\"}\n onChange={(e) => {\n const n = e.target.value === \"\" ? undefined : Math.max(0, Number(e.target.value) || 0);\n patchVariant(key, { stock: n });\n }}\n />\n </td>\n )}\n <td className=\"py-1 pr-2\">\n <Switch checked={v?.active !== false} onCheckedChange={(on) => patchVariant(key, { active: on })} />\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n </Field>\n )}\n {variantKeys.length > 60 && (\n <p className=\"text-sm text-amber-600\">\n {variantKeys.length} combinaties — te veel om per stuk te tonen. Beperk het aantal optie-waarden.\n </p>\n )}\n\n <div className=\"flex flex-wrap items-center gap-6\">\n <Field label=\"Status\">\n <Select\n value={def.status ?? \"draft\"}\n onValueChange={(v) => setDef((p) => ({ ...p, status: v as Product[\"status\"] }))}\n options={STATUS_OPTIONS}\n />\n </Field>\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.featured === true} onCheckedChange={(v) => setDef((p) => ({ ...p, featured: v }))} label=\"Uitgelicht\" />\n <Switch checked={def.trackStock === true} onCheckedChange={(v) => setDef((p) => ({ ...p, trackStock: v }))} label=\"Voorraad bijhouden\" />\n {def.trackStock && (def.options ?? []).length === 0 && (\n <Field label=\"Voorraad\">\n <Input\n type=\"number\"\n className=\"w-24\"\n placeholder=\"∞\"\n value={def.stock ?? \"\"}\n onChange={(e) =>\n setDef((p) => ({ ...p, stock: e.target.value === \"\" ? undefined : Math.max(0, Number(e.target.value) || 0) }))\n }\n />\n </Field>\n )}\n </div>\n\n <div className=\"flex items-center justify-between border-t border-slate-100 pt-4\">\n <div className=\"flex items-center gap-3\">\n <Button onClick={save}>Opslaan</Button>\n <span className=\"text-sm text-slate-500\">{formatCents(def.priceCents)}</span>\n </div>\n <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,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
|
4
|
+
import { ProductsTab } from "./ProductsTab";
|
|
5
|
+
import { CategoriesTab } from "./CategoriesTab";
|
|
6
|
+
import { OrdersTab } from "./OrdersTab";
|
|
7
|
+
import { ConnectionsTab } from "./ConnectionsTab";
|
|
8
|
+
function ShopPanel({ siteId }) {
|
|
9
|
+
return /* @__PURE__ */ jsxs(Tabs, { defaultValue: "orders", children: [
|
|
10
|
+
/* @__PURE__ */ jsxs(TabsList, { children: [
|
|
11
|
+
/* @__PURE__ */ jsx(TabsTrigger, { value: "orders", children: "Bestellingen" }),
|
|
12
|
+
/* @__PURE__ */ jsx(TabsTrigger, { value: "products", children: "Producten" }),
|
|
13
|
+
/* @__PURE__ */ jsx(TabsTrigger, { value: "categories", children: "Categorie\xEBn" }),
|
|
14
|
+
/* @__PURE__ */ jsx(TabsTrigger, { value: "connections", children: "Koppelingen" })
|
|
15
|
+
] }),
|
|
16
|
+
/* @__PURE__ */ jsx(TabsContent, { value: "orders", children: /* @__PURE__ */ jsx(OrdersTab, { siteId }) }),
|
|
17
|
+
/* @__PURE__ */ jsx(TabsContent, { value: "products", children: /* @__PURE__ */ jsx(ProductsTab, { siteId }) }),
|
|
18
|
+
/* @__PURE__ */ jsx(TabsContent, { value: "categories", children: /* @__PURE__ */ jsx(CategoriesTab, { siteId }) }),
|
|
19
|
+
/* @__PURE__ */ jsx(TabsContent, { value: "connections", children: /* @__PURE__ */ jsx(ConnectionsTab, { siteId }) })
|
|
20
|
+
] });
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
ShopPanel
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=ShopPanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/shop/ShopPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"../ui/tabs\";\nimport { ProductsTab } from \"./ProductsTab\";\nimport { CategoriesTab } from \"./CategoriesTab\";\nimport { OrdersTab } from \"./OrdersTab\";\nimport { ConnectionsTab } from \"./ConnectionsTab\";\n\n/** Webwinkel-beheer: producten, categorieën, bestellingen en koppelingen (Stripe/SMTP). */\nexport function ShopPanel({ siteId }: { siteId: string }) {\n return (\n <Tabs defaultValue=\"orders\">\n <TabsList>\n <TabsTrigger value=\"orders\">Bestellingen</TabsTrigger>\n <TabsTrigger value=\"products\">Producten</TabsTrigger>\n <TabsTrigger value=\"categories\">Categorieën</TabsTrigger>\n <TabsTrigger value=\"connections\">Koppelingen</TabsTrigger>\n </TabsList>\n <TabsContent value=\"orders\">\n <OrdersTab siteId={siteId} />\n </TabsContent>\n <TabsContent value=\"products\">\n <ProductsTab siteId={siteId} />\n </TabsContent>\n <TabsContent value=\"categories\">\n <CategoriesTab siteId={siteId} />\n </TabsContent>\n <TabsContent value=\"connections\">\n <ConnectionsTab siteId={siteId} />\n </TabsContent>\n </Tabs>\n );\n}\n"],"mappings":";AAYM,SACE,KADF;AAVN,SAAS,MAAM,aAAa,UAAU,mBAAmB;AACzD,SAAS,mBAAmB;AAC5B,SAAS,qBAAqB;AAC9B,SAAS,iBAAiB;AAC1B,SAAS,sBAAsB;AAGxB,SAAS,UAAU,EAAE,OAAO,GAAuB;AACxD,SACE,qBAAC,QAAK,cAAa,UACjB;AAAA,yBAAC,YACC;AAAA,0BAAC,eAAY,OAAM,UAAS,0BAAY;AAAA,MACxC,oBAAC,eAAY,OAAM,YAAW,uBAAS;AAAA,MACvC,oBAAC,eAAY,OAAM,cAAa,4BAAW;AAAA,MAC3C,oBAAC,eAAY,OAAM,eAAc,yBAAW;AAAA,OAC9C;AAAA,IACA,oBAAC,eAAY,OAAM,UACjB,8BAAC,aAAU,QAAgB,GAC7B;AAAA,IACA,oBAAC,eAAY,OAAM,YACjB,8BAAC,eAAY,QAAgB,GAC/B;AAAA,IACA,oBAAC,eAAY,OAAM,cACjB,8BAAC,iBAAc,QAAgB,GACjC;AAAA,IACA,oBAAC,eAAY,OAAM,eACjB,8BAAC,kBAAe,QAAgB,GAClC;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Callable-helper voor de shop-beheerfuncties (saveShopSecrets, getShopStatus, …). */
|
|
2
|
+
export declare function shopCallable<TIn, TOut>(name: string): (data: TIn) => Promise<TOut>;
|
|
3
|
+
/** "13,95" / "13.95" → 1395. Ongeldig → null. */
|
|
4
|
+
export declare function eurosToCents(input: string): number | null;
|
|
5
|
+
/** 1395 → "13,95" (voor invoervelden; weergave met €-teken doet formatCents). */
|
|
6
|
+
export declare function centsToEuroInput(cents: number | undefined): string;
|
|
7
|
+
/** Cartesisch product van optie-waarden → alle variant-keys ("68|Roze"). */
|
|
8
|
+
export declare function allVariantKeys(options: {
|
|
9
|
+
name: string;
|
|
10
|
+
values: string[];
|
|
11
|
+
}[]): string[];
|
|
12
|
+
/** Statusweergave (NL-labels + badge-tint) voor bestellingen. */
|
|
13
|
+
export declare const ORDER_STATUS_LABELS: Record<string, {
|
|
14
|
+
label: string;
|
|
15
|
+
tone: "slate" | "green" | "amber";
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { getFunctions, httpsCallable } from "firebase/functions";
|
|
3
|
+
import { getFirebaseApp } from "../../firebase/client";
|
|
4
|
+
import { functionsRegion } from "../../firebase/region";
|
|
5
|
+
function shopCallable(name) {
|
|
6
|
+
return async (data) => {
|
|
7
|
+
const app = getFirebaseApp();
|
|
8
|
+
if (!app) throw new Error("Firebase niet beschikbaar.");
|
|
9
|
+
const fn = httpsCallable(getFunctions(app, functionsRegion()), name);
|
|
10
|
+
const res = await fn(data);
|
|
11
|
+
return res.data;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function eurosToCents(input) {
|
|
15
|
+
const cleaned = input.trim().replace(/[€\s]/g, "").replace(",", ".");
|
|
16
|
+
if (cleaned === "") return null;
|
|
17
|
+
const n = Number(cleaned);
|
|
18
|
+
if (!Number.isFinite(n) || n < 0) return null;
|
|
19
|
+
return Math.round(n * 100);
|
|
20
|
+
}
|
|
21
|
+
function centsToEuroInput(cents) {
|
|
22
|
+
if (cents === void 0) return "";
|
|
23
|
+
return (cents / 100).toFixed(2).replace(".", ",");
|
|
24
|
+
}
|
|
25
|
+
function allVariantKeys(options) {
|
|
26
|
+
if (options.length === 0) return [];
|
|
27
|
+
return options.reduce(
|
|
28
|
+
(acc, opt) => acc.flatMap((prefix) => opt.values.map((v) => prefix ? `${prefix}|${v}` : v)),
|
|
29
|
+
[""]
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const ORDER_STATUS_LABELS = {
|
|
33
|
+
pending: { label: "Wacht op betaling", tone: "amber" },
|
|
34
|
+
paid: { label: "Betaald", tone: "green" },
|
|
35
|
+
shipped: { label: "Verzonden", tone: "green" },
|
|
36
|
+
cancelled: { label: "Geannuleerd", tone: "slate" },
|
|
37
|
+
refunded: { label: "Terugbetaald", tone: "slate" }
|
|
38
|
+
};
|
|
39
|
+
export {
|
|
40
|
+
ORDER_STATUS_LABELS,
|
|
41
|
+
allVariantKeys,
|
|
42
|
+
centsToEuroInput,
|
|
43
|
+
eurosToCents,
|
|
44
|
+
shopCallable
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=lib.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/shop/lib.ts"],"sourcesContent":["\"use client\";\n\nimport { getFunctions, httpsCallable } from \"firebase/functions\";\nimport { getFirebaseApp } from \"../../firebase/client\";\nimport { functionsRegion } from \"../../firebase/region\";\n\n/** Callable-helper voor de shop-beheerfuncties (saveShopSecrets, getShopStatus, …). */\nexport function shopCallable<TIn, TOut>(name: string) {\n return async (data: TIn): Promise<TOut> => {\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet beschikbaar.\");\n const fn = httpsCallable(getFunctions(app, functionsRegion()), name);\n const res = await fn(data as Record<string, unknown>);\n return res.data as TOut;\n };\n}\n\n/** \"13,95\" / \"13.95\" → 1395. Ongeldig → null. */\nexport function eurosToCents(input: string): number | null {\n const cleaned = input.trim().replace(/[€\\s]/g, \"\").replace(\",\", \".\");\n if (cleaned === \"\") return null;\n const n = Number(cleaned);\n if (!Number.isFinite(n) || n < 0) return null;\n return Math.round(n * 100);\n}\n\n/** 1395 → \"13,95\" (voor invoervelden; weergave met €-teken doet formatCents). */\nexport function centsToEuroInput(cents: number | undefined): string {\n if (cents === undefined) return \"\";\n return (cents / 100).toFixed(2).replace(\".\", \",\");\n}\n\n/** Cartesisch product van optie-waarden → alle variant-keys (\"68|Roze\"). */\nexport function allVariantKeys(options: { name: string; values: string[] }[]): string[] {\n if (options.length === 0) return [];\n return options.reduce<string[]>(\n (acc, opt) => acc.flatMap((prefix) => opt.values.map((v) => (prefix ? `${prefix}|${v}` : v))),\n [\"\"],\n );\n}\n\n/** Statusweergave (NL-labels + badge-tint) voor bestellingen. */\nexport const ORDER_STATUS_LABELS: Record<string, { label: string; tone: \"slate\" | \"green\" | \"amber\" }> = {\n pending: { label: \"Wacht op betaling\", tone: \"amber\" },\n paid: { label: \"Betaald\", tone: \"green\" },\n shipped: { label: \"Verzonden\", tone: \"green\" },\n cancelled: { label: \"Geannuleerd\", tone: \"slate\" },\n refunded: { label: \"Terugbetaald\", tone: \"slate\" },\n};\n"],"mappings":";AAEA,SAAS,cAAc,qBAAqB;AAC5C,SAAS,sBAAsB;AAC/B,SAAS,uBAAuB;AAGzB,SAAS,aAAwB,MAAc;AACpD,SAAO,OAAO,SAA6B;AACzC,UAAM,MAAM,eAAe;AAC3B,QAAI,CAAC,IAAK,OAAM,IAAI,MAAM,4BAA4B;AACtD,UAAM,KAAK,cAAc,aAAa,KAAK,gBAAgB,CAAC,GAAG,IAAI;AACnE,UAAM,MAAM,MAAM,GAAG,IAA+B;AACpD,WAAO,IAAI;AAAA,EACb;AACF;AAGO,SAAS,aAAa,OAA8B;AACzD,QAAM,UAAU,MAAM,KAAK,EAAE,QAAQ,UAAU,EAAE,EAAE,QAAQ,KAAK,GAAG;AACnE,MAAI,YAAY,GAAI,QAAO;AAC3B,QAAM,IAAI,OAAO,OAAO;AACxB,MAAI,CAAC,OAAO,SAAS,CAAC,KAAK,IAAI,EAAG,QAAO;AACzC,SAAO,KAAK,MAAM,IAAI,GAAG;AAC3B;AAGO,SAAS,iBAAiB,OAAmC;AAClE,MAAI,UAAU,OAAW,QAAO;AAChC,UAAQ,QAAQ,KAAK,QAAQ,CAAC,EAAE,QAAQ,KAAK,GAAG;AAClD;AAGO,SAAS,eAAe,SAAyD;AACtF,MAAI,QAAQ,WAAW,EAAG,QAAO,CAAC;AAClC,SAAO,QAAQ;AAAA,IACb,CAAC,KAAK,QAAQ,IAAI,QAAQ,CAAC,WAAW,IAAI,OAAO,IAAI,CAAC,MAAO,SAAS,GAAG,MAAM,IAAI,CAAC,KAAK,CAAE,CAAC;AAAA,IAC5F,CAAC,EAAE;AAAA,EACL;AACF;AAGO,MAAM,sBAA4F;AAAA,EACvG,SAAS,EAAE,OAAO,qBAAqB,MAAM,QAAQ;AAAA,EACrD,MAAM,EAAE,OAAO,WAAW,MAAM,QAAQ;AAAA,EACxC,SAAS,EAAE,OAAO,aAAa,MAAM,QAAQ;AAAA,EAC7C,WAAW,EAAE,OAAO,eAAe,MAAM,QAAQ;AAAA,EACjD,UAAU,EAAE,OAAO,gBAAgB,MAAM,QAAQ;AACnD;","names":[]}
|