@voyantjs/ui 0.28.3 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"notification-reminder-rule-dialog.d.ts","sourceRoot":"","sources":["../../src/components/notification-reminder-rule-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,8BAA8B,EAGpC,MAAM,+BAA+B,CAAA;AAuDtC,KAAK,mCAAmC,GAAG;IACzC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,IAAI,CAAC,EAAE,8BAA8B,CAAA;IACrC,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,CAAA;AAED,wBAAgB,8BAA8B,CAAC,EAC7C,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,SAAS,GACV,EAAE,mCAAmC,2CAiNrC"}
1
+ {"version":3,"file":"notification-reminder-rule-dialog.d.ts","sourceRoot":"","sources":["../../src/components/notification-reminder-rule-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,8BAA8B,EAGpC,MAAM,+BAA+B,CAAA;AAsDtC,KAAK,mCAAmC,GAAG;IACzC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,IAAI,CAAC,EAAE,8BAA8B,CAAA;IACrC,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,CAAA;AAED,wBAAgB,8BAA8B,CAAC,EAC7C,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,SAAS,GACV,EAAE,mCAAmC,2CAkMrC"}
@@ -21,8 +21,9 @@ const reminderRuleFormSchema = z.object({
21
21
  "booking_cancelled_non_payment",
22
22
  ]),
23
23
  channel: z.enum(["email", "sms"]),
24
- templateId: z.string().min(1, "Template is required"),
25
- relativeDaysFromDueDate: z.number().int().min(-365).max(365),
24
+ // Optional default template — stages own per-channel templates and
25
+ // override this. Empty string is normalized to null in the payload.
26
+ templateId: z.string().optional(),
26
27
  });
27
28
  const reminderTargetOptions = [
28
29
  { value: "booking_confirmed", label: "Booking confirmed" },
@@ -30,7 +31,6 @@ const reminderTargetOptions = [
30
31
  { value: "booking_cancelled_non_payment", label: "Booking cancelled (non-payment)" },
31
32
  { value: "booking_payment_schedule", label: "Booking payment schedule" },
32
33
  ];
33
- const dueDateTargetTypes = new Set(["booking_payment_schedule"]);
34
34
  function slugifyReminderRule(value) {
35
35
  const slug = value
36
36
  .trim()
@@ -50,12 +50,9 @@ export function NotificationReminderRuleDialog({ open, onOpenChange, rule, onSuc
50
50
  targetType: "booking_payment_schedule",
51
51
  channel: "email",
52
52
  templateId: "",
53
- relativeDaysFromDueDate: 0,
54
53
  },
55
54
  });
56
55
  const channel = form.watch("channel");
57
- const targetType = form.watch("targetType");
58
- const usesDueDateTiming = dueDateTargetTypes.has(targetType);
59
56
  const { data: templates } = useNotificationTemplates({
60
57
  channel,
61
58
  status: "active",
@@ -75,7 +72,6 @@ export function NotificationReminderRuleDialog({ open, onOpenChange, rule, onSuc
75
72
  targetType: rule.targetType === "invoice" ? "booking_payment_schedule" : rule.targetType,
76
73
  channel: rule.channel,
77
74
  templateId: resolvedTemplateId,
78
- relativeDaysFromDueDate: rule.relativeDaysFromDueDate,
79
75
  });
80
76
  return;
81
77
  }
@@ -92,11 +88,8 @@ export function NotificationReminderRuleDialog({ open, onOpenChange, rule, onSuc
92
88
  targetType: values.targetType,
93
89
  channel: values.channel,
94
90
  provider: null,
95
- templateId: values.templateId,
91
+ templateId: values.templateId ? values.templateId : null,
96
92
  templateSlug: null,
97
- relativeDaysFromDueDate: dueDateTargetTypes.has(values.targetType)
98
- ? values.relativeDaysFromDueDate
99
- : 0,
100
93
  isSystem: rule?.isSystem ?? false,
101
94
  metadata: rule?.metadata ?? null,
102
95
  };
@@ -117,13 +110,13 @@ export function NotificationReminderRuleDialog({ open, onOpenChange, rule, onSuc
117
110
  if (!value)
118
111
  return;
119
112
  form.setValue("status", value);
120
- }, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "draft", children: "Draft" }), _jsx(SelectItem, { value: "active", children: "Active" }), _jsx(SelectItem, { value: "archived", children: "Archived" })] })] })] })] }), _jsxs("div", { className: usesDueDateTiming ? "grid grid-cols-2 gap-4" : "grid gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Channel" }), _jsxs(Select, { value: form.watch("channel"), onValueChange: (value) => {
121
- if (!value)
122
- return;
123
- form.setValue("channel", value);
124
- }, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "email", children: "Email" }), _jsx(SelectItem, { value: "sms", children: "SMS" })] })] })] }), usesDueDateTiming ? (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Send timing" }), _jsx(Input, { type: "number", value: form.watch("relativeDaysFromDueDate"), onChange: (event) => form.setValue("relativeDaysFromDueDate", Number.parseInt(event.target.value || "0", 10)) }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Days from due date: -3 sends 3 days before, 0 on the due date, 3 after." })] })) : null] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Template" }), _jsxs(Select, { value: form.watch("templateId"), onValueChange: (value) => {
113
+ }, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "draft", children: "Draft" }), _jsx(SelectItem, { value: "active", children: "Active" }), _jsx(SelectItem, { value: "archived", children: "Archived" })] })] })] })] }), _jsx("div", { className: "grid gap-4", children: _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Channel" }), _jsxs(Select, { value: form.watch("channel"), onValueChange: (value) => {
114
+ if (!value)
115
+ return;
116
+ form.setValue("channel", value);
117
+ }, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "email", children: "Email" }), _jsx(SelectItem, { value: "sms", children: "SMS" })] })] })] }) }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Default template" }), _jsxs(Select, { value: form.watch("templateId"), onValueChange: (value) => {
125
118
  if (!value)
