@withpotter/components 0.13.0-next.82 → 0.13.0-next.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/dashboard/activity-feed.d.ts +2 -0
  2. package/dist/dashboard/activity-feed.js +3 -1
  3. package/dist/dashboard/activity-feed.js.map +1 -1
  4. package/dist/dashboard/activity-paid.d.ts +10 -0
  5. package/dist/dashboard/activity-paid.js +7 -0
  6. package/dist/dashboard/activity-paid.js.map +1 -0
  7. package/dist/dashboard/home/ask-controls.js +1 -0
  8. package/dist/dashboard/home/ask-controls.js.map +1 -1
  9. package/dist/dashboard/home/ask-mode-chip.js +4 -4
  10. package/dist/dashboard/home/ask-mode-chip.js.map +1 -1
  11. package/dist/dashboard/home/home-ask.js +3 -3
  12. package/dist/dashboard/home/home-ask.js.map +1 -1
  13. package/dist/dashboard/home/home-quick-actions.js +9 -1
  14. package/dist/dashboard/home/home-quick-actions.js.map +1 -1
  15. package/dist/dashboard/home/host-quick-actions.d.ts +19 -0
  16. package/dist/dashboard/home/host-quick-actions.js +12 -0
  17. package/dist/dashboard/home/host-quick-actions.js.map +1 -0
  18. package/dist/dashboard/home/widgets/activity-widget.js +56 -43
  19. package/dist/dashboard/home/widgets/activity-widget.js.map +1 -1
  20. package/dist/dashboard/home/widgets/home-widgets.js +3 -2
  21. package/dist/dashboard/home/widgets/home-widgets.js.map +1 -1
  22. package/dist/dashboard/home/widgets/host-widgets.d.ts +6 -0
  23. package/dist/dashboard/home/widgets/host-widgets.js +16 -0
  24. package/dist/dashboard/home/widgets/host-widgets.js.map +1 -0
  25. package/dist/dashboard/home/widgets/member-label.d.ts +11 -0
  26. package/dist/dashboard/home/widgets/member-label.js +10 -0
  27. package/dist/dashboard/home/widgets/member-label.js.map +1 -0
  28. package/dist/dashboard/home/widgets/middle-ellipsis.d.ts +5 -0
  29. package/dist/dashboard/home/widgets/middle-ellipsis.js +11 -0
  30. package/dist/dashboard/home/widgets/middle-ellipsis.js.map +1 -0
  31. package/dist/dashboard/home/widgets/money-widgets.js +9 -4
  32. package/dist/dashboard/home/widgets/money-widgets.js.map +1 -1
  33. package/dist/dashboard/home/widgets/registry.d.ts +6 -3
  34. package/dist/dashboard/home/widgets/registry.js +12 -2
  35. package/dist/dashboard/home/widgets/registry.js.map +1 -1
  36. package/dist/dashboard/home/widgets/revenue-series.d.ts +12 -0
  37. package/dist/dashboard/home/widgets/revenue-series.js +26 -0
  38. package/dist/dashboard/home/widgets/revenue-series.js.map +1 -0
  39. package/dist/dashboard/home/widgets/team-widget.js +2 -1
  40. package/dist/dashboard/home/widgets/team-widget.js.map +1 -1
  41. package/dist/dashboard/home/widgets/types.d.ts +13 -1
  42. package/dist/dashboard/home/widgets/types.js.map +1 -1
  43. package/dist/dashboard/home/widgets/use-home-layout.js +9 -4
  44. package/dist/dashboard/home/widgets/use-home-layout.js.map +1 -1
  45. package/dist/dashboard/home/widgets/website-widget.js +9 -1
  46. package/dist/dashboard/home/widgets/website-widget.js.map +1 -1
  47. package/dist/features/gift-card-artwork.js +1 -1
  48. package/dist/features/gift-card-artwork.js.map +1 -1
  49. package/dist/features/gift-cards/gift-card-logic.d.ts +41 -0
  50. package/dist/features/gift-cards/gift-card-logic.js +65 -0
  51. package/dist/features/gift-cards/gift-card-logic.js.map +1 -0
  52. package/dist/features/gift-cards/gift-cards-feature.js +656 -518
  53. package/dist/features/gift-cards/gift-cards-feature.js.map +1 -1
  54. package/dist/features/gift-cards/hint.d.ts +9 -0
  55. package/dist/features/gift-cards/hint.js +25 -0
  56. package/dist/features/gift-cards/hint.js.map +1 -0
  57. package/dist/features/product-form.js +1 -1
  58. package/dist/features/product-form.js.map +1 -1
  59. package/dist/sidebar/nav-renderers.js +2 -0
  60. package/dist/sidebar/nav-renderers.js.map +1 -1
  61. package/dist/sidebar/nav-section.js +5 -3
  62. package/dist/sidebar/nav-section.js.map +1 -1
  63. package/package.json +4 -4
@@ -1,21 +1,20 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useRef, useState } from "react";
4
- import {
5
- Copy,
6
- Download,
7
- Gift,
8
- History,
9
- Mail,
10
- Plus,
11
- Search,
12
- X
13
- } from "lucide-react";
3
+ import { useCallback, useEffect, useId, useRef, useState } from "react";
4
+ import { Copy, Download, Gift, History, Mail, Plus, Search } from "lucide-react";
14
5
  import { useAuth } from "@withpotter/lib/auth-context";
15
6
  import { normalizeCurrencyCode } from "@withpotter/lib/utils";
7
+ import { Button } from "@withpotter/ui-kit/button";
8
+ import { Field } from "@withpotter/ui-kit/field";
9
+ import { FormDialog } from "@withpotter/ui-kit/form-dialog";
16
10
  import { InlineDeleteButton } from "@withpotter/ui-kit/inline-delete-button";
11
+ import { Input } from "@withpotter/ui-kit/input";
12
+ import { ListPage } from "@withpotter/ui-kit/list-page";
13
+ import { Textarea } from "@withpotter/ui-kit/textarea";
14
+ import { Tooltip } from "@withpotter/ui-kit/tooltip";
17
15
  import {
18
16
  giftCardsApi,
17
+ giftcardsApi,
19
18
  communicationsApi
20
19
  } from "@withpotter/lib/api";
21
20
  import { useFormatCurrency } from "@withpotter/hooks/use-format-currency";
@@ -34,6 +33,15 @@ import {
34
33
  GiftCardArtwork,
35
34
  GIFT_CARD_PRESETS
36
35
  } from "../gift-card-artwork";
36
+ import {
37
+ createInFlightDeduper,
38
+ currencySymbol,
39
+ isFirstRun,
40
+ PAGE_HINT_LINES,
41
+ STATUS_FILTERS,
42
+ STATUS_LABEL
43
+ } from "./gift-card-logic";
44
+ import { Hint } from "./hint";
37
45
  const PAGE_SIZE = 20;
38
46
  const LIVE = process.env.NEXT_PUBLIC_BOX_GIFT_CARDS_LIVE === "true";
39
47
  const LIVE_REFRESH_MS = 2e3;
@@ -65,24 +73,6 @@ function statusClass(status) {
65
73
  return "bg-[var(--bg-muted)] text-[var(--text-tertiary)]";
66
74
  }
67
75
  }
