@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.
@@ -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
- async function refund(order) {
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 volledige bedrag (${formatCents(order.amount.totalCents)}) wordt via Stripe teruggestort; de klant krijgt een e-mail.`,
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")({ siteId, orderId: order.id });
109
- toast({ title: "Terugbetaling gestart", tone: "success" });
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__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: exportCsv, disabled: orders.length === 0, children: "CSV" })
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("a", { className: "text-brand-700 underline", href: `mailto:${current.customer.email}`, children: current.customer.email }),
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, onClick: () => refund(current), children: "Terugbetalen" })
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",