126
119
  return;
127
120
  form.setValue("templateId", value);
128
- }, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: "Select template" }) }), _jsx(SelectContent, { children: (templates?.data ?? []).map((template) => (_jsxs(SelectItem, { value: template.id, children: [template.name, " (", template.slug, ")"] }, template.id))) })] })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: "Cancel" }), _jsxs(Button, { type: "submit", disabled: isPending, children: [isPending ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, isEditing ? "Save Changes" : "Create Rule"] })] })] })] }) }));
121
+ }, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: "Select template" }) }), _jsx(SelectContent, { children: (templates?.data ?? []).map((template) => (_jsxs(SelectItem, { value: template.id, children: [template.name, " (", template.slug, ")"] }, template.id))) })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Used as a fallback. Per-stage channels can override this." })] }), !isEditing ? (_jsxs("p", { className: "rounded-md border border-dashed bg-muted/40 px-3 py-2 text-xs text-muted-foreground", children: ["After creating the rule, click ", _jsx("strong", { children: "Manage stages" }), " on the row to define when it fires (anchor, window, cadence) and which channels deliver it."] })) : null] }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: "Cancel" }), _jsxs(Button, { type: "submit", disabled: isPending, children: [isPending ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, isEditing ? "Save Changes" : "Create Rule"] })] })] })] }) }));
129
122
  }
@@ -1,2 +1,10 @@
1
- export declare function NotificationReminderRulesPage(): import("react/jsx-runtime").JSX.Element;
1
+ export interface NotificationReminderRulesPageProps {
2
+ /**
3
+ * Path used for the per-rule "Manage stages" link. Receives the rule id and
4
+ * returns the URL the consumer's router understands. Defaults to
5
+ * `/notifications/reminder-rules/<id>`.
6
+ */
7
+ manageStagesHref?: (ruleId: string) => string;
8
+ }
9
+ export declare function NotificationReminderRulesPage({ manageStagesHref, }?: NotificationReminderRulesPageProps): import("react/jsx-runtime").JSX.Element;
2
10
  //# sourceMappingURL=notification-reminder-rules-page.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"notification-reminder-rules-page.d.ts","sourceRoot":"","sources":["../../src/components/notification-reminder-rules-page.tsx"],"names":[],"mappings":"AAyCA,wBAAgB,6BAA6B,4CA+J5C"}
1
+ {"version":3,"file":"notification-reminder-rules-page.d.ts","sourceRoot":"","sources":["../../src/components/notification-reminder-rules-page.tsx"],"names":[],"mappings":"AA4BA,MAAM,WAAW,kCAAkC;IACjD;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAA;CAC9C;AAED,wBAAgB,6BAA6B,CAAC,EAC5C,gBAAgE,GACjE,GAAE,kCAAuC,2CAoKzC"}
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useNotificationReminderRules, } from "@voyantjs/notifications-react";
4
- import { Loader2, Pencil, Plus, Search } from "lucide-react";
4
+ import { Layers, Loader2, Pencil, Plus, Search } from "lucide-react";
5
5
  import { useState } from "react";
6
6
  import { Badge } from "./badge.js";