68
- function Modal({ title, onClose, children }) {
69
- return /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-end justify-center bg-black/40 sm:items-center sm:p-4", children: /* @__PURE__ */ jsxs("div", { className: "max-h-[92vh] w-full overflow-y-auto bg-[var(--bg-base)] sm:max-w-xl sm:rounded-md sm:border sm:border-[var(--border-default)]", children: [
70
- /* @__PURE__ */ jsxs("div", { className: "sticky top-0 z-10 flex items-center justify-between border-b border-[var(--border-default)] bg-[var(--bg-base)] px-4 py-3", children: [
71
- /* @__PURE__ */ jsx("h2", { className: "text-base font-semibold text-[var(--text-primary)]", children: title }),
72
- /* @__PURE__ */ jsx(
73
- "button",
74
- {
75
- type: "button",
76
- onClick: onClose,
77
- className: "rounded-md p-2 text-[var(--text-tertiary)] hover:bg-[var(--interactive-hover)]",
78
- "aria-label": "Close",
79
- children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
80
- }
81
- )
82
- ] }),
83
- children
84
- ] }) });
85
- }
86
76
  function IssueGiftCardModal({
87
77
  currency,
88
78
  merchantName,
@@ -90,6 +80,16 @@ function IssueGiftCardModal({
90
80
  onCreated
91
81
  }) {
92
82
  const { tenant } = useAuth();
83
+ const formId = useId();
84
+ const ids = {
85
+ amount: `${formId}-amount`,
86
+ recipientName: `${formId}-recipient-name`,
87
+ recipientEmail: `${formId}-recipient-email`,
88
+ purchaserName: `${formId}-purchaser-name`,
89
+ purchaserEmail: `${formId}-purchaser-email`,
90
+ message: `${formId}-message`,
91
+ expiresAt: `${formId}-expires-at`
92
+ };
93
93
  const [form, setForm] = useState({
94
94
  originalAmount: 0,
95
95
  currency,
@@ -115,178 +115,233 @@ function IssueGiftCardModal({
115
115
  setSaving(false);
116
116
  }
117
117
  };
118
- const inputClass = "w-full rounded-md border border-[var(--border-default)] bg-[var(--bg-base)] px-3 py-2 text-sm text-[var(--text-primary)] focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20";
119
- return /* @__PURE__ */ jsx(Modal, { title: "Issue gift card", onClose, children: /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "space-y-5 p-4", children: [
120
- /* @__PURE__ */ jsxs("div", { className: "grid gap-4 sm:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] sm:items-start", children: [
121
- /* @__PURE__ */ jsx(
122
- GiftCardArtwork,
123
- {
124
- design: form.design ?? GIFT_CARD_PRESETS[0],
125
- merchantName,
126
- amount: form.originalAmount > 0 ? new Intl.NumberFormat(void 0, {
127
- style: "currency",
128
- currency: form.currency || currency
129
- }).format(form.originalAmount) : void 0,
130
- recipientName: form.recipientName,
131
- senderName: form.purchaserName
132
- }
133
- ),
134
- /* @__PURE__ */ jsxs("fieldset", { children: [
135
- /* @__PURE__ */ jsx("legend", { className: "text-sm font-medium text-[var(--text-secondary)]", children: "Design" }),
136
- /* @__PURE__ */ jsx("div", { className: "mt-2 grid grid-cols-4 gap-2 sm:grid-cols-1", children: GIFT_CARD_PRESETS.map((design) => {
137
- const selected = form.design?.designId === design.designId;
138
- return /* @__PURE__ */ jsxs(
139
- "button",
140
- {
141
- type: "button",
142
- onClick: () => setField("design", design),
143
- className: `flex min-w-0 items-center gap-2 rounded-md border p-2 text-left text-xs font-medium ${selected ? "border-brand-500 bg-brand-500/10 text-[var(--text-primary)]" : "border-[var(--border-default)] text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)]"}`,
144
- "aria-label": design.name,
145
- "aria-pressed": selected,
146
- children: [
147
- /* @__PURE__ */ jsx(
148
- "span",
149
- {
150
- className: "h-5 w-5 shrink-0 rounded-sm border border-black/10",
151
- style: { backgroundColor: design.palette.background }
152
- }
153
- ),
154
- /* @__PURE__ */ jsx("span", { className: "hidden truncate sm:block", children: design.name })
155
- ]
156
- },
157
- design.designId
158
- );
159
- }) })
160
- ] })
161
- ] }),
162
- /* @__PURE__ */ jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [
163
- /* @__PURE__ */ jsxs("label", { className: "space-y-1.5", children: [
164
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-[var(--text-secondary)]", children: "Amount" }),
165
- /* @__PURE__ */ jsx(
166
- "input",
167
- {
168
- className: inputClass,
169
- type: "number",
170
- min: "0.01",
171
- step: "0.01",
172
- required: true,
173
- value: form.originalAmount || "",
174
- onChange: (event) => setField("originalAmount", Number(event.target.value))
175
- }
176
- )
177
- ] }),
178
- /* @__PURE__ */ jsxs("label", { className: "space-y-1.5", children: [
179
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-[var(--text-secondary)]", children: "Currency" }),
180
- /* @__PURE__ */ jsx(
181
- "input",
182
- {
183
- className: inputClass,
184
- readOnly: true,
185
- required: true,
186
- value: form.currency
187
- }
188
- )
189
- ] }),
190
- /* @__PURE__ */ jsxs("label", { className: "space-y-1.5", children: [
191
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-[var(--text-secondary)]", children: "Recipient name" }),
192
- /* @__PURE__ */ jsx(
193
- "input",
194
- {
195
- className: inputClass,
196
- value: form.recipientName ?? "",
197
- onChange: (event) => setField("recipientName", event.target.value)
198
- }
199
- ),
200
- /* @__PURE__ */ jsx("span", { className: "block text-xs text-[var(--text-muted)]", children: "The person who receives the gift card and spends it." })
201
- ] }),
202
- /* @__PURE__ */ jsxs("label", { className: "space-y-1.5", children: [
203
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-[var(--text-secondary)]", children: "Recipient email" }),
118
+ const symbol = currencySymbol(form.currency || currency);
119
+ return /* @__PURE__ */ jsx(
120
+ FormDialog,
121
+ {
122
+ title: "Issue gift card",
123
+ description: "We create the code for you. Fields marked * are required.",
124
+ size: "lg",
125
+ className: "max-w-4xl",
126
+ onClose,
127
+ footer: /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
128
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", onClick: onClose, children: "Cancel" }),
204
129
  /* @__PURE__ */ jsx(
205
- "input",
130
+ Button,
206
131
  {
207
- className: inputClass,
208
- type: "email",
209
- value: form.recipientEmail ?? "",
210
- onChange: (event) => setField("recipientEmail", event.target.value)
132
+ type: "submit",
133
+ form: formId,
134
+ disabled: saving || form.originalAmount <= 0,
135
+ children: saving ? "Issuing..." : "Issue gift card"
211
136
  }
212
137
  )
213
138
  ] }),
214
- /* @__PURE__ */ jsxs("label", { className: "space-y-1.5", children: [
215
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-[var(--text-secondary)]", children: "Purchaser name" }),
216
- /* @__PURE__ */ jsx(
217
- "input",
218
- {
219
- className: inputClass,
220
- value: form.purchaserName ?? "",
221
- onChange: (event) => setField("purchaserName", event.target.value)
222
- }
223
- ),
224
- /* @__PURE__ */ jsx("span", { className: "block text-xs text-[var(--text-muted)]", children: "The person paying for the gift card." })
225
- ] }),
226
- /* @__PURE__ */ jsxs("label", { className: "space-y-1.5", children: [
227
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-[var(--text-secondary)]", children: "Purchaser email" }),
228
- /* @__PURE__ */ jsx(
229
- "input",
230
- {
231
- className: inputClass,
232
- type: "email",
233
- value: form.purchaserEmail ?? "",
234
- onChange: (event) => setField("purchaserEmail", event.target.value)
235
- }
236
- )
237
- ] })
238
- ] }),
239
- /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
240
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-[var(--text-secondary)]", children: "Personal message" }),
241
- /* @__PURE__ */ jsx(
242
- "textarea",
243
- {
244
- className: `${inputClass} min-h-24 resize-y`,
245
- maxLength: 500,
246
- value: form.personalMessage ?? "",
247
- onChange: (event) => setField("personalMessage", event.target.value)
248
- }
249
- )
250
- ] }),
251
- /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
252
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-[var(--text-secondary)]", children: "Expiration date" }),
253
- /* @__PURE__ */ jsx(
254
- "input",
139
+ children: /* @__PURE__ */ jsxs(
140
+ "form",
255
141
  {
256
- className: inputClass,
257
- type: "date",
258
- min: minimumExpirationDate(),
259
- value: form.expiresAt ?? "",
260
- onChange: (event) => setField("expiresAt", event.target.value)
261
- }
262
- )
263
- ] }),
264
- error && /* @__PURE__ */ jsx("p", { className: "rounded-md bg-[var(--error-bg)] px-3 py-2 text-sm text-[var(--error)]", children: error }),
265
- /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 border-t border-[var(--border-default)] pt-4", children: [
266
- /* @__PURE__ */ jsx(
267
- "button",
268
- {
269
- type: "button",
270
- onClick: onClose,
271
- className: "rounded-md border border-[var(--border-default)] px-4 py-2 text-sm font-medium text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)]",
272
- children: "Cancel"
273
- }
274
- ),
275
- /* @__PURE__ */ jsx(
276
- "button",
277
- {
278
- type: "submit",
279
- disabled: saving || form.originalAmount <= 0,
280
- className: "rounded-md bg-brand-500 px-4 py-2 text-sm font-medium text-[var(--text-inverse)] hover:bg-brand-600 disabled:opacity-50",
281
- children: saving ? "Issuing..." : "Issue gift card"
142
+ id: formId,
143
+ onSubmit: submit,
144
+ className: "grid gap-6 md:grid-cols-[17rem_minmax(0,1fr)]",
145
+ children: [
146
+ /* @__PURE__ */ jsxs("div", { className: "space-y-4 md:self-start", children: [
147
+ /* @__PURE__ */ jsx(
148
+ GiftCardArtwork,
149
+ {
150
+ design: form.design ?? GIFT_CARD_PRESETS[0],
151
+ merchantName,
152
+ amount: form.originalAmount > 0 ? new Intl.NumberFormat(void 0, {
153
+ style: "currency",
154
+ currency: form.currency || currency
155
+ }).format(form.originalAmount) : void 0,
156
+ recipientName: form.recipientName,
157
+ senderName: form.purchaserName
158
+ }
159
+ ),
160
+ /* @__PURE__ */ jsxs("fieldset", { children: [
161
+ /* @__PURE__ */ jsx("legend", { className: "text-sm font-medium text-[var(--text-secondary)]", children: "Design" }),
162
+ /* @__PURE__ */ jsx("div", { className: "mt-2 grid grid-cols-4 gap-2 md:grid-cols-2", children: GIFT_CARD_PRESETS.map((design) => {
163
+ const selected = form.design?.designId === design.designId;
164
+ return /* @__PURE__ */ jsxs(
165
+ "button",
166
+ {
167
+ type: "button",
168
+ onClick: () => setField("design", design),
169
+ className: `flex min-w-0 items-center gap-2 rounded-md border p-2 text-left text-xs font-medium ${selected ? "border-brand-500 bg-brand-500/10 text-[var(--text-primary)]" : "border-[var(--border-default)] text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)]"}`,
170
+ "aria-label": design.name,
171
+ "aria-pressed": selected,
172
+ children: [
173
+ /* @__PURE__ */ jsx(
174
+ "span",
175
+ {
176
+ className: "h-5 w-5 shrink-0 rounded-sm border border-black/10",
177
+ style: { backgroundColor: design.palette.background }
178
+ }
179
+ ),
180
+ /* @__PURE__ */ jsx("span", { className: "hidden truncate md:block", children: design.name })
181
+ ]
182
+ },
183
+ design.designId
184
+ );
185
+ }) })
186
+ ] })
187
+ ] }),
188
+ /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
189
+ /* @__PURE__ */ jsx(
190
+ Field,
191
+ {
192
+ label: "Amount",
193
+ htmlFor: ids.amount,
194
+ required: true,
195
+ hint: `In ${form.currency || currency}, your business currency. It cannot be changed here.`,
196
+ children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
197
+ /* @__PURE__ */ jsx(
198
+ "span",
199
+ {
200
+ "aria-hidden": true,
201
+ className: "pointer-events-none absolute inset-y-0 left-3 flex items-center text-sm text-[var(--text-tertiary)]",
202
+ children: symbol
203
+ }
204
+ ),
205
+ /* @__PURE__ */ jsx(
206
+ Input,
207
+ {
208
+ id: ids.amount,
209
+ type: "number",
210
+ min: "0.01",
211
+ step: "0.01",
212
+ required: true,
213
+ "aria-required": true,
214
+ className: symbol.length > 2 ? "pl-14" : "pl-8",
215
+ value: form.originalAmount || "",
216
+ onChange: (event) => setField("originalAmount", Number(event.target.value))
217
+ }
218
+ )
219
+ ] })
220
+ }
221
+ ),
222
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [
223
+ /* @__PURE__ */ jsx(
224
+ Field,
225
+ {
226
+ label: "Recipient name",
227
+ htmlFor: ids.recipientName,
228
+ hint: "Optional. Who receives and spends the card.",
229
+ children: /* @__PURE__ */ jsx(
230
+ Input,
231
+ {
232
+ id: ids.recipientName,
233
+ value: form.recipientName ?? "",
234
+ onChange: (event) => setField("recipientName", event.target.value)
235
+ }
236
+ )
237
+ }
238
+ ),
239
+ /* @__PURE__ */ jsx(
240
+ Field,
241
+ {
242
+ label: "Recipient email",
243
+ htmlFor: ids.recipientEmail,
244
+ hint: "Optional. Nothing is emailed when you issue. Send it from the card's details.",
245
+ children: /* @__PURE__ */ jsx(
246
+ Input,
247
+ {
248
+ id: ids.recipientEmail,
249
+ type: "email",
250
+ value: form.recipientEmail ?? "",
251
+ onChange: (event) => setField("recipientEmail", event.target.value)
252
+ }
253
+ )
254
+ }
255
+ ),
256
+ /* @__PURE__ */ jsx(
257
+ Field,
258
+ {
259
+ label: "Purchaser name",
260
+ htmlFor: ids.purchaserName,
261
+ hint: "Optional. Who is paying for the card.",
262
+ children: /* @__PURE__ */ jsx(
263
+ Input,
264
+ {
265
+ id: ids.purchaserName,
266
+ value: form.purchaserName ?? "",
267
+ onChange: (event) => setField("purchaserName", event.target.value)
268
+ }
269
+ )
270
+ }
271
+ ),
272
+ /* @__PURE__ */ jsx(
273
+ Field,
274
+ {
275
+ label: "Purchaser email",
276
+ htmlFor: ids.purchaserEmail,
277
+ hint: "Optional.",
278
+ children: /* @__PURE__ */ jsx(
279
+ Input,
280
+ {
281
+ id: ids.purchaserEmail,
282
+ type: "email",
283
+ value: form.purchaserEmail ?? "",
284
+ onChange: (event) => setField("purchaserEmail", event.target.value)
285
+ }
286
+ )
287
+ }
288
+ )
289
+ ] }),
290
+ /* @__PURE__ */ jsx(
291
+ Field,
292
+ {
293
+ label: "Personal message",
294
+ htmlFor: ids.message,
295
+ hint: "Optional. Up to 500 characters.",
296
+ children: /* @__PURE__ */ jsx(
297
+ Textarea,
298
+ {
299
+ id: ids.message,
300
+ rows: 3,
301
+ maxLength: 500,
302
+ value: form.personalMessage ?? "",
303
+ onChange: (event) => setField("personalMessage", event.target.value)
304
+ }
305
+ )
306
+ }
307
+ ),
308
+ /* @__PURE__ */ jsx(
309
+ Field,
310
+ {
311
+ label: "Expiration date",
312
+ htmlFor: ids.expiresAt,
313
+ hint: "Optional. Leave it empty and the card has no expiry date.",
314
+ children: /* @__PURE__ */ jsx(
315
+ Input,
316
+ {
317
+ id: ids.expiresAt,
318
+ type: "date",
319
+ min: minimumExpirationDate(),
320
+ value: form.expiresAt ?? "",
321
+ onChange: (event) => setField("expiresAt", event.target.value)
322
+ }
323
+ )
324
+ }
325
+ ),
326
+ error && /* @__PURE__ */ jsx(
327
+ "p",
328
+ {
329
+ role: "alert",
330
+ className: "rounded-md bg-[var(--error-bg)] px-3 py-2 text-sm text-[var(--error)]",
331
+ children: error
332
+ }
333
+ )
334
+ ] })
335
+ ]
282
336
  }
