@voyantjs/notifications-react 0.106.0 → 0.107.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.
Files changed (87) hide show
  1. package/README.md +30 -0
  2. package/dist/admin/index.d.ts +82 -0
  3. package/dist/admin/index.d.ts.map +1 -0
  4. package/dist/admin/index.js +96 -0
  5. package/dist/admin/notification-deliveries-host.d.ts +7 -0
  6. package/dist/admin/notification-deliveries-host.d.ts.map +1 -0
  7. package/dist/admin/notification-deliveries-host.js +92 -0
  8. package/dist/admin/notification-delivery-detail-dialog.d.ts +8 -0
  9. package/dist/admin/notification-delivery-detail-dialog.d.ts.map +1 -0
  10. package/dist/admin/notification-delivery-detail-dialog.js +30 -0
  11. package/dist/admin/notification-reminder-rule-detail-host.d.ts +12 -0
  12. package/dist/admin/notification-reminder-rule-detail-host.d.ts.map +1 -0
  13. package/dist/admin/notification-reminder-rule-detail-host.js +23 -0
  14. package/dist/admin/notification-reminder-rule-dialog.d.ts +10 -0
  15. package/dist/admin/notification-reminder-rule-dialog.d.ts.map +1 -0
  16. package/dist/admin/notification-reminder-rule-dialog.js +122 -0
  17. package/dist/admin/notification-reminder-rules-host.d.ts +8 -0
  18. package/dist/admin/notification-reminder-rules-host.d.ts.map +1 -0
  19. package/dist/admin/notification-reminder-rules-host.js +57 -0
  20. package/dist/admin/notification-reminder-runs-host.d.ts +7 -0
  21. package/dist/admin/notification-reminder-runs-host.d.ts.map +1 -0
  22. package/dist/admin/notification-reminder-runs-host.js +28 -0
  23. package/dist/admin/notification-settings-host.d.ts +7 -0
  24. package/dist/admin/notification-settings-host.d.ts.map +1 -0
  25. package/dist/admin/notification-settings-host.js +11 -0
  26. package/dist/admin/notification-template-authoring-help.d.ts +25 -0
  27. package/dist/admin/notification-template-authoring-help.d.ts.map +1 -0
  28. package/dist/admin/notification-template-authoring-help.js +8 -0
  29. package/dist/admin/notification-template-detail-host.d.ts +11 -0
  30. package/dist/admin/notification-template-detail-host.d.ts.map +1 -0
  31. package/dist/admin/notification-template-detail-host.js +159 -0
  32. package/dist/admin/notification-template-dialog.d.ts +10 -0
  33. package/dist/admin/notification-template-dialog.d.ts.map +1 -0
  34. package/dist/admin/notification-template-dialog.js +364 -0
  35. package/dist/admin/notification-templates-host.d.ts +9 -0
  36. package/dist/admin/notification-templates-host.d.ts.map +1 -0
  37. package/dist/admin/notification-templates-host.js +52 -0
  38. package/dist/admin/notifications-admin-shared.d.ts +14 -0
  39. package/dist/admin/notifications-admin-shared.d.ts.map +1 -0
  40. package/dist/admin/notifications-admin-shared.js +17 -0
  41. package/dist/admin/reminders-preview-host.d.ts +7 -0
  42. package/dist/admin/reminders-preview-host.d.ts.map +1 -0
  43. package/dist/admin/reminders-preview-host.js +13 -0
  44. package/dist/components/notification-settings-form.d.ts +2 -0
  45. package/dist/components/notification-settings-form.d.ts.map +1 -0
  46. package/dist/components/notification-settings-form.js +66 -0
  47. package/dist/components/reminders-preview-list.d.ts +6 -0
  48. package/dist/components/reminders-preview-list.d.ts.map +1 -0
  49. package/dist/components/reminders-preview-list.js +19 -0
  50. package/dist/components/stage-channel-editor-dialog.d.ts +11 -0
  51. package/dist/components/stage-channel-editor-dialog.d.ts.map +1 -0
  52. package/dist/components/stage-channel-editor-dialog.js +77 -0
  53. package/dist/components/stage-channel-list.d.ts +6 -0
  54. package/dist/components/stage-channel-list.d.ts.map +1 -0
  55. package/dist/components/stage-channel-list.js +20 -0
  56. package/dist/components/stage-editor-dialog.d.ts +10 -0
  57. package/dist/components/stage-editor-dialog.d.ts.map +1 -0
  58. package/dist/components/stage-editor-dialog.js +104 -0
  59. package/dist/components/stage-list.d.ts +5 -0
  60. package/dist/components/stage-list.d.ts.map +1 -0
  61. package/dist/components/stage-list.js +34 -0
  62. package/dist/components/template-picker.d.ts +19 -0
  63. package/dist/components/template-picker.d.ts.map +1 -0
  64. package/dist/components/template-picker.js +26 -0
  65. package/dist/components/timezone-combobox.d.ts +9 -0
  66. package/dist/components/timezone-combobox.d.ts.map +1 -0
  67. package/dist/components/timezone-combobox.js +67 -0
  68. package/dist/i18n/en.d.ts +3 -0
  69. package/dist/i18n/en.d.ts.map +1 -0
  70. package/dist/i18n/en.js +385 -0
  71. package/dist/i18n/index.d.ts +5 -0
  72. package/dist/i18n/index.d.ts.map +1 -0
  73. package/dist/i18n/index.js +3 -0
  74. package/dist/i18n/messages.d.ts +386 -0
  75. package/dist/i18n/messages.d.ts.map +1 -0
  76. package/dist/i18n/messages.js +1 -0
  77. package/dist/i18n/provider.d.ts +26 -0
  78. package/dist/i18n/provider.d.ts.map +1 -0
  79. package/dist/i18n/provider.js +44 -0
  80. package/dist/i18n/ro.d.ts +3 -0
  81. package/dist/i18n/ro.d.ts.map +1 -0
  82. package/dist/i18n/ro.js +385 -0
  83. package/dist/ui.d.ts +9 -0
  84. package/dist/ui.d.ts.map +1 -0
  85. package/dist/ui.js +8 -0
  86. package/package.json +71 -10
  87. package/src/styles.css +2 -0