7
- import { Button } from "./button.js";
7
+ import { Button, buttonVariants } from "./button.js";
8
8
  import { Input } from "./input.js";
9
9
  import { NotificationReminderRuleDialog } from "./notification-reminder-rule-dialog.js";
10
10
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select.js";
@@ -14,22 +14,12 @@ const reminderTargetLabels = {
14
14
  payment_complete: "Payment complete",
15
15
  booking_cancelled_non_payment: "Booking cancelled (non-payment)",
16
16
  };
17
- const dueDateTargetTypes = new Set([
18
- "booking_payment_schedule",
19
- ]);
20
17
  function getReminderTargetLabel(targetType) {
21
18
  if (targetType === "invoice")
22
19
  return "Invoice";
23
20
  return reminderTargetLabels[targetType] ?? targetType;
24
21
  }
25
- function formatReminderTiming(targetType, days) {
26
- if (!dueDateTargetTypes.has(targetType))
27
- return "Event";
28
- if (days === 0)
29
- return "Due date";
30
- return days < 0 ? `${Math.abs(days)} days before` : `${days} days after`;
31
- }
32
- export function NotificationReminderRulesPage() {
22
+ export function NotificationReminderRulesPage({ manageStagesHref = (id) => `/notifications/reminder-rules/${id}`, } = {}) {
33
23
  const [search, setSearch] = useState("");
34
24
  const [channel, setChannel] = useState("all");
35
25
  const [status, setStatus] = useState("all");
@@ -45,10 +35,10 @@ export function NotificationReminderRulesPage() {
45
35
  return (_jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Reminder Rules" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Schedule booking payment reminders and event notifications against templates and channels." })] }), _jsxs(Button, { onClick: () => {
46
36
  setEditing(undefined);
47
37
  setDialogOpen(true);
48
- }, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "New Rule"] })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "relative max-w-sm flex-1", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { placeholder: "Search rules...", value: search, onChange: (event) => setSearch(event.target.value), className: "pl-9" })] }), _jsxs(Select, { value: targetType, onValueChange: (value) => setTargetType(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[190px]", children: _jsx(SelectValue, { placeholder: "Target" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All targets" }), Object.entries(reminderTargetLabels).map(([value, label]) => (_jsx(SelectItem, { value: value, children: label }, value)))] })] }), _jsxs(Select, { value: channel, onValueChange: (value) => setChannel(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[140px]", children: _jsx(SelectValue, { placeholder: "Channel" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All channels" }), _jsx(SelectItem, { value: "email", children: "Email" }), _jsx(SelectItem, { value: "sms", children: "SMS" })] })] }), _jsxs(Select, { value: status, onValueChange: (value) => setStatus(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[140px]", children: _jsx(SelectValue, { placeholder: "Status" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All statuses" }), _jsx(SelectItem, { value: "draft", children: "Draft" }), _jsx(SelectItem, { value: "active", children: "Active" }), _jsx(SelectItem, { value: "archived", children: "Archived" })] })] })] }), isPending ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) })) : null, !isPending && (!data?.data || data.data.length === 0) ? (_jsx("div", { className: "rounded-md border border-dashed p-8 text-center", children: _jsx("p", { className: "text-sm text-muted-foreground", children: "No reminder rules yet." }) })) : null, !isPending && data?.data && data.data.length > 0 ? (_jsx("div", { className: "rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/40 text-left text-xs uppercase tracking-wide text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-4 py-3", children: "Rule" }), _jsx("th", { className: "px-4 py-3", children: "Target" }), _jsx("th", { className: "px-4 py-3", children: "Channel" }), _jsx("th", { className: "px-4 py-3", children: "Timing" }), _jsx("th", { className: "px-4 py-3", children: "Status" }), _jsx("th", { className: "px-4 py-3 text-right", children: "Actions" })] }) }), _jsx("tbody", { children: data.data.map((rule) => (_jsxs("tr", { className: "border-t", children: [_jsx("td", { className: "px-4 py-3", children: _jsx("div", { className: "font-medium", children: rule.name }) }), _jsx("td", { className: "px-4 py-3", children: getReminderTargetLabel(rule.targetType) }), _jsx("td", { className: "px-4 py-3", children: _jsx(Badge, { variant: "outline", children: rule.channel }) }), _jsx("td", { className: "px-4 py-3", children: formatReminderTiming(rule.targetType, rule.relativeDaysFromDueDate) }), _jsx("td", { className: "px-4 py-3", children: _jsx(Badge, { variant: rule.status === "active" ? "default" : "secondary", children: rule.status }) }), _jsx("td", { className: "px-4 py-3 text-right", children: _jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
49
- setEditing(rule);
50
- setDialogOpen(true);
51
- }, children: _jsx(Pencil, { className: "h-4 w-4" }) }) })] }, rule.id))) })] }) })) : null, _jsx(NotificationReminderRuleDialog, { open: dialogOpen, onOpenChange: setDialogOpen, rule: editing, onSuccess: () => {
38
+ }, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "New Rule"] })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "relative max-w-sm flex-1", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { placeholder: "Search rules...", value: search, onChange: (event) => setSearch(event.target.value), className: "pl-9" })] }), _jsxs(Select, { value: targetType, onValueChange: (value) => setTargetType(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[190px]", children: _jsx(SelectValue, { placeholder: "Target" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All targets" }), Object.entries(reminderTargetLabels).map(([value, label]) => (_jsx(SelectItem, { value: value, children: label }, value)))] })] }), _jsxs(Select, { value: channel, onValueChange: (value) => setChannel(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[140px]", children: _jsx(SelectValue, { placeholder: "Channel" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All channels" }), _jsx(SelectItem, { value: "email", children: "Email" }), _jsx(SelectItem, { value: "sms", children: "SMS" })] })] }), _jsxs(Select, { value: status, onValueChange: (value) => setStatus(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[140px]", children: _jsx(SelectValue, { placeholder: "Status" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All statuses" }), _jsx(SelectItem, { value: "draft", children: "Draft" }), _jsx(SelectItem, { value: "active", children: "Active" }), _jsx(SelectItem, { value: "archived", children: "Archived" })] })] })] }), isPending ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) })) : null, !isPending && (!data?.data || data.data.length === 0) ? (_jsx("div", { className: "rounded-md border border-dashed p-8 text-center", children: _jsx("p", { className: "text-sm text-muted-foreground", children: "No reminder rules yet." }) })) : null, !isPending && data?.data && data.data.length > 0 ? (_jsx("div", { className: "rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/40 text-left text-xs uppercase tracking-wide text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-4 py-3", children: "Rule" }), _jsx("th", { className: "px-4 py-3", children: "Target" }), _jsx("th", { className: "px-4 py-3", children: "Channel" }), _jsx("th", { className: "px-4 py-3", children: "Status" }), _jsx("th", { className: "px-4 py-3 text-right", children: "Actions" })] }) }), _jsx("tbody", { children: data.data.map((rule) => (_jsxs("tr", { className: "border-t", children: [_jsx("td", { className: "px-4 py-3", children: _jsx("div", { className: "font-medium", children: rule.name }) }), _jsx("td", { className: "px-4 py-3", children: getReminderTargetLabel(rule.targetType) }), _jsx("td", { className: "px-4 py-3", children: _jsx(Badge, { variant: "outline", children: rule.channel }) }), _jsx("td", { className: "px-4 py-3", children: _jsx(Badge, { variant: rule.status === "active" ? "default" : "secondary", children: rule.status }) }), _jsx("td", { className: "px-4 py-3 text-right", children: _jsxs("div", { className: "flex items-center justify-end gap-1", children: [_jsxs("a", { href: manageStagesHref(rule.id), className: buttonVariants({ variant: "ghost", size: "sm" }), children: [_jsx(Layers, { className: "mr-2 h-4 w-4" }), "Manage stages"] }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
39
+ setEditing(rule);
40
+ setDialogOpen(true);
41
+ }, children: _jsx(Pencil, { className: "h-4 w-4" }) })] }) })] }, rule.id))) })] }) })) : null, _jsx(NotificationReminderRuleDialog, { open: dialogOpen, onOpenChange: setDialogOpen, rule: editing, onSuccess: () => {
52
42
  setDialogOpen(false);
53
43
  setEditing(undefined);
54
44
  void refetch();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/ui",
3
- "version": "0.28.3",
3
+ "version": "0.30.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -35,10 +35,10 @@
35
35
  "tw-animate-css": "^1.3.5",
36
36
  "vaul": "^1.1.2",
37
37
  "zod": "^4.3.6",
38
- "@voyantjs/i18n": "0.28.3",
39
- "@voyantjs/notifications": "0.28.3",
40
- "@voyantjs/notifications-react": "0.28.3",
41
- "@voyantjs/utils": "0.28.3"
38
+ "@voyantjs/i18n": "0.30.0",
39
+ "@voyantjs/notifications": "0.30.0",
40
+ "@voyantjs/notifications-react": "0.30.0",
41
+ "@voyantjs/utils": "0.30.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@tailwindcss/postcss": "^4.1.11",