283
337
  )
284
- ] })
285
- ] }) });
338
+ }
339
+ );
286
340
  }
287
341
  function BalanceModal({ onClose, initialCode = "" }) {
288
342
  const { tenant } = useAuth();
289
343
  const formatCurrency = useFormatCurrency();
344
+ const codeId = useId();
290
345
  const [code, setCode] = useState(initialCode);
291
346
  const [checking, setChecking] = useState(false);
292
347
  const [result, setResult] = useState(null);
@@ -304,35 +359,36 @@ function BalanceModal({ onClose, initialCode = "" }) {
304
359
  setChecking(false);
305
360
  }
306
361
  };
307
- return /* @__PURE__ */ jsx(Modal, { title: "Check gift card balance", onClose, children: /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "space-y-4 p-4", children: [
308
- /* @__PURE__ */ jsxs("label", { className: "block space-y-1.5", children: [
309
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-[var(--text-secondary)]", children: "Gift card code" }),
310
- /* @__PURE__ */ jsx(
311
- "input",
312
- {
313
- autoFocus: true,
314
- className: "w-full rounded-md border border-[var(--border-default)] bg-[var(--bg-base)] px-3 py-2 font-mono text-sm uppercase text-[var(--text-primary)] focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20",
315
- value: code,
316
- onChange: (event) => {
317
- setCode(event.target.value.toUpperCase());
318
- setResult(null);
319
- }
362
+ return /* @__PURE__ */ jsx(FormDialog, { title: "Check gift card balance", size: "sm", onClose, children: /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "space-y-4", children: [
363
+ /* @__PURE__ */ jsx(Field, { label: "Gift card code", htmlFor: codeId, required: true, children: /* @__PURE__ */ jsx(
364
+ Input,
365
+ {
366
+ id: codeId,
367
+ required: true,
368
+ "aria-required": true,
369
+ autoComplete: "off",
370
+ spellCheck: false,
371
+ className: "font-mono uppercase",
372
+ value: code,
373
+ onChange: (event) => {
374
+ setCode(event.target.value.toUpperCase());
375
+ setResult(null);
320
376
  }
321
- )
322
- ] }),
323
- result && /* @__PURE__ */ jsx(
377
+ }
378
+ ) }),
379
+ /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", children: result && /* @__PURE__ */ jsx(
324
380
  "div",
325
381
  {
326
382
  className: `rounded-md p-4 ${result.valid ? "bg-[var(--success-bg)] text-[var(--success)]" : "bg-[var(--error-bg)] text-[var(--error)]"}`,
327
383
  children: result.valid ? `Available balance: ${formatCurrency(result.balance ?? 0)}` : result.error || "This gift card is not valid."
328
384
  }
329
- ),
385
+ ) }),
330
386
  /* @__PURE__ */ jsx(
331
- "button",
387
+ Button,
332
388
  {
333
389
  type: "submit",
390
+ className: "w-full",
334
391
  disabled: checking || !code.trim(),
335
- className: "w-full rounded-md bg-brand-500 px-4 py-2 text-sm font-medium text-[var(--text-inverse)] hover:bg-brand-600 disabled:opacity-50",
336
392
  children: checking ? "Checking..." : "Check balance"
337
393
  }
338
394
  )
@@ -409,7 +465,7 @@ function GiftCardDetailModal({
409
465
  if (!tenant?.slug) return;
410
466
  setSaving(true);
411
467
  try {
412
- await giftCardsApi.delete(tenant.slug, giftCard.id);
468
+ await giftcardsApi.cancel(tenant.slug, giftCard.id);
413
469
  toast({ title: "Gift card cancelled" });
414
470
  onChanged();
415
471
  onClose();
@@ -418,60 +474,63 @@ function GiftCardDetailModal({
418
474
  setSaving(false);
419
475
  }
420
476
  };
421
- return /* @__PURE__ */ jsx(Modal, { title: "Gift card details", onClose, children: /* @__PURE__ */ jsxs("div", { className: "space-y-5 p-4", children: [
477
+ return /* @__PURE__ */ jsx(FormDialog, { title: "Gift card details", onClose, children: /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
422
478
  /* @__PURE__ */ jsx(GiftCardArtwork, { ...artworkData }),
423
479
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
424
- /* @__PURE__ */ jsxs(
425
- "button",
480
+ /* @__PURE__ */ jsx(
481
+ Button,
426
482
  {
427
483
  type: "button",
484
+ variant: "secondary",
485
+ icon: /* @__PURE__ */ jsx(Download, {}),
428
486
  onClick: () => downloadGiftCardArtwork(artworkData),
429
- className: "flex items-center justify-center gap-2 rounded-md border border-[var(--border-default)] px-3 py-2 text-sm font-medium text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)]",
430
- children: [
431
- /* @__PURE__ */ jsx(Download, { className: "h-4 w-4" }),
432
- "Download"
433
- ]
487
+ children: "Download"
434
488
  }
435
489
  ),
436
- /* @__PURE__ */ jsxs(
437
- "button",
490
+ /* @__PURE__ */ jsx(
491
+ Tooltip,
438
492
  {
439
- type: "button",
440
- onClick: sendByEmail,
441
- disabled: sharing || !giftCard.recipientEmail,
442
- title: giftCard.recipientEmail ? void 0 : "Add a recipient email to send this card",
443
- className: "flex items-center justify-center gap-2 rounded-md bg-brand-500 px-3 py-2 text-sm font-medium text-[var(--text-inverse)] hover:bg-brand-600 disabled:cursor-not-allowed disabled:opacity-50",
444
- children: [
445
- /* @__PURE__ */ jsx(Mail, { className: "h-4 w-4" }),
446
- sharing ? "Sending..." : "Send by email"
447
- ]
493
+ label: giftCard.recipientEmail ? "" : "Add a recipient email to send this card",
494
+ children: /* @__PURE__ */ jsx("span", { className: "block", children: /* @__PURE__ */ jsx(
495
+ Button,
496
+ {
497
+ type: "button",
498
+ variant: "secondary",
499
+ icon: /* @__PURE__ */ jsx(Mail, {}),
500
+ className: "w-full",
501
+ onClick: sendByEmail,
502
+ disabled: sharing || !giftCard.recipientEmail,
503
+ children: sharing ? "Sending..." : "Send by email"
504
+ }
505
+ ) })
448
506
  }
449
507
  )
450
508
  ] }),
451
509
  /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-[var(--border-default)] bg-[var(--bg-subtle)] p-4", children: [
452
510
  /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
453
511
  /* @__PURE__ */ jsxs("div", { children: [
454
- /* @__PURE__ */ jsx("p", { className: "text-xs font-medium uppercase text-[var(--text-muted)]", children: "Code" }),
512
+ /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-[var(--text-muted)]", children: "Code" }),
455
513
  /* @__PURE__ */ jsx("p", { className: "mt-1 font-mono text-xl font-semibold text-[var(--text-primary)]", children: giftCard.code })
456
514
  ] }),
457
515
  /* @__PURE__ */ jsx(
458
- "button",
516
+ Button,
459
517
  {
460
518
  type: "button",
519
+ variant: "secondary",
520
+ size: "icon-sm",
521
+ "aria-label": "Copy code",
461
522
  onClick: copyCode,
462
- className: "rounded-md border border-[var(--border-default)] bg-[var(--bg-base)] p-2 text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)]",
463
- title: "Copy code",
464
- children: /* @__PURE__ */ jsx(Copy, { className: "h-4 w-4" })
523
+ children: /* @__PURE__ */ jsx(Copy, {})
465
524
  }
466
525
  )
467
526
  ] }),
468
527
  /* @__PURE__ */ jsxs("div", { className: "mt-4 grid grid-cols-2 gap-4", children: [
469
528
  /* @__PURE__ */ jsxs("div", { children: [
470
- /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: "Balance" }),
529
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: "Balance left" }),
471
530
  /* @__PURE__ */ jsx("p", { className: "font-semibold text-[var(--text-primary)]", children: formatCurrency(Number(giftCard.balance), giftCard.currency) })
472
531
  ] }),
473
532
  /* @__PURE__ */ jsxs("div", { children: [
474
- /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: "Issued value" }),
533
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: "Original value" }),
475
534
  /* @__PURE__ */ jsx("p", { className: "font-semibold text-[var(--text-primary)]", children: formatCurrency(
476
535
  Number(giftCard.originalAmount),
477
536
  giftCard.currency
@@ -491,44 +550,41 @@ function GiftCardDetailModal({
491
550
  /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-[var(--text-primary)]", children: "Redeem value" }),
492
551
  /* @__PURE__ */ jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [
493
552
  /* @__PURE__ */ jsx(
494
- "input",
553
+ Input,
495
554
  {
555
+ label: "Amount",
496
556
  type: "number",
497
557
  min: "0.01",
498
558
  max: Number(giftCard.balance),
499
559
  step: "0.01",
500
560
  required: true,
501
- placeholder: "Amount",
561
+ "aria-required": true,
502
562
  value: amount,
503
- onChange: (event) => setAmount(event.target.value),
504
- className: "rounded-md border border-[var(--border-default)] bg-[var(--bg-base)] px-3 py-2 text-sm focus:border-brand-500 focus:outline-none"
563
+ onChange: (event) => setAmount(event.target.value)
505
564
  }
506
565
  ),
507
566
  /* @__PURE__ */ jsx(
508
- "input",
567
+ Input,
509
568
  {
510
- placeholder: "Order or reference ID",
569
+ label: "Order or reference ID (optional)",
511
570
  value: referenceId,
512
- onChange: (event) => setReferenceId(event.target.value),
513
- className: "rounded-md border border-[var(--border-default)] bg-[var(--bg-base)] px-3 py-2 text-sm focus:border-brand-500 focus:outline-none"
571
+ onChange: (event) => setReferenceId(event.target.value)
514
572
  }
515
573
  )
516
574
  ] }),
517
575
  /* @__PURE__ */ jsx(
518
- "input",
576
+ Input,
519
577
  {
520
- placeholder: "Note (optional)",
578
+ label: "Note (optional)",
521
579
  value: note,
522
- onChange: (event) => setNote(event.target.value),
523
- className: "w-full rounded-md border border-[var(--border-default)] bg-[var(--bg-base)] px-3 py-2 text-sm focus:border-brand-500 focus:outline-none"
580
+ onChange: (event) => setNote(event.target.value)
524
581
  }
525
582
  ),
526
583
  /* @__PURE__ */ jsx(
527
- "button",
584
+ Button,
528
585
  {
529
586
  type: "submit",
530
587
  disabled: saving || !amount || Number(amount) <= 0,
531
- className: "rounded-md bg-brand-500 px-4 py-2 text-sm font-medium text-[var(--text-inverse)] hover:bg-brand-600 disabled:opacity-50",
532
588
  children: saving ? "Redeeming..." : "Redeem gift card"
533
589
  }
534
590
  )
@@ -556,8 +612,15 @@ function GiftCardDetailModal({
556
612
  redemption.id
557
613
  )) })
558
614
  ] }),
559
- error && /* @__PURE__ */ jsx("p", { className: "rounded-md bg-[var(--error-bg)] px-3 py-2 text-sm text-[var(--error)]", children: error }),
560
- /* @__PURE__ */ jsx("div", { className: "flex justify-end border-t border-[var(--border-default)] pt-4", children: /* @__PURE__ */ jsx(
615
+ error && /* @__PURE__ */ jsx(
616
+ "p",
617
+ {
618
+ role: "alert",
619
+ className: "rounded-md bg-[var(--error-bg)] px-3 py-2 text-sm text-[var(--error)]",
620
+ children: error
621
+ }
622
+ ),
623
+ giftCard.status !== "cancelled" && /* @__PURE__ */ jsx("div", { className: "flex justify-end border-t border-[var(--border-default)] pt-4", children: /* @__PURE__ */ jsx(
561
624
  InlineDeleteButton,
562
625
  {
563
626
  onDelete: remove,
@@ -573,12 +636,14 @@ function GiftCardsFeature() {
573
636
  const { tenant } = useAuth();
574
637
  const formatCurrency = useFormatCurrency();
575
638
  const { toast } = useToast();
639
+ const slug = tenant?.slug;
576
640
  const currency = normalizeCurrencyCode(
577
641
  tenant?.settings?.currency
578
642
  );
579
643
  const [giftCards, setGiftCards] = useState([]);
580
644
  const [stats, setStats] = useState(null);
581
645
  const [loading, setLoading] = useState(true);
646
+ const [loaded, setLoaded] = useState(false);
582
647
  const [search, setSearch] = useState("");
583
648
  const [status, setStatus] = useState("");
584
649
  const [page, setPage] = useState(1);
@@ -586,313 +651,386 @@ function GiftCardsFeature() {
586
651
  const [showIssue, setShowIssue] = useState(false);
587
652
  const [showBalance, setShowBalance] = useState(false);
588
653
  const [selected, setSelected] = useState(null);
654
+ const [dedupe] = useState(createInFlightDeduper);
655
+ const toastRef = useRef(toast);
656
+ useEffect(() => {
657
+ toastRef.current = toast;
658
+ });
589
659
  const latestRead = useRef(0);
590
660
  const readGiftCards = useCallback(
591
661
  async (quiet) => {
592
- if (!tenant?.slug) return;
662
+ if (!slug) return;
593
663
  const read = quiet ? latestRead.current : ++latestRead.current;
594
664
  if (!quiet) setLoading(true);
665
+ const query = {
666
+ ...status ? { status } : {},
667
+ ...search.trim() ? { search: search.trim() } : {},
668
+ page,
669
+ limit: PAGE_SIZE
670
+ };
595
671
  try {
596
- const [list, giftCardStats] = await Promise.all([
597
- giftCardsApi.list(tenant.slug, {
598
- ...status ? { status } : {},
599
- ...search.trim() ? { search: search.trim() } : {},
600
- page,
601
- limit: PAGE_SIZE
602
- }),
603
- giftCardsApi.stats(tenant.slug)
604
- ]);
672
+ const list = await dedupe.run(
673
+ `list:${slug}:${JSON.stringify(query)}`,
674
+ () => giftCardsApi.list(slug, query)
675
+ );
605
676
  if (read !== latestRead.current) return;
606
677
  setGiftCards(list.rows);
607
678
  setCount(list.count);
608
- setStats(giftCardStats);
609
679
  } catch (loadError) {
610
680
  if (quiet) throw loadError;
611
- toast({
681
+ toastRef.current({
612
682
  title: "Unable to load gift cards",
613
683
  description: errorMessage(loadError)
614
684
  });
615
685
  } finally {
616
- if (!quiet) setLoading(false);
686
+ if (!quiet && read === latestRead.current) {
687
+ setLoading(false);
688
+ setLoaded(true);
689
+ }
690
+ }
691
+ },
692
+ [dedupe, page, search, slug, status]
693
+ );
694
+ const readStats = useCallback(
695
+ async (quiet) => {
696
+ if (!slug) return;
697
+ try {
698
+ setStats(await dedupe.run(`stats:${slug}`, () => giftCardsApi.stats(slug)));
699
+ } catch (statsError) {
700
+ if (quiet) throw statsError;
617
701
  }
618
702
  },
619
- [page, search, status, tenant?.slug, toast]
703
+ [dedupe, slug]
704
+ );
705
+ const reloadAll = useCallback(
706
+ () => Promise.all([readGiftCards(false), readStats(false)]),
707
+ [readGiftCards, readStats]
620
708
  );
621
- const loadGiftCards = useCallback(() => readGiftCards(false), [readGiftCards]);
622
709
  useEffect(() => {
623
- loadGiftCards();
624
- }, [loadGiftCards]);
710
+ readGiftCards(false);
711
+ }, [readGiftCards]);
712
+ useEffect(() => {
713
+ readStats(false);
714
+ }, [readStats]);
625
715
  useQuietPoll(
626
- useCallback(() => readGiftCards(true), [readGiftCards]),
716
+ useCallback(
717
+ () => Promise.all([readGiftCards(true), readStats(true)]),
718
+ [readGiftCards, readStats]
719
+ ),
627
720
  LIVE_REFRESH_MS,
628
721
  LIVE
629
722
  );
630
- useRegisterRefresh(loadGiftCards);
723
+ useRegisterRefresh(reloadAll);
631
724
  const copyCode = async (event, code) => {
632
725
  event.stopPropagation();
633
726
  await navigator.clipboard.writeText(code);
634
727
  toast({ title: "Gift card code copied", description: code });
635
728
  };
636
729
  const totalPages = Math.max(1, Math.ceil(count / PAGE_SIZE));
637
- const filters = [
638
- { value: "", label: "All" },
639
- { value: "active", label: "Active" },
640
- { value: "used", label: "Used" },
641
- { value: "expired", label: "Expired" },
642
- { value: "cancelled", label: "Cancelled" }
643
- ];
644
- return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-7xl space-y-5 lg:space-y-6", children: [
645
- /* @__PURE__ */ jsx(
646
- MobilePageHeader,
647
- {
648
- title: "Gift cards",
649
- action: { label: "Issue gift card", onClick: () => setShowIssue(true) }
650
- }
651
- ),
652
- /* @__PURE__ */ jsxs("div", { className: "hidden items-center justify-between gap-3 lg:flex", children: [
653
- /* @__PURE__ */ jsxs("div", { children: [
654
- /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold text-[var(--text-primary)]", children: "Gift cards" }),
655
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-[var(--text-tertiary)]", children: "Credit a customer pays for now and spends later. Discounts and loyalty points cost the customer nothing." })
656
- ] }),
657
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
658
- !LIVE && /* @__PURE__ */ jsx(RefreshButton, { onClick: loadGiftCards, refreshing: loading }),
659
- /* @__PURE__ */ jsxs(
660
- "button",
661
- {
662
- type: "button",
663
- onClick: () => setShowBalance(true),
664
- className: "flex items-center gap-2 rounded-md border border-[var(--border-default)] px-3 py-2 text-sm font-medium text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)]",
665
- children: [
666
- /* @__PURE__ */ jsx(Search, { className: "h-4 w-4" }),
667
- "Check balance"
668
- ]
669
- }
670
- ),
671
- /* @__PURE__ */ jsxs(
672
- "button",
673
- {
674
- type: "button",
675
- onClick: () => setShowIssue(true),
676
- className: "flex items-center gap-2 rounded-md bg-brand-500 px-4 py-2 text-sm font-medium text-[var(--text-inverse)] hover:bg-brand-600",
677
- children: [
678
- /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
679
- "Issue gift card"
680
- ]
681
- }
682
- )
683
- ] })
684
- ] }),
685
- /* @__PURE__ */ jsx("div", { className: "hidden lg:block", children: /* @__PURE__ */ jsxs(MetricRow, { children: [
686
- /* @__PURE__ */ jsx(
687
- MetricTile,
688
- {
689
- label: "Issued value",
690
- value: formatCurrency(stats?.totalIssued ?? 0, currency)
691
- }
692
- ),
693
- /* @__PURE__ */ jsx(
694
- MetricTile,
695
- {
696
- label: "Outstanding",
697
- value: formatCurrency(stats?.outstanding ?? 0, currency)
698
- }
699
- ),
700
- /* @__PURE__ */ jsx(
701
- MetricTile,
702
- {
703
- label: "Redeemed",
704
- value: formatCurrency(stats?.redeemed ?? 0, currency)
705
- }
706
- ),
707
- /* @__PURE__ */ jsx(MetricTile, { label: "Active cards", value: stats?.activeCards ?? 0 })
708
- ] }) }),
709
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-px overflow-hidden rounded-md bg-[var(--border-default)] lg:hidden", children: [
710
- /* @__PURE__ */ jsxs("div", { className: "bg-[var(--bg-subtle)] p-4", children: [
711
- /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: "Outstanding" }),
712
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-lg font-semibold text-[var(--text-primary)]", children: formatCurrency(stats?.outstanding ?? 0, currency) })
713
- ] }),
714
- /* @__PURE__ */ jsxs("div", { className: "bg-[var(--bg-subtle)] p-4", children: [
715
- /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: "Active cards" }),
716
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-lg font-semibold text-[var(--text-primary)]", children: stats?.activeCards ?? 0 })
717
- ] })
718
- ] }),
719
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2 overflow-x-auto lg:hidden", children: [
720
- /* @__PURE__ */ jsxs(
721
- "button",
730
+ const hasFilters = Boolean(search.trim() || status);
731
+ const firstRun = loaded && isFirstRun({
732
+ loading,
733
+ search,
734
+ status,
735
+ count,
736
+ totalIssued: stats?.totalIssued
737
+ });
738
+ const hasCards = loaded && !firstRun;
739
+ const showStats = hasCards && stats !== null;
740
+ const openIssue = () => setShowIssue(true);
741
+ const openBalance = () => setShowBalance(true);
742
+ return /* @__PURE__ */ jsxs(
743
+ ListPage,
744
+ {
745
+ largeTitle: true,
746
+ title: "Gift cards",
747
+ mobileHeader: /* @__PURE__ */ jsx(
748
+ MobilePageHeader,
722
749
  {
723
- type: "button",
724
- onClick: () => setShowIssue(true),
725
- className: "flex shrink-0 items-center gap-1.5 rounded-full bg-brand-500 px-3.5 py-2 text-sm font-medium text-[var(--text-inverse)]",
726
- children: [
727
- /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
728
- "Issue gift card"
729
- ]
750
+ title: "Gift cards",
751
+ action: hasCards ? { label: "Issue gift card", onClick: openIssue } : void 0
730
752
  }
731
753
  ),
732
- /* @__PURE__ */ jsxs(
733
- "button",
734
- {
735
- type: "button",
736
- onClick: () => setShowBalance(true),
737
- className: "flex shrink-0 items-center gap-1.5 rounded-full bg-[var(--bg-subtle)] px-3.5 py-2 text-sm font-medium text-[var(--text-secondary)]",
738
- children: [
739
- /* @__PURE__ */ jsx(Search, { className: "h-4 w-4" }),
740
- "Check balance"
741
- ]
742
- }
743
- )
744
- ] }),
745
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 lg:flex-row lg:items-center", children: [
746
- /* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
747
- /* @__PURE__ */ jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--text-muted)]" }),
748
- /* @__PURE__ */ jsx(
749
- "input",
750
- {
751
- type: "search",
752
- placeholder: "Search code, recipient, or email",
753
- value: search,
754
- onChange: (event) => {
755
- setSearch(event.target.value);
756
- setPage(1);
757
- },
758
- className: "w-full rounded-md border border-[var(--border-default)] bg-[var(--bg-base)] py-2 pl-10 pr-3 text-sm focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/20"
759
- }
760
- )
754
+ actions: /* @__PURE__ */ jsxs(Fragment, { children: [
755
+ /* @__PURE__ */ jsx(Hint, { lines: PAGE_HINT_LINES, topic: "gift cards" }),
756
+ !LIVE && /* @__PURE__ */ jsx(RefreshButton, { onClick: reloadAll, refreshing: loading }),
757
+ hasCards && /* @__PURE__ */ jsxs(Fragment, { children: [
758
+ /* @__PURE__ */ jsx(
759
+ Button,
760
+ {
761
+ type: "button",
762
+ variant: "secondary",
763
+ icon: /* @__PURE__ */ jsx(Search, {}),
764
+ onClick: openBalance,
765
+ children: "Check balance"
766
+ }
767
+ ),
768
+ /* @__PURE__ */ jsx(Button, { type: "button", icon: /* @__PURE__ */ jsx(Plus, {}), onClick: openIssue, children: "Issue gift card" })
769
+ ] })
761
770
  ] }),
762
- /* @__PURE__ */ jsx("div", { className: "flex gap-2 overflow-x-auto", children: filters.map((filter) => /* @__PURE__ */ jsx(
763
- "button",
764
- {
765
- type: "button",
766
- onClick: () => {
767
- setStatus(filter.value);
768
- setPage(1);
769
- },
770
- className: `shrink-0 rounded-md px-3 py-2 text-sm font-medium ${status === filter.value ? "bg-brand-500 text-[var(--text-inverse)]" : "border border-[var(--border-default)] bg-[var(--bg-base)] text-[var(--text-secondary)] hover:bg-[var(--interactive-hover)]"}`,
771
- children: filter.label
772
- },
773
- filter.value
774
- )) })
775
- ] }),
776
- loading && giftCards.length === 0 && /* @__PURE__ */ jsx(InlineListSkeleton, {}),
777
- !loading && giftCards.length === 0 && /* @__PURE__ */ jsx(
778
- TableEmptyState,
779
- {
780
- icon: Gift,
781
- title: search || status ? "No gift cards found" : "Issue your first gift card",
782
- description: search || status ? "Try changing the search or status filter" : "Create a stored-value card for a customer or walk-in purchase"
783
- }
784
- ),
785
- giftCards.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
786
- /* @__PURE__ */ jsx("div", { className: "space-y-2 lg:hidden", children: giftCards.map((giftCard) => /* @__PURE__ */ jsxs(
787
- "button",
788
- {
789
- type: "button",
790
- onClick: () => setSelected(giftCard),
791
- className: "flex w-full items-center justify-between gap-3 rounded-md border border-[var(--border-default)] bg-[var(--bg-base)] p-4 text-left",
792
- children: [
793
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
794
- /* @__PURE__ */ jsx("p", { className: "truncate font-mono text-sm font-semibold text-[var(--text-primary)]", children: giftCard.code }),
795
- /* @__PURE__ */ jsx("p", { className: "truncate text-sm text-[var(--text-tertiary)]", children: giftCard.recipientName || giftCard.recipientEmail || "Walk-in" })
771
+ children: [
772
+ showStats && /* @__PURE__ */ jsxs(Fragment, { children: [
773
+ /* @__PURE__ */ jsx("div", { className: "hidden lg:block", children: /* @__PURE__ */ jsxs(MetricRow, { children: [
774
+ /* @__PURE__ */ jsx(
775
+ MetricTile,
776
+ {
777
+ label: "Total sold",
778
+ value: formatCurrency(stats?.totalIssued ?? 0, currency)
779
+ }
780
+ ),
781
+ /* @__PURE__ */ jsx(
782
+ MetricTile,
783
+ {
784
+ label: "Still to spend",
785
+ value: formatCurrency(stats?.outstanding ?? 0, currency)
786
+ }
787
+ ),
788
+ /* @__PURE__ */ jsx(
789
+ MetricTile,
790
+ {
791
+ label: "Spent",
792
+ value: formatCurrency(stats?.redeemed ?? 0, currency)
793
+ }
794
+ ),
795
+ /* @__PURE__ */ jsx(MetricTile, { label: "Cards in use", value: stats?.activeCards ?? 0 })
796
+ ] }) }),
797
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-px overflow-hidden rounded-md bg-[var(--border-default)] lg:hidden", children: [
798
+ /* @__PURE__ */ jsxs("div", { className: "bg-[var(--bg-subtle)] p-4", children: [
799
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: "Still to spend" }),
800
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-lg font-semibold text-[var(--text-primary)]", children: formatCurrency(stats?.outstanding ?? 0, currency) })
796
801
  ] }),
797
- /* @__PURE__ */ jsxs("div", { className: "shrink-0 text-right", children: [
798
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-[var(--text-primary)]", children: formatCurrency(Number(giftCard.balance), giftCard.currency) }),
799
- /* @__PURE__ */ jsx("span", { className: `text-xs capitalize ${statusClass(giftCard.status)}`, children: giftCard.status })
802
+ /* @__PURE__ */ jsxs("div", { className: "bg-[var(--bg-subtle)] p-4", children: [
803
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: "Cards in use" }),
804
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-lg font-semibold text-[var(--text-primary)]", children: stats?.activeCards ?? 0 })
800
805
  ] })
801
- ]
802
- },
803
- giftCard.id
804
- )) }),
805
- /* @__PURE__ */ jsx("div", { className: "hidden overflow-x-auto rounded-md border border-[var(--border-default)] bg-[var(--bg-base)] lg:block", children: /* @__PURE__ */ jsxs("table", { className: "w-full min-w-[760px]", children: [
806
- /* @__PURE__ */ jsx("thead", { className: "border-b border-[var(--border-default)] bg-[var(--bg-subtle)]", children: /* @__PURE__ */ jsx("tr", { children: ["Code", "Recipient", "Issued", "Balance", "Expires", "Status"].map(
807
- (heading) => /* @__PURE__ */ jsx(
808
- "th",
809
- {
810
- className: "px-4 py-3 text-left text-xs font-medium uppercase text-[var(--text-tertiary)]",
811
- children: heading
812
- },
813
- heading
814
- )
815
- ) }) }),
816
- /* @__PURE__ */ jsx("tbody", { className: "divide-y divide-[var(--border-default)]", children: giftCards.map((giftCard) => /* @__PURE__ */ jsxs(
817
- "tr",
806
+ ] })
807
+ ] }),
808
+ hasCards && /* @__PURE__ */ jsx("div", { className: "flex gap-2 overflow-x-auto lg:hidden", children: /* @__PURE__ */ jsx(
809
+ Button,
818
810
  {
819
- onClick: () => setSelected(giftCard),
820
- className: "cursor-pointer hover:bg-[var(--interactive-hover)]",
821
- children: [
822
- /* @__PURE__ */ jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
823
- /* @__PURE__ */ jsx("span", { className: "font-mono text-sm font-medium text-[var(--text-primary)]", children: giftCard.code }),
824
- /* @__PURE__ */ jsx(
825
- "button",
811
+ type: "button",
812
+ variant: "secondary",
813
+ shape: "pill",
814
+ icon: /* @__PURE__ */ jsx(Search, {}),
815
+ onClick: openBalance,
816
+ children: "Check balance"
817
+ }
818
+ ) }),
819
+ !firstRun && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 lg:flex-row lg:items-center", children: [
820
+ /* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
821
+ /* @__PURE__ */ jsx(
822
+ Search,
823
+ {
824
+ "aria-hidden": true,
825
+ className: "pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-[var(--text-muted)]"
826
+ }
827
+ ),
828
+ /* @__PURE__ */ jsx(
829
+ Input,
830
+ {
831
+ type: "search",
832
+ "aria-label": "Search gift cards",
833
+ placeholder: "Search code, recipient, or email",
834
+ value: search,
835
+ onChange: (event) => {
836
+ setSearch(event.target.value);
837
+ setPage(1);
838
+ },
839
+ className: "pl-10"
840
+ }
841
+ )
842
+ ] }),
843
+ /* @__PURE__ */ jsx(
844
+ "div",
845
+ {
846
+ role: "group",
847
+ "aria-label": "Filter by status",
848
+ className: "flex gap-1 overflow-x-auto",
849
+ children: STATUS_FILTERS.map((filter) => {
850
+ const pressed = status === filter.value;
851
+ return /* @__PURE__ */ jsx(Tooltip, { label: filter.hint, children: /* @__PURE__ */ jsx(
852
+ Button,
826
853
  {
827
854
  type: "button",
828
- onClick: (event) => copyCode(event, giftCard.code),
829
- className: "rounded p-1 text-[var(--text-muted)] hover:text-[var(--text-primary)]",
830
- title: "Copy code",
831
- children: /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5" })
855
+ size: "sm",
856
+ variant: pressed ? "secondary" : "ghost",
857
+ "aria-pressed": pressed,
858
+ className: "shrink-0",
859
+ onClick: () => {
860
+ setStatus(filter.value);
861
+ setPage(1);
862
+ },
863
+ children: filter.label
832
864
  }
833
- )
834
- ] }) }),
835
- /* @__PURE__ */ jsxs("td", { className: "px-4 py-3", children: [
836
- /* @__PURE__ */ jsx("p", { className: "text-sm text-[var(--text-primary)]", children: giftCard.recipientName || "Walk-in" }),
837
- /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: giftCard.recipientEmail })
838
- ] }),
839
- /* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-sm text-[var(--text-secondary)]", children: formatCurrency(
840
- Number(giftCard.originalAmount),
841
- giftCard.currency
842
- ) }),
843
- /* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-sm font-medium text-[var(--text-primary)]", children: formatCurrency(Number(giftCard.balance), giftCard.currency) }),
844
- /* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-sm text-[var(--text-secondary)]", children: dateLabel(giftCard.expiresAt) }),
845
- /* @__PURE__ */ jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsx(
846
- "span",
865
+ ) }, filter.value);
866
+ })
867
+ }
868
+ )
869
+ ] }),
870
+ /* @__PURE__ */ jsx("h2", { className: "sr-only", children: "Issued gift cards" }),
871
+ !loaded && /* @__PURE__ */ jsx(InlineListSkeleton, {}),
872
+ loading && loaded && giftCards.length === 0 && /* @__PURE__ */ jsx(InlineListSkeleton, {}),
873
+ firstRun && /* @__PURE__ */ jsxs("div", { children: [
874
+ /* @__PURE__ */ jsx(
875
+ TableEmptyState,
876
+ {
877
+ icon: Gift,
878
+ title: "Issue your first gift card",
879
+ description: "Create a stored-value card for a customer or walk-in purchase",
880
+ action: { label: "Issue gift card", onClick: openIssue }
881
+ }
882
+ ),
883
+ /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx(
884
+ Button,
885
+ {
886
+ type: "button",
887
+ variant: "ghost",
888
+ icon: /* @__PURE__ */ jsx(Search, {}),
889
+ onClick: openBalance,
890
+ children: "Check balance"
891
+ }
892
+ ) })
893
+ ] }),
894
+ loaded && !loading && !firstRun && giftCards.length === 0 && /* @__PURE__ */ jsx(
895
+ TableEmptyState,
896
+ {
897
+ icon: Gift,
898
+ title: "No gift cards found",
899
+ description: "Try changing the search or status filter",
900
+ action: hasFilters ? {
901
+ label: "Clear search and filters",
902
+ onClick: () => {
903
+ setSearch("");
904
+ setStatus("");
905
+ setPage(1);
906
+ }
907
+ } : void 0
908
+ }
909
+ ),
910
+ giftCards.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
911
+ /* @__PURE__ */ jsx("div", { className: "space-y-2 lg:hidden", children: giftCards.map((giftCard) => /* @__PURE__ */ jsxs(
912
+ "button",
913
+ {
914
+ type: "button",
915
+ onClick: () => setSelected(giftCard),
916
+ className: "flex w-full items-center justify-between gap-3 rounded-md border border-[var(--border-default)] bg-[var(--bg-base)] p-4 text-left",
917
+ children: [
918
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
919
+ /* @__PURE__ */ jsx("p", { className: "truncate font-mono text-sm font-semibold text-[var(--text-primary)]", children: giftCard.code }),
920
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm text-[var(--text-tertiary)]", children: giftCard.recipientName || giftCard.recipientEmail || "Walk-in" })
921
+ ] }),
922
+ /* @__PURE__ */ jsxs("div", { className: "shrink-0 text-right", children: [
923
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold text-[var(--text-primary)]", children: formatCurrency(Number(giftCard.balance), giftCard.currency) }),
924
+ /* @__PURE__ */ jsx("span", { className: `text-xs ${statusClass(giftCard.status)}`, children: STATUS_LABEL[giftCard.status] })
925
+ ] })
926
+ ]
927
+ },
928
+ giftCard.id
929
+ )) }),
930
+ /* @__PURE__ */ jsx("div", { className: "hidden overflow-x-auto rounded-md border border-[var(--border-default)] bg-[var(--bg-base)] lg:block", children: /* @__PURE__ */ jsxs("table", { className: "w-full min-w-[760px]", children: [
931
+ /* @__PURE__ */ jsx("thead", { className: "border-b border-[var(--border-default)] bg-[var(--bg-subtle)]", children: /* @__PURE__ */ jsx("tr", { children: ["Code", "Recipient", "Issued", "Balance", "Expires", "Status"].map(
932
+ (heading) => /* @__PURE__ */ jsx(
933
+ "th",
847
934
  {
848
- className: `inline-flex rounded-full px-2.5 py-1 text-xs font-medium capitalize ${statusClass(giftCard.status)}`,
849
- children: giftCard.status
850
- }
851
- ) })
852
- ]
853
- },
854
- giftCard.id
855
- )) })
856
- ] }) }),
857
- /* @__PURE__ */ jsx(
858
- TablePagination,
859
- {
860
- page,
861
- totalPages,
862
- pageSize: PAGE_SIZE,
863
- totalItems: count,
864
- entityName: "gift cards",
865
- onPageChange: setPage
866
- }
867
- )
868
- ] }),
869
- showIssue && /* @__PURE__ */ jsx(
870
- IssueGiftCardModal,
871
- {
872
- currency,
873
- merchantName: tenant?.businessName || "Gift card",
874
- onClose: () => setShowIssue(false),
875
- onCreated: (giftCard) => {
876
- setShowIssue(false);
877
- setSelected(giftCard);
878
- toast({
879
- title: "Gift card issued",
880
- description: giftCard.code
881
- });
882
- loadGiftCards();
883
- }
884
- }
885
- ),
886
- showBalance && /* @__PURE__ */ jsx(BalanceModal, { onClose: () => setShowBalance(false) }),
887
- selected && /* @__PURE__ */ jsx(
888
- GiftCardDetailModal,
889
- {
890
- giftCard: selected,
891
- onClose: () => setSelected(null),
892
- onChanged: loadGiftCards
893
- }
894
- )
895
- ] });
935
+ scope: "col",
936
+ className: "px-4 py-3 text-left text-xs font-medium uppercase text-[var(--text-tertiary)]",
937
+ children: heading
938
+ },
939
+ heading
940
+ )
941
+ ) }) }),
942
+ /* @__PURE__ */ jsx("tbody", { className: "divide-y divide-[var(--border-default)]", children: giftCards.map((giftCard) => /* @__PURE__ */ jsxs(
943
+ "tr",
944
+ {
945
+ tabIndex: 0,
946
+ "aria-label": `Open gift card ${giftCard.code}`,
947
+ onClick: () => setSelected(giftCard),
948
+ onKeyDown: (event) => {
949
+ if (event.target !== event.currentTarget) return;
950
+ if (event.key === "Enter" || event.key === " ") {
951
+ event.preventDefault();
952
+ setSelected(giftCard);
953
+ }
954
+ },
955
+ className: "cursor-pointer hover:bg-[var(--interactive-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--focus-ring)]",
956
+ children: [
957
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
958
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-sm font-medium text-[var(--text-primary)]", children: giftCard.code }),
959
+ /* @__PURE__ */ jsx(
960
+ Button,
961
+ {
962
+ type: "button",
963
+ variant: "ghost",
964
+ size: "icon-sm",
965
+ "aria-label": `Copy code ${giftCard.code}`,
966
+ onClick: (event) => copyCode(event, giftCard.code),
967
+ children: /* @__PURE__ */ jsx(Copy, {})
968
+ }
969
+ )
970
+ ] }) }),
971
+ /* @__PURE__ */ jsxs("td", { className: "px-4 py-3", children: [
972
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-[var(--text-primary)]", children: giftCard.recipientName || "Walk-in" }),
973
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: giftCard.recipientEmail })
974
+ ] }),
975
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-sm text-[var(--text-secondary)]", children: formatCurrency(
976
+ Number(giftCard.originalAmount),
977
+ giftCard.currency
978
+ ) }),
979
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-sm font-medium text-[var(--text-primary)]", children: formatCurrency(Number(giftCard.balance), giftCard.currency) }),
980
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-3 text-sm text-[var(--text-secondary)]", children: dateLabel(giftCard.expiresAt) }),
981
+ /* @__PURE__ */ jsx("td", { className: "px-4 py-3", children: /* @__PURE__ */ jsx(
982
+ "span",
983
+ {
984
+ className: `inline-flex rounded-full px-2.5 py-1 text-xs font-medium ${statusClass(giftCard.status)}`,
985
+ children: STATUS_LABEL[giftCard.status]
986
+ }
987
+ ) })
988
+ ]
989
+ },
990
+ giftCard.id
991
+ )) })
992
+ ] }) }),
993
+ /* @__PURE__ */ jsx(
994
+ TablePagination,
995
+ {
996
+ page,
997
+ totalPages,
998
+ pageSize: PAGE_SIZE,
999
+ totalItems: count,
1000
+ entityName: "gift cards",
1001
+ onPageChange: setPage
1002
+ }
1003
+ )
1004
+ ] }),
1005
+ showIssue && /* @__PURE__ */ jsx(
1006
+ IssueGiftCardModal,
1007
+ {
1008
+ currency,
1009
+ merchantName: tenant?.businessName || "Gift card",
1010
+ onClose: () => setShowIssue(false),
1011
+ onCreated: (giftCard) => {
1012
+ setShowIssue(false);
1013
+ setSelected(giftCard);
1014
+ toast({
1015
+ title: "Gift card issued",
1016
+ description: giftCard.code
1017
+ });
1018
+ reloadAll();
1019
+ }
1020
+ }
1021
+ ),
1022
+ showBalance && /* @__PURE__ */ jsx(BalanceModal, { onClose: () => setShowBalance(false) }),
1023
+ selected && /* @__PURE__ */ jsx(
1024
+ GiftCardDetailModal,
1025
+ {
1026
+ giftCard: selected,
1027
+ onClose: () => setSelected(null),
1028
+ onChanged: reloadAll
1029
+ }
1030
+ )
1031
+ ]
1032
+ }
1033
+ );
896
1034
  }
897
1035
  export {
898
1036
  GiftCardsFeature