@xiwebdesign/xicms-core 7.17.1 → 7.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/settings/MailSettings.js +41 -0
- package/dist/admin/settings/MailSettings.js.map +1 -1
- package/dist/admin/shop/CategoriesTab.js +27 -5
- package/dist/admin/shop/CategoriesTab.js.map +1 -1
- package/dist/admin/shop/DiscountsTab.js +9 -1
- package/dist/admin/shop/DiscountsTab.js.map +1 -1
- package/dist/admin/shop/OrdersTab.js +287 -11
- package/dist/admin/shop/OrdersTab.js.map +1 -1
- package/dist/admin/shop/ProductsTab.d.ts +3 -1
- package/dist/admin/shop/ProductsTab.js +164 -88
- package/dist/admin/shop/ProductsTab.js.map +1 -1
- package/dist/admin.css +1 -1
- package/dist/modules/shop/blocks/ProductDetail.js +1 -1
- package/dist/modules/shop/blocks/ProductDetail.js.map +1 -1
- package/dist/modules/shop/settings.d.ts +1 -0
- package/dist/modules/shop/settings.js +6 -0
- package/dist/modules/shop/settings.js.map +1 -1
- package/dist/schema/shop.d.ts +16 -0
- package/dist/schema/shop.js +49 -4
- package/dist/schema/shop.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useMemo, useState } from "react";
|
|
4
|
-
import { collection, limit, onSnapshot, orderBy, query } from "firebase/firestore";
|
|
4
|
+
import { collection, getDocs, limit, onSnapshot, orderBy, query, where } from "firebase/firestore";
|
|
5
5
|
import { getDb } from "../../firebase/client.js";
|
|
6
6
|
import { formatCents } from "../../schema/shop.js";
|
|
7
|
-
import { Badge, Button, Card, Input } from "../ui/primitives.js";
|
|
7
|
+
import { Badge, Button, Card, Field, Input, Textarea } from "../ui/primitives.js";
|
|
8
8
|
import { Select } from "../ui/select.js";
|
|
9
9
|
import { Switch } from "../ui/switch.js";
|
|
10
10
|
import { useToast } from "../ui/toast.js";
|
|
11
11
|
import { useConfirm } from "../ui/ConfirmDialog.js";
|
|
12
12
|
import { Dialog, DialogContent, DialogClose } from "../ui/dialog.js";
|
|
13
|
-
import { ORDER_STATUS_LABELS, shopCallable } from "./lib.js";
|
|
13
|
+
import { ORDER_STATUS_LABELS, centsToEuroInput, eurosToCents, shopCallable } from "./lib.js";
|
|
14
14
|
const FILTERS = [
|
|
15
15
|
{ value: "all", label: "Alles" },
|
|
16
16
|
{ value: "pending", label: "Wacht op betaling" },
|
|
@@ -37,6 +37,27 @@ function OrdersTab({ siteId }) {
|
|
|
37
37
|
const [busy, setBusy] = useState(false);
|
|
38
38
|
const [invoice, setInvoice] = useState(null);
|
|
39
39
|
const [shipOpen, setShipOpen] = useState(false);
|
|
40
|
+
const [mailOpen, setMailOpen] = useState(false);
|
|
41
|
+
const [mailSubject, setMailSubject] = useState("");
|
|
42
|
+
const [mailBody, setMailBody] = useState("");
|
|
43
|
+
const [mailCopy, setMailCopy] = useState(true);
|
|
44
|
+
const [refundOpen, setRefundOpen] = useState(false);
|
|
45
|
+
const [refundInput, setRefundInput] = useState("");
|
|
46
|
+
const [noteInput, setNoteInput] = useState("");
|
|
47
|
+
const [manualOpen, setManualOpen] = useState(false);
|
|
48
|
+
const [manual, setManual] = useState({ name: "", email: "", phone: "", address: "", zip: "", city: "", shipping: "", note: "", paid: true, notify: false });
|
|
49
|
+
const [manualLines, setManualLines] = useState([]);
|
|
50
|
+
const [producten, setProducten] = useState([]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!manualOpen || producten.length > 0) return;
|
|
53
|
+
const db = getDb();
|
|
54
|
+
if (!db) return;
|
|
55
|
+
void getDocs(query(collection(db, "sites", siteId, "products"), where("status", "==", "active"))).then(
|
|
56
|
+
(snap) => setProducten(
|
|
57
|
+
snap.docs.map((d) => ({ id: d.id, ...d.data() })).sort((a, b) => a.name.localeCompare(b.name, "nl"))
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
}, [manualOpen, producten.length, siteId]);
|
|
40
61
|
const [trackingUrl, setTrackingUrl] = useState("");
|
|
41
62
|
const [notify, setNotify] = useState(true);
|
|
42
63
|
useEffect(() => {
|
|
@@ -72,6 +93,10 @@ function OrdersTab({ siteId }) {
|
|
|
72
93
|
return list;
|
|
73
94
|
}, [orders, filter, search]);
|
|
74
95
|
const current = orders.find((o) => o.id === selected) ?? null;
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
const o = orders.find((x) => x.id === selected);
|
|
98
|
+
setNoteInput(o?.adminNote ?? "");
|
|
99
|
+
}, [selected, orders]);
|
|
75
100
|
async function setStatus(order, status) {
|
|
76
101
|
setBusy(true);
|
|
77
102
|
try {
|
|
@@ -95,24 +120,123 @@ function OrdersTab({ siteId }) {
|
|
|
95
120
|
setBusy(false);
|
|
96
121
|
}
|
|
97
122
|
}
|
|
98
|
-
|
|
123
|
+
function openstaand(order) {
|
|
124
|
+
return Math.max(0, order.amount.totalCents - (order.refundedCents ?? 0));
|
|
125
|
+
}
|
|
126
|
+
async function refund(order, bedragCents) {
|
|
127
|
+
const rest = openstaand(order);
|
|
128
|
+
const bedrag = bedragCents ?? rest;
|
|
129
|
+
const providerNaam = (order.provider ?? (order.mollie?.paymentId ? "mollie" : "stripe")) === "mollie" ? "Mollie" : "Stripe";
|
|
130
|
+
const volledig = bedrag >= rest;
|
|
99
131
|
const ok = await confirm({
|
|
100
|
-
title: `Bestelling ${order.number} terugbetalen?`,
|
|
101
|
-
description: `Het
|
|
132
|
+
title: volledig ? `Bestelling ${order.number} terugbetalen?` : `${formatCents(bedrag)} terugbetalen?`,
|
|
133
|
+
description: volledig ? `Het openstaande bedrag (${formatCents(rest)}) wordt via ${providerNaam} teruggestort; de klant krijgt een e-mail.` : `Er wordt ${formatCents(bedrag)} van ${formatCents(order.amount.totalCents)} via ${providerNaam} teruggestort. De bestelling blijft verder gewoon staan.`,
|
|
102
134
|
tone: "danger",
|
|
103
135
|
confirmLabel: "Terugbetalen"
|
|
104
136
|
});
|
|
105
137
|
if (!ok) return;
|
|
106
138
|
setBusy(true);
|
|
107
139
|
try {
|
|
108
|
-
await shopCallable("refundOrder")({
|
|
109
|
-
|
|
140
|
+
await shopCallable("refundOrder")({
|
|
141
|
+
siteId,
|
|
142
|
+
orderId: order.id,
|
|
143
|
+
...volledig ? {} : { amountCents: bedrag }
|
|
144
|
+
});
|
|
145
|
+
toast({ title: volledig ? "Terugbetaling gestart" : `${formatCents(bedrag)} teruggestort`, tone: "success" });
|
|
146
|
+
setRefundOpen(false);
|
|
147
|
+
setRefundInput("");
|
|
110
148
|
} catch (e) {
|
|
111
149
|
toast({ title: "Terugbetalen mislukt", description: e instanceof Error ? e.message : void 0, tone: "error" });
|
|
112
150
|
} finally {
|
|
113
151
|
setBusy(false);
|
|
114
152
|
}
|
|
115
153
|
}
|
|
154
|
+
async function mailKlant(order) {
|
|
155
|
+
if (!mailSubject.trim() || !mailBody.trim()) return;
|
|
156
|
+
setBusy(true);
|
|
157
|
+
try {
|
|
158
|
+
await shopCallable("sendCustomerMail")({
|
|
159
|
+
siteId,
|
|
160
|
+
orderId: order.id,
|
|
161
|
+
subject: mailSubject.trim(),
|
|
162
|
+
body: mailBody.trim(),
|
|
163
|
+
copyToShop: mailCopy
|
|
164
|
+
});
|
|
165
|
+
toast({ title: "Bericht verstuurd", description: order.customer.email, tone: "success" });
|
|
166
|
+
setMailOpen(false);
|
|
167
|
+
setMailSubject("");
|
|
168
|
+
setMailBody("");
|
|
169
|
+
} catch (e) {
|
|
170
|
+
toast({ title: "Versturen mislukt", description: e instanceof Error ? e.message : void 0, tone: "error" });
|
|
171
|
+
} finally {
|
|
172
|
+
setBusy(false);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function createManual() {
|
|
176
|
+
const regels = manualLines.filter((l) => l.productId && l.qty > 0);
|
|
177
|
+
if (!manual.name.trim() || !manual.email.trim() || regels.length === 0) {
|
|
178
|
+
toast({ title: "Vul naam, e-mail en minstens \xE9\xE9n product in", tone: "error" });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
setBusy(true);
|
|
182
|
+
try {
|
|
183
|
+
const res = await shopCallable("createManualOrder")({
|
|
184
|
+
siteId,
|
|
185
|
+
items: regels.map((l) => ({ productId: l.productId, variantKey: l.variantKey || void 0, qty: l.qty })),
|
|
186
|
+
customer: { name: manual.name.trim(), email: manual.email.trim(), phone: manual.phone.trim() || void 0 },
|
|
187
|
+
shipping: manual.address.trim() ? { address: manual.address.trim(), zip: manual.zip.trim(), city: manual.city.trim(), country: "NL" } : void 0,
|
|
188
|
+
shippingCents: eurosToCents(manual.shipping) ?? 0,
|
|
189
|
+
note: manual.note.trim() || void 0,
|
|
190
|
+
alBetaald: manual.paid,
|
|
191
|
+
notify: manual.notify
|
|
192
|
+
});
|
|
193
|
+
toast({ title: "Bestelling aangemaakt", description: manual.paid ? "Geboekt als betaald." : "Staat open.", tone: "success" });
|
|
194
|
+
setManualOpen(false);
|
|
195
|
+
setManualLines([]);
|
|
196
|
+
setManual({ name: "", email: "", phone: "", address: "", zip: "", city: "", shipping: "", note: "", paid: true, notify: false });
|
|
197
|
+
setSelected(res.id);
|
|
198
|
+
} catch (e) {
|
|
199
|
+
toast({ title: "Aanmaken mislukt", description: e instanceof Error ? e.message : void 0, tone: "error" });
|
|
200
|
+
} finally {
|
|
201
|
+
setBusy(false);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function markPaid(order) {
|
|
205
|
+
const ok = await confirm({
|
|
206
|
+
title: `Bestelling ${order.number} als betaald boeken?`,
|
|
207
|
+
description: "Gebruik dit als je het geld buiten de webwinkel om hebt ontvangen (overschrijving, contant, pin). De voorraad wordt afgeboekt en de factuur wordt gemaakt.",
|
|
208
|
+
confirmLabel: "Als betaald boeken"
|
|
209
|
+
});
|
|
210
|
+
if (!ok) return;
|
|
211
|
+
setBusy(true);
|
|
212
|
+
try {
|
|
213
|
+
await shopCallable("markOrderPaidManually")({
|
|
214
|
+
siteId,
|
|
215
|
+
orderId: order.id,
|
|
216
|
+
notify
|
|
217
|
+
});
|
|
218
|
+
toast({ title: "Geboekt als betaald", tone: "success" });
|
|
219
|
+
} catch (e) {
|
|
220
|
+
toast({ title: "Mislukt", description: e instanceof Error ? e.message : void 0, tone: "error" });
|
|
221
|
+
} finally {
|
|
222
|
+
setBusy(false);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function saveNote(order) {
|
|
226
|
+
setBusy(true);
|
|
227
|
+
try {
|
|
228
|
+
await shopCallable("setOrderNote")({
|
|
229
|
+
siteId,
|
|
230
|
+
orderId: order.id,
|
|
231
|
+
note: noteInput
|
|
232
|
+
});
|
|
233
|
+
toast({ title: "Notitie opgeslagen", tone: "success" });
|
|
234
|
+
} catch (e) {
|
|
235
|
+
toast({ title: "Opslaan mislukt", description: e instanceof Error ? e.message : void 0, tone: "error" });
|
|
236
|
+
} finally {
|
|
237
|
+
setBusy(false);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
116
240
|
async function resendMail(order) {
|
|
117
241
|
setBusy(true);
|
|
118
242
|
try {
|
|
@@ -191,7 +315,10 @@ function OrdersTab({ siteId }) {
|
|
|
191
315
|
/* @__PURE__ */ jsxs(Card, { className: "h-fit", children: [
|
|
192
316
|
/* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between gap-2", children: [
|
|
193
317
|
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "Bestellingen" }),
|
|
194
|
-
/* @__PURE__ */
|
|
318
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
319
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => setManualOpen(true), children: "+ Handmatig" }),
|
|
320
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: exportCsv, disabled: orders.length === 0, children: "CSV" })
|
|
321
|
+
] })
|
|
195
322
|
] }),
|
|
196
323
|
/* @__PURE__ */ jsx(Input, { placeholder: "Zoek op nummer, naam of e-mail\u2026", value: search, onChange: (e) => setSearch(e.target.value), className: "mb-2" }),
|
|
197
324
|
/* @__PURE__ */ jsx(Select, { value: filter, onValueChange: setFilter, options: FILTERS, className: "mb-2" }),
|
|
@@ -241,7 +368,19 @@ function OrdersTab({ siteId }) {
|
|
|
241
368
|
/* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
|
|
242
369
|
current.customer.name,
|
|
243
370
|
/* @__PURE__ */ jsx("br", {}),
|
|
244
|
-
/* @__PURE__ */ jsx(
|
|
371
|
+
/* @__PURE__ */ jsx(
|
|
372
|
+
"button",
|
|
373
|
+
{
|
|
374
|
+
type: "button",
|
|
375
|
+
className: "text-brand-700 underline",
|
|
376
|
+
title: "Mail deze klant vanuit de winkel (in huisstijl, met een kopie bij de bestelling)",
|
|
377
|
+
onClick: () => {
|
|
378
|
+
setMailSubject(`Over je bestelling ${current.number}`);
|
|
379
|
+
setMailOpen(true);
|
|
380
|
+
},
|
|
381
|
+
children: current.customer.email
|
|
382
|
+
}
|
|
383
|
+
),
|
|
245
384
|
current.customer.phone && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
246
385
|
/* @__PURE__ */ jsx("br", {}),
|
|
247
386
|
current.customer.phone
|
|
@@ -267,6 +406,27 @@ function OrdersTab({ siteId }) {
|
|
|
267
406
|
/* @__PURE__ */ jsx("h4", { className: "mb-1 text-sm font-semibold", children: "Opmerking van de klant" }),
|
|
268
407
|
/* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap text-sm", children: current.note })
|
|
269
408
|
] }),
|
|
409
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
410
|
+
/* @__PURE__ */ jsx("h4", { className: "mb-1 text-sm font-semibold", children: "Interne notitie" }),
|
|
411
|
+
/* @__PURE__ */ jsx(
|
|
412
|
+
Textarea,
|
|
413
|
+
{
|
|
414
|
+
rows: 2,
|
|
415
|
+
value: noteInput,
|
|
416
|
+
placeholder: "Alleen zichtbaar in het beheer\u2026",
|
|
417
|
+
onChange: (e) => setNoteInput(e.target.value)
|
|
418
|
+
}
|
|
419
|
+
),
|
|
420
|
+
noteInput !== (current.adminNote ?? "") && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", className: "mt-2", disabled: busy, onClick: () => void saveNote(current), children: "Notitie opslaan" })
|
|
421
|
+
] }),
|
|
422
|
+
(current.messages?.length ?? 0) > 0 && /* @__PURE__ */ jsxs("div", { children: [
|
|
423
|
+
/* @__PURE__ */ jsx("h4", { className: "mb-1 text-sm font-semibold", children: "Verstuurde berichten" }),
|
|
424
|
+
/* @__PURE__ */ jsx("ul", { className: "grid gap-2", children: current.messages.map((m, i) => /* @__PURE__ */ jsxs("li", { className: "rounded-lg border border-slate-200 p-2 text-sm", children: [
|
|
425
|
+
/* @__PURE__ */ jsx("p", { className: "font-medium text-slate-800", children: m.subject }),
|
|
426
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-slate-400", children: new Date(m.at).toLocaleString("nl-NL", { dateStyle: "short", timeStyle: "short" }) }),
|
|
427
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 whitespace-pre-wrap text-slate-600", children: m.body })
|
|
428
|
+
] }, i)) })
|
|
429
|
+
] }),
|
|
270
430
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
271
431
|
/* @__PURE__ */ jsx("h4", { className: "mb-1 text-sm font-semibold", children: "Regels" }),
|
|
272
432
|
/* @__PURE__ */ jsx("table", { className: "w-full text-sm", children: /* @__PURE__ */ jsxs("tbody", { children: [
|
|
@@ -338,6 +498,7 @@ function OrdersTab({ siteId }) {
|
|
|
338
498
|
] }),
|
|
339
499
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 border-t border-slate-100 pt-4", children: [
|
|
340
500
|
current.status === "paid" && !shipOpen && /* @__PURE__ */ jsx(Button, { size: "sm", onClick: () => setShipOpen(true), children: "Markeer als verzonden" }),
|
|
501
|
+
current.status === "pending" && /* @__PURE__ */ jsx(Button, { size: "sm", disabled: busy, onClick: () => void markPaid(current), children: "Als betaald boeken" }),
|
|
341
502
|
["pending", "paid"].includes(current.status) && /* @__PURE__ */ jsx(
|
|
342
503
|
Button,
|
|
343
504
|
{
|
|
@@ -354,9 +515,16 @@ function OrdersTab({ siteId }) {
|
|
|
354
515
|
children: "Annuleer"
|
|
355
516
|
}
|
|
356
517
|
),
|
|
518
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", disabled: busy, onClick: () => {
|
|
519
|
+
setMailSubject(`Over je bestelling ${current.number}`);
|
|
520
|
+
setMailOpen(true);
|
|
521
|
+
}, children: "\u2709 Klant mailen" }),
|
|
357
522
|
(current.status === "paid" || current.status === "shipped") && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
358
523
|
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", disabled: busy, onClick: () => resendMail(current), children: "Mail opnieuw versturen" }),
|
|
359
|
-
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "danger", disabled: busy
|
|
524
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "danger", disabled: busy || openstaand(current) === 0, onClick: () => {
|
|
525
|
+
setRefundInput(centsToEuroInput(openstaand(current)));
|
|
526
|
+
setRefundOpen(true);
|
|
527
|
+
}, children: "Terugbetalen" })
|
|
360
528
|
] }),
|
|
361
529
|
current.invoicePath && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", disabled: busy, onClick: () => viewInvoice(current, current.invoicePath), children: "Factuur bekijken" }),
|
|
362
530
|
current.stripe?.paymentIntentId && /* @__PURE__ */ jsx(
|
|
@@ -372,6 +540,114 @@ function OrdersTab({ siteId }) {
|
|
|
372
540
|
] })
|
|
373
541
|
] }) }) : /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Selecteer een bestelling." }) })
|
|
374
542
|
] }),
|
|
543
|
+
/* @__PURE__ */ jsx(Dialog, { open: manualOpen, onOpenChange: setManualOpen, children: /* @__PURE__ */ jsx(DialogContent, { title: "Bestelling handmatig aanmaken", width: "min(92vw, 44rem)", children: /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
|
|
544
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Voor een bestelling die buiten de webwinkel om binnenkwam. Prijzen en btw komen uit je productenlijst, dus de omzet en de voorraad blijven kloppen." }),
|
|
545
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-3 md:grid-cols-3", children: [
|
|
546
|
+
/* @__PURE__ */ jsx(Field, { label: "Naam", children: /* @__PURE__ */ jsx(Input, { value: manual.name, onChange: (e) => setManual((p) => ({ ...p, name: e.target.value })) }) }),
|
|
547
|
+
/* @__PURE__ */ jsx(Field, { label: "E-mail", children: /* @__PURE__ */ jsx(Input, { value: manual.email, onChange: (e) => setManual((p) => ({ ...p, email: e.target.value })) }) }),
|
|
548
|
+
/* @__PURE__ */ jsx(Field, { label: "Telefoon (optioneel)", children: /* @__PURE__ */ jsx(Input, { value: manual.phone, onChange: (e) => setManual((p) => ({ ...p, phone: e.target.value })) }) })
|
|
549
|
+
] }),
|
|
550
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
551
|
+
/* @__PURE__ */ jsx("p", { className: "mb-1 text-sm font-medium", children: "Producten" }),
|
|
552
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
|
|
553
|
+
manualLines.map((l, i) => {
|
|
554
|
+
const prod = producten.find((p) => p.id === l.productId);
|
|
555
|
+
const varianten = (prod?.options?.length ?? 0) > 0 ? prod?.variants ?? [] : [];
|
|
556
|
+
return /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[1fr_150px_70px_auto] items-center gap-2", children: [
|
|
557
|
+
/* @__PURE__ */ jsx(
|
|
558
|
+
Select,
|
|
559
|
+
{
|
|
560
|
+
value: l.productId,
|
|
561
|
+
onValueChange: (v) => setManualLines((p) => p.map((x, j) => j === i ? { ...x, productId: v, variantKey: "" } : x)),
|
|
562
|
+
placeholder: "Kies product\u2026",
|
|
563
|
+
options: producten.map((p) => ({ value: p.id, label: p.name }))
|
|
564
|
+
}
|
|
565
|
+
),
|
|
566
|
+
varianten.length > 0 ? /* @__PURE__ */ jsx(
|
|
567
|
+
Select,
|
|
568
|
+
{
|
|
569
|
+
value: l.variantKey,
|
|
570
|
+
onValueChange: (v) => setManualLines((p) => p.map((x, j) => j === i ? { ...x, variantKey: v } : x)),
|
|
571
|
+
placeholder: "Uitvoering\u2026",
|
|
572
|
+
options: varianten.map((v) => ({ value: v.key, label: v.key.split("|").join(", ") }))
|
|
573
|
+
}
|
|
574
|
+
) : /* @__PURE__ */ jsx("span", { className: "text-xs text-slate-400", children: "\u2014" }),
|
|
575
|
+
/* @__PURE__ */ jsx(
|
|
576
|
+
Input,
|
|
577
|
+
{
|
|
578
|
+
type: "number",
|
|
579
|
+
value: String(l.qty),
|
|
580
|
+
onChange: (e) => setManualLines((p) => p.map((x, j) => j === i ? { ...x, qty: Math.max(1, Number(e.target.value) || 1) } : x))
|
|
581
|
+
}
|
|
582
|
+
),
|
|
583
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: () => setManualLines((p) => p.filter((_, j) => j !== i)), children: "\u2715" })
|
|
584
|
+
] }, i);
|
|
585
|
+
}),
|
|
586
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: () => setManualLines((p) => [...p, { productId: "", variantKey: "", qty: 1 }]), children: "+ Regel" })
|
|
587
|
+
] })
|
|
588
|
+
] }),
|
|
589
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-3 md:grid-cols-4", children: [
|
|
590
|
+
/* @__PURE__ */ jsx(Field, { label: "Adres", children: /* @__PURE__ */ jsx(Input, { value: manual.address, onChange: (e) => setManual((p) => ({ ...p, address: e.target.value })) }) }),
|
|
591
|
+
/* @__PURE__ */ jsx(Field, { label: "Postcode", children: /* @__PURE__ */ jsx(Input, { value: manual.zip, onChange: (e) => setManual((p) => ({ ...p, zip: e.target.value })) }) }),
|
|
592
|
+
/* @__PURE__ */ jsx(Field, { label: "Plaats", children: /* @__PURE__ */ jsx(Input, { value: manual.city, onChange: (e) => setManual((p) => ({ ...p, city: e.target.value })) }) }),
|
|
593
|
+
/* @__PURE__ */ jsx(Field, { label: "Verzendkosten (\u20AC)", children: /* @__PURE__ */ jsx(Input, { value: manual.shipping, onChange: (e) => setManual((p) => ({ ...p, shipping: e.target.value })), placeholder: "0,00" }) })
|
|
594
|
+
] }),
|
|
595
|
+
/* @__PURE__ */ jsx(Field, { label: "Notitie (optioneel)", children: /* @__PURE__ */ jsx(Input, { value: manual.note, onChange: (e) => setManual((p) => ({ ...p, note: e.target.value })), placeholder: "bv. telefonisch besteld, contant betaald" }) }),
|
|
596
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-4", children: [
|
|
597
|
+
/* @__PURE__ */ jsx(Switch, { checked: manual.paid, onCheckedChange: (v) => setManual((p) => ({ ...p, paid: v })), label: "Al betaald (voorraad afboeken + factuur maken)" }),
|
|
598
|
+
/* @__PURE__ */ jsx(Switch, { checked: manual.notify, onCheckedChange: (v) => setManual((p) => ({ ...p, notify: v })), label: "Bevestigingsmail naar de klant" })
|
|
599
|
+
] }),
|
|
600
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
|
|
601
|
+
/* @__PURE__ */ jsx(DialogClose, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "secondary", children: "Annuleren" }) }),
|
|
602
|
+
/* @__PURE__ */ jsx(Button, { disabled: busy, onClick: () => void createManual(), children: busy ? "Bezig\u2026" : "Bestelling aanmaken" })
|
|
603
|
+
] })
|
|
604
|
+
] }) }) }),
|
|
605
|
+
/* @__PURE__ */ jsx(Dialog, { open: mailOpen, onOpenChange: setMailOpen, children: /* @__PURE__ */ jsx(DialogContent, { title: current ? `Mail ${current.customer.name}` : "Klant mailen", width: "min(92vw, 34rem)", children: /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
|
|
606
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-slate-500", children: [
|
|
607
|
+
"Gaat naar ",
|
|
608
|
+
/* @__PURE__ */ jsx("strong", { children: current?.customer.email }),
|
|
609
|
+
" in de huisstijl van je site. Antwoorden komen binnen op je notificatie-adres."
|
|
610
|
+
] }),
|
|
611
|
+
/* @__PURE__ */ jsx(Field, { label: "Onderwerp", children: /* @__PURE__ */ jsx(Input, { value: mailSubject, onChange: (e) => setMailSubject(e.target.value) }) }),
|
|
612
|
+
/* @__PURE__ */ jsx(Field, { label: "Bericht", children: /* @__PURE__ */ jsx(Textarea, { rows: 7, value: mailBody, onChange: (e) => setMailBody(e.target.value), placeholder: "Beste \u2026" }) }),
|
|
613
|
+
/* @__PURE__ */ jsx(Switch, { checked: mailCopy, onCheckedChange: setMailCopy, label: "Kopie naar mijzelf" }),
|
|
614
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
|
|
615
|
+
/* @__PURE__ */ jsx(DialogClose, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "secondary", children: "Annuleren" }) }),
|
|
616
|
+
/* @__PURE__ */ jsx(Button, { disabled: busy || !mailSubject.trim() || !mailBody.trim(), onClick: () => current && void mailKlant(current), children: busy ? "Bezig\u2026" : "Versturen" })
|
|
617
|
+
] })
|
|
618
|
+
] }) }) }),
|
|
619
|
+
/* @__PURE__ */ jsx(Dialog, { open: refundOpen, onOpenChange: setRefundOpen, children: /* @__PURE__ */ jsx(DialogContent, { title: current ? `Terugbetalen \u2014 ${current.number}` : "Terugbetalen", width: "min(92vw, 26rem)", children: /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
|
|
620
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-slate-500", children: [
|
|
621
|
+
"Openstaand: ",
|
|
622
|
+
/* @__PURE__ */ jsx("strong", { children: current ? formatCents(openstaand(current)) : "\u2014" }),
|
|
623
|
+
current && (current.refundedCents ?? 0) > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
624
|
+
" (er is al ",
|
|
625
|
+
formatCents(current.refundedCents),
|
|
626
|
+
" terugbetaald)"
|
|
627
|
+
] })
|
|
628
|
+
] }),
|
|
629
|
+
/* @__PURE__ */ jsx(Field, { label: "Bedrag (\u20AC)", hint: "Laat het volledige bedrag staan voor een hele terugbetaling", children: /* @__PURE__ */ jsx(Input, { value: refundInput, onChange: (e) => setRefundInput(e.target.value), className: "w-36" }) }),
|
|
630
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
|
|
631
|
+
/* @__PURE__ */ jsx(DialogClose, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "secondary", children: "Annuleren" }) }),
|
|
632
|
+
/* @__PURE__ */ jsx(
|
|
633
|
+
Button,
|
|
634
|
+
{
|
|
635
|
+
variant: "danger",
|
|
636
|
+
disabled: busy,
|
|
637
|
+
onClick: () => {
|
|
638
|
+
if (!current) return;
|
|
639
|
+
const cents = eurosToCents(refundInput);
|
|
640
|
+
if (cents === null || cents <= 0) {
|
|
641
|
+
toast({ title: "Vul een geldig bedrag in", tone: "error" });
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
void refund(current, Math.min(cents, openstaand(current)));
|
|
645
|
+
},
|
|
646
|
+
children: busy ? "Bezig\u2026" : "Terugbetalen"
|
|
647
|
+
}
|
|
648
|
+
)
|
|
649
|
+
] })
|
|
650
|
+
] }) }) }),
|
|
375
651
|
/* @__PURE__ */ jsx(Dialog, { open: !!invoice, onOpenChange: (o) => !o && setInvoice(null), children: invoice && /* @__PURE__ */ jsxs(DialogContent, { title: `Factuur ${invoice.number}`, width: "min(92vw, 48rem)", children: [
|
|
376
652
|
/* @__PURE__ */ jsx(
|
|
377
653
|
"iframe",
|