@@ -0,0 +1,11 @@
1
+ import { type ReminderStageChannelRecord } from "../index.js";
2
+ export interface StageChannelEditorDialogProps {
3
+ reminderRuleId: string;
4
+ stageId: string;
5
+ channel: ReminderStageChannelRecord | null;
6
+ defaultOrderIndex?: number;
7
+ open: boolean;
8
+ onOpenChange: (open: boolean) => void;
9
+ }
10
+ export declare function StageChannelEditorDialog({ reminderRuleId, stageId, channel, defaultOrderIndex, open, onOpenChange, }: StageChannelEditorDialogProps): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=stage-channel-editor-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage-channel-editor-dialog.d.ts","sourceRoot":"","sources":["../../src/components/stage-channel-editor-dialog.tsx"],"names":[],"mappings":"AAqBA,OAAO,EAAE,KAAK,0BAA0B,EAAmC,MAAM,aAAa,CAAA;AAoC9F,MAAM,WAAW,6BAA6B;IAC5C,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,0BAA0B,GAAG,IAAI,CAAA;IAC1C,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;CACtC;AAED,wBAAgB,wBAAwB,CAAC,EACvC,cAAc,EACd,OAAO,EACP,OAAO,EACP,iBAAqB,EACrB,IAAI,EACJ,YAAY,GACb,EAAE,6BAA6B,2CA+J/B"}
@@ -0,0 +1,77 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
4
+ import { Field, FieldDescription, FieldGroup, FieldLabel } from "@voyantjs/ui/components/field";
5
+ import { Loader2 } from "lucide-react";
6
+ import { useEffect, useState } from "react";
7
+ import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
8
+ import { useReminderStageChannelMutation } from "../index.js";
9
+ import { TemplatePicker } from "./template-picker.js";
10
+ function fromRecord(channel, orderIndex) {
11
+ if (!channel) {
12
+ return {
13
+ orderIndex,
14
+ channel: "email",
15
+ provider: "automatic",
16
+ templateId: null,
17
+ recipientKind: "primary",
18
+ };
19
+ }
20
+ const provider = channel.provider === "resend" || channel.provider === "twilio" ? channel.provider : "automatic";
21
+ return {
22
+ orderIndex: channel.orderIndex,
23
+ channel: channel.channel,
24
+ provider,
25
+ templateId: channel.templateId ?? null,
26
+ recipientKind: channel.recipientKind,
27
+ };
28
+ }
29
+ export function StageChannelEditorDialog({ reminderRuleId, stageId, channel, defaultOrderIndex = 0, open, onOpenChange, }) {
30
+ const messages = useNotificationsUiMessagesOrDefault();
31
+ const { create, update } = useReminderStageChannelMutation(reminderRuleId, stageId);
32
+ const [form, setForm] = useState(() => fromRecord(channel, defaultOrderIndex));
33
+ const isEdit = Boolean(channel);
34
+ const isPending = create.isPending || update.isPending;
35
+ useEffect(() => {
36
+ if (open)
37
+ setForm(fromRecord(channel, defaultOrderIndex));
38
+ }, [open, channel, defaultOrderIndex]);
39
+ const setField = (key, value) => setForm((prev) => ({ ...prev, [key]: value }));
40
+ const handleSubmit = async () => {
41
+ const input = {
42
+ orderIndex: form.orderIndex,
43
+ channel: form.channel,
44
+ provider: form.provider === "automatic" ? null : form.provider,
45
+ templateId: form.templateId,
46
+ templateSlug: null,
47
+ recipientKind: form.recipientKind,
48
+ recipientRole: null,
49
+ };
50
+ if (isEdit && channel) {
51
+ await update.mutateAsync({ channelId: channel.id, input });
52
+ }
53
+ else {
54
+ await create.mutateAsync(input);
55
+ }
56
+ onOpenChange(false);
57
+ };
58
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEdit ? messages.channel.titles.edit : messages.channel.titles.create }) }), _jsx(DialogBody, { children: _jsxs(FieldGroup, { children: [_jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-[1fr_8rem]", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "channel-channel", children: messages.channel.fields.channel }), _jsxs(Select, { value: form.channel, onValueChange: (v) => {
59
+ if (!v)
60
+ return;
61
+ const next = v;
62
+ setForm((prev) => ({
63
+ ...prev,
64
+ channel: next,
65
+ // Picked template no longer matches the new channel — clear it.
66
+ templateId: null,
67
+ }));
68
+ }, children: [_jsx(SelectTrigger, { id: "channel-channel", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "email", children: messages.channel.channels.email }), _jsx(SelectItem, { value: "sms", children: messages.channel.channels.sms })] })] })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "channel-order", children: messages.channel.fields.orderIndex }), _jsx(Input, { id: "channel-order", type: "number", value: form.orderIndex, onChange: (e) => setField("orderIndex", Number(e.target.value)) })] })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { children: messages.channel.fields.template }), _jsx(TemplatePicker, { value: form.templateId, onChange: (value) => setField("templateId", value), channel: form.channel, placeholder: messages.channel.placeholders.template }), _jsx(FieldDescription, { children: "Filtered by the channel above. Resolved at send time so editing the template doesn't need a rule update." })] }), _jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "channel-recipient-kind", children: messages.channel.fields.recipientKind }), _jsxs(Select, { value: form.recipientKind, onValueChange: (v) => {
69
+ if (!v)
70
+ return;
71
+ setField("recipientKind", v);
72
+ }, children: [_jsx(SelectTrigger, { id: "channel-recipient-kind", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "primary", children: messages.channel.recipientKinds.primary }), _jsx(SelectItem, { value: "cc", children: messages.channel.recipientKinds.cc }), _jsx(SelectItem, { value: "bcc", children: messages.channel.recipientKinds.bcc })] })] })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "channel-provider", children: messages.channel.fields.provider }), _jsxs(Select, { value: form.provider, onValueChange: (v) => {
73
+ if (!v)
74
+ return;
75
+ setField("provider", v);
76
+ }, children: [_jsx(SelectTrigger, { id: "channel-provider", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "automatic", children: messages.channel.providers.automatic }), _jsx(SelectItem, { value: "resend", children: messages.channel.providers.resend }), _jsx(SelectItem, { value: "twilio", children: messages.channel.providers.twilio })] })] }), _jsx(FieldDescription, { children: messages.channel.descriptions.automaticProvider })] })] })] }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: isPending, children: messages.common.cancel }), _jsxs(Button, { onClick: handleSubmit, disabled: isPending, children: [isPending ? _jsx(Loader2, { className: "size-4 animate-spin" }) : null, isEdit ? messages.common.save : messages.common.create] })] })] }) }));
77
+ }
@@ -0,0 +1,6 @@
1
+ export interface StageChannelListProps {
2
+ reminderRuleId: string;
3
+ stageId: string;
4
+ }
5
+ export declare function StageChannelList({ reminderRuleId, stageId }: StageChannelListProps): import("react/jsx-runtime").JSX.Element;
6
+ //# sourceMappingURL=stage-channel-list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage-channel-list.d.ts","sourceRoot":"","sources":["../../src/components/stage-channel-list.tsx"],"names":[],"mappings":"AAaA,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,wBAAgB,gBAAgB,CAAC,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,qBAAqB,2CAyElF"}
@@ -0,0 +1,20 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components";
4
+ import { Pencil, Plus, Trash2 } from "lucide-react";
5
+ import { useState } from "react";
6
+ import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
7
+ import { useReminderStageChannelMutation, useReminderStageChannels, } from "../index.js";
8
+ import { StageChannelEditorDialog } from "./stage-channel-editor-dialog.js";
9
+ export function StageChannelList({ reminderRuleId, stageId }) {
10
+ const messages = useNotificationsUiMessagesOrDefault();
11
+ const { data: channels, isLoading } = useReminderStageChannels(reminderRuleId, stageId);
12
+ const { remove } = useReminderStageChannelMutation(reminderRuleId, stageId);
13
+ const [editing, setEditing] = useState(null);
14
+ const [creating, setCreating] = useState(false);
15
+ return (_jsxs(Card, { className: "border-dashed", children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0 pb-2", children: [_jsx(CardTitle, { className: "text-sm font-medium", children: messages.channel.listHeading }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => setCreating(true), children: [_jsx(Plus, { className: "size-4" }), " ", messages.channel.addChannel] })] }), _jsxs(CardContent, { className: "space-y-2", children: [isLoading && _jsx("p", { className: "text-sm text-muted-foreground", children: messages.common.loading }), channels && channels.length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: messages.channel.listEmpty })), channels?.map((channel) => (_jsxs("div", { className: "flex items-center justify-between rounded-md border px-3 py-2 text-sm", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Badge, { variant: "outline", children: messages.channel.channels[channel.channel] }), _jsx(Badge, { variant: "secondary", children: messages.channel.recipientKinds[channel.recipientKind] })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { size: "icon", variant: "ghost", onClick: () => setEditing(channel), children: _jsx(Pencil, { className: "size-4" }) }), _jsx(Button, { size: "icon", variant: "ghost", onClick: () => {
16
+ if (window.confirm(messages.channel.deleteConfirm)) {
17
+ void remove.mutateAsync(channel.id);
18
+ }
19
+ }, children: _jsx(Trash2, { className: "size-4" }) })] })] }, channel.id)))] }), creating && (_jsx(StageChannelEditorDialog, { reminderRuleId: reminderRuleId, stageId: stageId, channel: null, defaultOrderIndex: channels?.length ?? 0, open: creating, onOpenChange: setCreating })), editing && (_jsx(StageChannelEditorDialog, { reminderRuleId: reminderRuleId, stageId: stageId, channel: editing, open: Boolean(editing), onOpenChange: (open) => !open && setEditing(null) }))] }));
20
+ }
@@ -0,0 +1,10 @@
1
+ import { type ReminderRuleStageRecord } from "../index.js";
2
+ export interface StageEditorDialogProps {
3
+ reminderRuleId: string;
4
+ stage: ReminderRuleStageRecord | null;
5
+ defaultOrderIndex?: number;
6
+ open: boolean;
7
+ onOpenChange: (open: boolean) => void;
8
+ }
9
+ export declare function StageEditorDialog({ reminderRuleId, stage, defaultOrderIndex, open, onOpenChange, }: StageEditorDialogProps): import("react/jsx-runtime").JSX.Element;
10
+ //# sourceMappingURL=stage-editor-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage-editor-dialog.d.ts","sourceRoot":"","sources":["../../src/components/stage-editor-dialog.tsx"],"names":[],"mappings":"AA8BA,OAAO,EAAE,KAAK,uBAAuB,EAAgC,MAAM,aAAa,CAAA;AAuExF,MAAM,WAAW,sBAAsB;IACrC,cAAc,EAAE,MAAM,CAAA;IACtB,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAAA;IACrC,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;CACtC;AAED,wBAAgB,iBAAiB,CAAC,EAChC,cAAc,EACd,KAAK,EACL,iBAAqB,EACrB,IAAI,EACJ,YAAY,GACb,EAAE,sBAAsB,2CAkTxB"}
@@ -0,0 +1,104 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
4
+ import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet, FieldTitle, } from "@voyantjs/ui/components/field";
5
+ import { Switch } from "@voyantjs/ui/components/switch";
6
+ import { Loader2, Plus, Trash2 } from "lucide-react";
7
+ import { useEffect, useState } from "react";
8
+ import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
9
+ import { useReminderRuleStageMutation } from "../index.js";
10
+ let intervalRowSeq = 0;
11
+ const nextIntervalRowKey = () => `iv-${++intervalRowSeq}`;
12
+ const ANCHORS = [
13
+ "due_date",
14
+ "booking_created_at",
15
+ "departure_date",
16
+ "invoice_issued_at",
17
+ "last_send_at",
18
+ ];
19
+ const CADENCES = ["once", "every_n_days", "escalating"];
20
+ function fromRecord(stage, orderIndex) {
21
+ if (!stage) {
22
+ return {
23
+ name: "",
24
+ orderIndex,
25
+ anchor: "due_date",
26
+ windowStartDays: -7,
27
+ windowEndDays: 0,
28
+ cadenceKind: "once",
29
+ cadenceEveryDays: null,
30
+ cadenceIntervals: [],
31
+ maxSendsInStage: null,
32
+ respectQuietHours: true,
33
+ };
34
+ }
35
+ return {
36
+ name: stage.name ?? "",
37
+ orderIndex: stage.orderIndex,
38
+ anchor: stage.anchor,
39
+ windowStartDays: stage.windowStartDays,
40
+ windowEndDays: stage.windowEndDays,
41
+ cadenceKind: stage.cadenceKind,
42
+ cadenceEveryDays: stage.cadenceEveryDays,
43
+ cadenceIntervals: stage.cadenceIntervals?.map((i) => ({
44
+ rowKey: nextIntervalRowKey(),
45
+ whenDaysUntilDueGT: i.whenDaysUntilDueGT ?? null,
46
+ repeatEveryDays: i.repeatEveryDays,
47
+ })) ?? [],
48
+ maxSendsInStage: stage.maxSendsInStage,
49
+ respectQuietHours: stage.respectQuietHours,
50
+ };
51
+ }
52
+ export function StageEditorDialog({ reminderRuleId, stage, defaultOrderIndex = 0, open, onOpenChange, }) {
53
+ const messages = useNotificationsUiMessagesOrDefault();
54
+ const { create, update } = useReminderRuleStageMutation(reminderRuleId);
55
+ const [form, setForm] = useState(() => fromRecord(stage, defaultOrderIndex));
56
+ const isEdit = Boolean(stage);
57
+ const isPending = create.isPending || update.isPending;
58
+ useEffect(() => {
59
+ if (open)
60
+ setForm(fromRecord(stage, defaultOrderIndex));
61
+ }, [open, stage, defaultOrderIndex]);
62
+ const setField = (key, value) => setForm((prev) => ({ ...prev, [key]: value }));
63
+ const addInterval = () => setField("cadenceIntervals", [
64
+ ...form.cadenceIntervals,
65
+ { rowKey: nextIntervalRowKey(), whenDaysUntilDueGT: null, repeatEveryDays: 7 },
66
+ ]);
67
+ const removeInterval = (rowKey) => setField("cadenceIntervals", form.cadenceIntervals.filter((row) => row.rowKey !== rowKey));
68
+ const handleSubmit = async () => {
69
+ const input = {
70
+ name: form.name || null,
71
+ orderIndex: form.orderIndex,
72
+ anchor: form.anchor,
73
+ windowStartDays: form.windowStartDays,
74
+ windowEndDays: form.windowEndDays,
75
+ cadenceKind: form.cadenceKind,
76
+ cadenceEveryDays: form.cadenceKind === "every_n_days" ? form.cadenceEveryDays : null,
77
+ cadenceIntervals: form.cadenceKind === "escalating"
78
+ ? form.cadenceIntervals.map((row) => ({
79
+ whenDaysUntilDueGT: row.whenDaysUntilDueGT,
80
+ repeatEveryDays: row.repeatEveryDays,
81
+ }))
82
+ : null,
83
+ maxSendsInStage: form.maxSendsInStage,
84
+ respectQuietHours: form.respectQuietHours,
85
+ };
86
+ if (isEdit && stage) {
87
+ await update.mutateAsync({ stageId: stage.id, input });
88
+ }
89
+ else {
90
+ await create.mutateAsync(input);
91
+ }
92
+ onOpenChange(false);
93
+ };
94
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-2xl", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEdit ? messages.stage.titles.edit : messages.stage.titles.create }) }), _jsx(DialogBody, { children: _jsxs(FieldGroup, { children: [_jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-[1fr_8rem]", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "stage-name", children: messages.stage.fields.name }), _jsx(Input, { id: "stage-name", value: form.name, onChange: (e) => setField("name", e.target.value), placeholder: messages.stage.placeholders.name })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "stage-order", children: messages.stage.fields.orderIndex }), _jsx(Input, { id: "stage-order", type: "number", value: form.orderIndex, onChange: (e) => setField("orderIndex", Number(e.target.value)) })] })] }), _jsxs(FieldSet, { children: [_jsx(FieldLegend, { variant: "label", children: messages.stage.fields.anchor }), _jsx(FieldDescription, { children: messages.stage.descriptions.window }), _jsxs(FieldGroup, { className: "gap-4", children: [_jsx(Field, { children: _jsxs(Select, { value: form.anchor, onValueChange: (v) => setField("anchor", v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: ANCHORS.map((a) => (_jsx(SelectItem, { value: a, children: messages.stage.anchors[a] }, a))) })] }) }), _jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "window-start", children: messages.stage.fields.windowStartDays }), _jsx(Input, { id: "window-start", type: "number", value: form.windowStartDays, onChange: (e) => setField("windowStartDays", Number(e.target.value)) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "window-end", children: messages.stage.fields.windowEndDays }), _jsx(Input, { id: "window-end", type: "number", value: form.windowEndDays, onChange: (e) => setField("windowEndDays", Number(e.target.value)) })] })] })] })] }), _jsxs(FieldSet, { children: [_jsx(FieldLegend, { variant: "label", children: messages.stage.fields.cadenceKind }), _jsx(FieldDescription, { children: messages.stage.descriptions.cadence }), _jsxs(FieldGroup, { className: "gap-4", children: [_jsx(Field, { children: _jsxs(Select, { value: form.cadenceKind, onValueChange: (v) => setField("cadenceKind", v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: CADENCES.map((c) => (_jsx(SelectItem, { value: c, children: messages.stage.cadences[c] }, c))) })] }) }), form.cadenceKind === "every_n_days" && (_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "cadence-every", children: messages.stage.fields.cadenceEveryDays }), _jsx(Input, { id: "cadence-every", type: "number", min: 1, value: form.cadenceEveryDays ?? "", onChange: (e) => setField("cadenceEveryDays", e.target.value ? Number(e.target.value) : null) })] })), form.cadenceKind === "escalating" && (_jsxs(Field, { children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(FieldLabel, { children: messages.stage.fields.cadenceIntervals }), _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: addInterval, children: [_jsx(Plus, { className: "size-4" }), messages.stage.intervalRow.addInterval] })] }), form.cadenceIntervals.length === 0 ? (_jsx(FieldDescription, { children: messages.stage.descriptions.emptyIntervals })) : null, _jsx("div", { className: "space-y-2", children: form.cadenceIntervals.map((interval) => (_jsxs("div", { className: "grid grid-cols-[1fr_1fr_auto] items-end gap-2", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { className: "text-xs", children: messages.stage.intervalRow.whenDaysUntilDueGT }), _jsx(Input, { type: "number", value: interval.whenDaysUntilDueGT ?? "", onChange: (e) => setField("cadenceIntervals", form.cadenceIntervals.map((row) => row.rowKey === interval.rowKey
95
+ ? {
96
+ ...row,
97
+ whenDaysUntilDueGT: e.target.value
98
+ ? Number(e.target.value)
99
+ : null,
100
+ }
101
+ : row)) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { className: "text-xs", children: messages.stage.intervalRow.repeatEveryDays }), _jsx(Input, { type: "number", min: 1, value: interval.repeatEveryDays, onChange: (e) => setField("cadenceIntervals", form.cadenceIntervals.map((row) => row.rowKey === interval.rowKey
102
+ ? { ...row, repeatEveryDays: Number(e.target.value) }
103
+ : row)) })] }), _jsx(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => removeInterval(interval.rowKey), "aria-label": messages.stage.intervalRow.removeInterval, children: _jsx(Trash2, { className: "size-4" }) })] }, interval.rowKey))) })] }))] })] }), _jsxs(FieldSet, { children: [_jsx(FieldLegend, { variant: "label", children: messages.stage.descriptions.stopConditions }), _jsxs(FieldGroup, { className: "gap-4", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "max-sends", children: messages.stage.fields.maxSendsInStage }), _jsx(Input, { id: "max-sends", type: "number", min: 1, value: form.maxSendsInStage ?? "", onChange: (e) => setField("maxSendsInStage", e.target.value ? Number(e.target.value) : null), placeholder: messages.common.optionalPlaceholder }), _jsx(FieldDescription, { children: messages.stage.descriptions.maxSendsInStage })] }), _jsxs(Field, { orientation: "horizontal", children: [_jsx(Switch, { id: "respect-quiet-hours", checked: form.respectQuietHours, onCheckedChange: (v) => setField("respectQuietHours", Boolean(v)) }), _jsxs(FieldLabel, { htmlFor: "respect-quiet-hours", className: "!w-auto !flex-row", children: [_jsx(FieldTitle, { children: messages.stage.fields.respectQuietHours }), _jsx(FieldDescription, { children: messages.stage.descriptions.respectQuietHours })] })] })] })] })] }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: isPending, children: messages.common.cancel }), _jsxs(Button, { onClick: handleSubmit, disabled: isPending, children: [isPending ? _jsx(Loader2, { className: "size-4 animate-spin" }) : null, isEdit ? messages.common.save : messages.common.create] })] })] }) }));
104
+ }
@@ -0,0 +1,5 @@
1
+ export interface StageListProps {
2
+ reminderRuleId: string;
3
+ }
4
+ export declare function StageList({ reminderRuleId }: StageListProps): import("react/jsx-runtime").JSX.Element;
5
+ //# sourceMappingURL=stage-list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage-list.d.ts","sourceRoot":"","sources":["../../src/components/stage-list.tsx"],"names":[],"mappings":"AAcA,MAAM,WAAW,cAAc;IAC7B,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,wBAAgB,SAAS,CAAC,EAAE,cAAc,EAAE,EAAE,cAAc,2CAqH3D"}
@@ -0,0 +1,34 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components";
4
+ import { ArrowDown, ArrowUp, Pencil, Plus, Trash2 } from "lucide-react";
5
+ import { useState } from "react";
6
+ import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
7
+ import { useReminderRuleStageMutation, useReminderRuleStages, } from "../index.js";
8
+ import { StageChannelList } from "./stage-channel-list.js";
9
+ import { StageEditorDialog } from "./stage-editor-dialog.js";
10
+ export function StageList({ reminderRuleId }) {
11
+ const messages = useNotificationsUiMessagesOrDefault();
12
+ const { data: stages, isLoading } = useReminderRuleStages(reminderRuleId);
13
+ const { remove, reorder } = useReminderRuleStageMutation(reminderRuleId);
14
+ const [editing, setEditing] = useState(null);
15
+ const [creating, setCreating] = useState(false);
16
+ const move = async (index, direction) => {
17
+ if (!stages)
18
+ return;
19
+ const target = index + direction;
20
+ if (target < 0 || target >= stages.length)
21
+ return;
22
+ const next = [...stages];
23
+ const [item] = next.splice(index, 1);
24
+ if (!item)
25
+ return;
26
+ next.splice(target, 0, item);
27
+ await reorder.mutateAsync({ stageIds: next.map((s) => s.id) });
28
+ };
29
+ return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h3", { className: "text-base font-semibold", children: messages.stage.listHeading }), _jsxs(Button, { size: "sm", onClick: () => setCreating(true), children: [_jsx(Plus, { className: "size-4" }), " ", messages.stage.addStage] })] }), isLoading && _jsx("p", { className: "text-sm text-muted-foreground", children: messages.common.loading }), stages && stages.length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: messages.stage.listEmpty })), stages?.map((stage, index) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0 pb-3", children: [_jsxs(CardTitle, { className: "flex items-center gap-2 text-sm font-medium", children: [_jsxs(Badge, { variant: "outline", children: ["#", stage.orderIndex] }), stage.name ?? messages.stage.placeholders.name, _jsx(Badge, { variant: "secondary", children: messages.stage.anchors[stage.anchor] }), _jsx(Badge, { children: messages.stage.cadences[stage.cadenceKind] })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { size: "icon", variant: "ghost", onClick: () => move(index, -1), disabled: index === 0, children: _jsx(ArrowUp, { className: "size-4" }) }), _jsx(Button, { size: "icon", variant: "ghost", onClick: () => move(index, 1), disabled: index === stages.length - 1, children: _jsx(ArrowDown, { className: "size-4" }) }), _jsx(Button, { size: "icon", variant: "ghost", onClick: () => setEditing(stage), children: _jsx(Pencil, { className: "size-4" }) }), _jsx(Button, { size: "icon", variant: "ghost", onClick: () => {
30
+ if (window.confirm(messages.stage.deleteConfirm)) {
31
+ void remove.mutateAsync(stage.id);
32
+ }
33
+ }, children: _jsx(Trash2, { className: "size-4" }) })] })] }), _jsxs(CardContent, { className: "space-y-3 text-sm", children: [_jsxs("div", { className: "flex flex-wrap gap-x-6 gap-y-1 text-muted-foreground", children: [_jsxs("span", { children: [messages.stage.fields.windowStartDays, ": ", stage.windowStartDays] }), _jsxs("span", { children: [messages.stage.fields.windowEndDays, ": ", stage.windowEndDays] }), stage.cadenceKind === "every_n_days" && (_jsxs("span", { children: [messages.stage.fields.cadenceEveryDays, ": ", stage.cadenceEveryDays] })), stage.maxSendsInStage != null && (_jsxs("span", { children: [messages.stage.fields.maxSendsInStage, ": ", stage.maxSendsInStage] }))] }), _jsx(StageChannelList, { reminderRuleId: reminderRuleId, stageId: stage.id })] })] }, stage.id))), creating && (_jsx(StageEditorDialog, { reminderRuleId: reminderRuleId, stage: null, defaultOrderIndex: stages?.length ?? 0, open: creating, onOpenChange: setCreating })), editing && (_jsx(StageEditorDialog, { reminderRuleId: reminderRuleId, stage: editing, open: Boolean(editing), onOpenChange: (open) => !open && setEditing(null) }))] }));
34
+ }
@@ -0,0 +1,19 @@
1
+ import { type NotificationTemplateRecord } from "../index.js";
2
+ export interface TemplatePickerProps {
3
+ /** Currently selected template id (or null when nothing is picked). */
4
+ value: string | null;
5
+ onChange: (value: string | null) => void;
6
+ /** Restrict results to templates registered for this channel. */
7
+ channel?: NotificationTemplateRecord["channel"];
8
+ placeholder?: string;
9
+ emptyText?: string;
10
+ disabled?: boolean;
11
+ }
12
+ /**
13
+ * Async-friendly template picker. Searches active templates by name / slug
14
+ * filtered by the chosen channel and resolves to the template id. The
15
+ * currently selected template is fetched separately so its label keeps
16
+ * rendering even after the search list filters it out.
17
+ */
18
+ export declare function TemplatePicker({ value, onChange, channel, placeholder, emptyText, disabled, }: TemplatePickerProps): import("react/jsx-runtime").JSX.Element;
19
+ //# sourceMappingURL=template-picker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-picker.d.ts","sourceRoot":"","sources":["../../src/components/template-picker.tsx"],"names":[],"mappings":"AAKA,OAAO,EACL,KAAK,0BAA0B,EAGhC,MAAM,aAAa,CAAA;AAEpB,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACxC,iEAAiE;IACjE,OAAO,CAAC,EAAE,0BAA0B,CAAC,SAAS,CAAC,CAAA;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,QAAQ,EACR,OAAO,EACP,WAAW,EACX,SAAS,EACT,QAAQ,GACT,EAAE,mBAAmB,2CA8BrB"}
@@ -0,0 +1,26 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { AsyncCombobox } from "@voyantjs/ui/components/async-combobox";
4
+ import * as React from "react";
5
+ import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
6
+ import { useNotificationTemplate, useNotificationTemplates, } from "../index.js";
7
+ /**
8
+ * Async-friendly template picker. Searches active templates by name / slug
9
+ * filtered by the chosen channel and resolves to the template id. The
10
+ * currently selected template is fetched separately so its label keeps
11
+ * rendering even after the search list filters it out.
12
+ */
13
+ export function TemplatePicker({ value, onChange, channel, placeholder, emptyText, disabled, }) {
14
+ const messages = useNotificationsUiMessagesOrDefault().pickers.templates;
15
+ const [search, setSearch] = React.useState("");
16
+ const { data } = useNotificationTemplates({
17
+ channel,
18
+ status: "active",
19
+ search: search || undefined,
20
+ limit: 20,
21
+ offset: 0,
22
+ });
23
+ const { data: selected } = useNotificationTemplate(value ?? "", { enabled: Boolean(value) });
24
+ const items = data?.data ?? [];
25
+ return (_jsx(AsyncCombobox, { value: value, onChange: onChange, items: items, selectedItem: selected ?? null, getKey: (template) => template.id, getLabel: (template) => template.name, getSecondary: (template) => template.slug, onSearchChange: setSearch, placeholder: placeholder ?? messages.placeholder, emptyText: emptyText ?? messages.empty, disabled: disabled, clearable: true }));
26
+ }
@@ -0,0 +1,9 @@
1
+ export interface TimezoneComboboxProps {
2
+ value: string | null | undefined;
3
+ onChange: (value: string | null) => void;
4
+ placeholder?: string;
5
+ emptyText?: string;
6
+ disabled?: boolean;
7
+ }
8
+ export declare function TimezoneCombobox({ value, onChange, placeholder, emptyText, disabled, }: TimezoneComboboxProps): import("react/jsx-runtime").JSX.Element;
9
+ //# sourceMappingURL=timezone-combobox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timezone-combobox.d.ts","sourceRoot":"","sources":["../../src/components/timezone-combobox.tsx"],"names":[],"mappings":"AA+DA,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACxC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,QAAQ,EACR,WAAW,EACX,SAAS,EACT,QAAQ,GACT,EAAE,qBAAqB,2CAyCvB"}
@@ -0,0 +1,67 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
4
+ import * as React from "react";
5
+ import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
6
+ const TIMEZONES = (() => {
7
+ const intl = Intl;
8
+ if (typeof intl.supportedValuesOf === "function") {
9
+ try {
10
+ return [...intl.supportedValuesOf("timeZone")].sort();
11
+ }
12
+ catch {
13
+ // fall through
14
+ }
15
+ }
16
+ return [
17
+ "UTC",
18
+ "Africa/Cairo",
19
+ "Africa/Johannesburg",
20
+ "Africa/Lagos",
21
+ "America/Chicago",
22
+ "America/Denver",
23
+ "America/Los_Angeles",
24
+ "America/Mexico_City",
25
+ "America/New_York",
26
+ "America/Sao_Paulo",
27
+ "Asia/Bangkok",
28
+ "Asia/Dubai",
29
+ "Asia/Hong_Kong",
30
+ "Asia/Jakarta",
31
+ "Asia/Kolkata",
32
+ "Asia/Manila",
33
+ "Asia/Seoul",
34
+ "Asia/Shanghai",
35
+ "Asia/Singapore",
36
+ "Asia/Tokyo",
37
+ "Australia/Sydney",
38
+ "Europe/Amsterdam",
39
+ "Europe/Berlin",
40
+ "Europe/Bucharest",
41
+ "Europe/Istanbul",
42
+ "Europe/Lisbon",
43
+ "Europe/London",
44
+ "Europe/Madrid",
45
+ "Europe/Moscow",
46
+ "Europe/Paris",
47
+ "Europe/Rome",
48
+ "Europe/Vienna",
49
+ "Pacific/Auckland",
50
+ ];
51
+ })();
52
+ export function TimezoneCombobox({ value, onChange, placeholder, emptyText, disabled, }) {
53
+ const messages = useNotificationsUiMessagesOrDefault().pickers.timezones;
54
+ const [inputValue, setInputValue] = React.useState(value ?? "");
55
+ React.useEffect(() => {
56
+ setInputValue(value ?? "");
57
+ }, [value]);
58
+ return (_jsxs(Combobox, { items: TIMEZONES, value: value ?? null, inputValue: inputValue, autoHighlight: true, disabled: disabled, itemToStringLabel: (item) => item, itemToStringValue: (item) => item, onInputValueChange: (next) => {
59
+ setInputValue(next);
60
+ if (!next)
61
+ onChange(null);
62
+ }, onValueChange: (next) => {
63
+ const tz = next ?? null;
64
+ onChange(tz);
65
+ setInputValue(tz ?? "");
66
+ }, children: [_jsx(ComboboxInput, { placeholder: placeholder ?? messages.placeholder, showClear: Boolean(value) }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: emptyText ?? messages.empty }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (tz) => (_jsx(ComboboxItem, { value: tz, children: tz }, tz)) }) })] })] }));
67
+ }
@@ -0,0 +1,3 @@
1
+ import type { NotificationsUiMessages } from "./messages.js";
2
+ export declare const notificationsUiEn: NotificationsUiMessages;
3
+ //# sourceMappingURL=en.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AAE5D,eAAO,MAAM,iBAAiB,EAAE,uBA6Y/B,CAAA"}