@nebulit/embuilder 0.1.44 → 0.1.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/.claude/skills/slice-state-view/SKILL.md +0 -6
- package/templates/frontend/prompt.md +1 -1
- package/templates/frontend/setup-env.sh +43 -0
- package/templates/frontend/src/App.tsx +12 -24
- package/templates/frontend/src/lib/api-client.ts +60 -98
- package/templates/frontend/src/lib/api.ts +28 -1028
- package/templates/frontend/src/pages/Dashboard.tsx +7 -144
- package/templates/frontend/src/pages/Register.tsx +3 -3
- package/templates/.claude/skills/sample-slices/templates/.slices/Library/addbook/code-slice.json +0 -124
- package/templates/frontend/src/components/tables/ReservationTemplates.tsx +0 -189
- package/templates/frontend/src/pages/Menus.tsx +0 -224
|
@@ -1,168 +1,31 @@
|
|
|
1
1
|
import {DashboardLayout} from "@/components/layout/DashboardLayout";
|
|
2
|
-
import {
|
|
3
|
-
import {Users, Calendar, ClipboardList} from "lucide-react";
|
|
4
|
-
import {mockClerks} from "@/data/mock-data";
|
|
2
|
+
import {Calendar, ClipboardList} from "lucide-react";
|
|
5
3
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
|
6
|
-
import {Badge} from "@/components/ui/badge";
|
|
7
|
-
import {useActiveShiftsForDashboard} from "@/hooks/api/useShifts";
|
|
8
|
-
import {useActiveTasksForDashboard} from "@/hooks/api/useTasks";
|
|
9
|
-
import {useClerks} from "@/hooks/api/useClerks";
|
|
10
|
-
import {useUpcomingReservations} from "@/hooks/api/useReservations";
|
|
11
|
-
import {format} from "date-fns";
|
|
12
|
-
import {DashboardCalendar, CalendarEntry} from "@/components/calendar/Calendar";
|
|
13
|
-
import {useMemo, useCallback} from "react";
|
|
14
4
|
import {useApiContext} from "@/hooks/useApiContext";
|
|
15
|
-
import {registerNoShow, registerShowUp} from "@/lib/api";
|
|
16
5
|
|
|
17
6
|
export default function Dashboard() {
|
|
18
7
|
const ctx = useApiContext();
|
|
19
|
-
const {data: activeShifts = [], isLoading: shiftsLoading} = useActiveShiftsForDashboard();
|
|
20
|
-
const {data: activeTasks = [], isLoading: tasksLoading} = useActiveTasksForDashboard();
|
|
21
|
-
const {data: clerks = []} = useClerks();
|
|
22
|
-
const {data: upcomingReservations = []} = useUpcomingReservations();
|
|
23
|
-
const activeStaff = mockClerks.filter((c) => c.active).length;
|
|
8
|
+
//const {data: activeShifts = [], isLoading: shiftsLoading} = useActiveShiftsForDashboard();
|
|
24
9
|
|
|
25
|
-
const
|
|
26
|
-
const clerk = clerks.find((c) => c.clerkId === clerkId);
|
|
27
|
-
return clerk ? `${clerk.name} ${clerk.surname}` : clerkId;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const calendarEntries = useMemo<CalendarEntry[]>(() => {
|
|
31
|
-
return upcomingReservations.map((r) => ({
|
|
32
|
-
type: "reservation",
|
|
33
|
-
id: r.reservation_id,
|
|
34
|
-
title: r.name || r.reservation_id,
|
|
35
|
-
description: r.description,
|
|
36
|
-
start: r.start_date,
|
|
37
|
-
email: r.email,
|
|
38
|
-
phone: r.phone,
|
|
39
|
-
end: r.end_date,
|
|
40
|
-
showupRegistered: r.showup_registered,
|
|
41
|
-
}));
|
|
42
|
-
}, [upcomingReservations]);
|
|
43
|
-
|
|
44
|
-
const handleNoShow = useCallback((reservationId: string) => {
|
|
10
|
+
/*const handleNoShow = useCallback((reservationId: string) => {
|
|
45
11
|
registerNoShow(reservationId, ctx).catch(console.error);
|
|
46
|
-
}, [ctx])
|
|
47
|
-
|
|
48
|
-
const handleShowUp = useCallback((reservationId: string) => {
|
|
49
|
-
registerShowUp(reservationId, ctx).catch(console.error);
|
|
50
|
-
}, [ctx]);
|
|
12
|
+
}, [ctx]);*/
|
|
51
13
|
|
|
52
14
|
return (
|
|
53
15
|
<DashboardLayout title="Overview" subtitle="Welcome back! Here is your summary.">
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<StatCard
|
|
57
|
-
title="Active Staff"
|
|
58
|
-
value={activeStaff}
|
|
59
|
-
icon={Users}
|
|
60
|
-
change={`${mockClerks.length - activeStaff} inactive`}
|
|
61
|
-
changeType="neutral"
|
|
62
|
-
/>
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
{/* Quick Actions & Recent Activity */}
|
|
16
|
+
<div></div>
|
|
17
|
+
{/* Quick Actions & Recent Activity
|
|
66
18
|
<div className="mt-8 grid gap-6 lg:grid-cols-2">
|
|
67
|
-
{/* Active Shifts */}
|
|
68
19
|
<Card className="animate-fade-in">
|
|
69
20
|
<CardHeader>
|
|
70
21
|
<CardTitle className="flex items-center gap-2 text-lg">
|
|
71
22
|
<Calendar className="h-5 w-5 text-primary"/>
|
|
72
|
-
Active Shifts Today
|
|
73
23
|
</CardTitle>
|
|
74
24
|
</CardHeader>
|
|
75
25
|
<CardContent>
|
|
76
|
-
<div className="space-y-4">
|
|
77
|
-
{shiftsLoading ? (
|
|
78
|
-
<p className="text-sm text-muted-foreground">Loading...</p>
|
|
79
|
-
) : activeShifts.length === 0 ? (
|
|
80
|
-
<p className="text-sm text-muted-foreground">No active shifts</p>
|
|
81
|
-
) : (
|
|
82
|
-
activeShifts.map((shift) => (
|
|
83
|
-
<div
|
|
84
|
-
key={shift.shiftId}
|
|
85
|
-
className="flex items-center justify-between rounded-lg border border-border bg-muted/30 p-4"
|
|
86
|
-
>
|
|
87
|
-
<div>
|
|
88
|
-
<p className="font-medium text-foreground">{shift.name}</p>
|
|
89
|
-
<p className="text-sm text-muted-foreground">{shift.fromTo}</p>
|
|
90
|
-
{shift.assignees.length > 0 && (
|
|
91
|
-
<p className="mt-1 text-xs text-muted-foreground">
|
|
92
|
-
{shift.assignees.map(getClerkName).join(", ")}
|
|
93
|
-
</p>
|
|
94
|
-
)}
|
|
95
|
-
</div>
|
|
96
|
-
<Badge variant="secondary" className="bg-success/10 text-success">
|
|
97
|
-
Active
|
|
98
|
-
</Badge>
|
|
99
|
-
</div>
|
|
100
|
-
))
|
|
101
|
-
)}
|
|
102
|
-
</div>
|
|
103
26
|
</CardContent>
|
|
104
27
|
</Card>
|
|
105
|
-
</div
|
|
106
|
-
|
|
107
|
-
{/* Pending Tasks */}
|
|
108
|
-
<Card className="mt-6 animate-fade-in">
|
|
109
|
-
<CardHeader>
|
|
110
|
-
<CardTitle className="flex items-center gap-2 text-lg">
|
|
111
|
-
<ClipboardList className="h-5 w-5 text-primary"/>
|
|
112
|
-
Pending Tasks
|
|
113
|
-
</CardTitle>
|
|
114
|
-
</CardHeader>
|
|
115
|
-
<CardContent>
|
|
116
|
-
{tasksLoading ? (
|
|
117
|
-
<p className="text-sm text-muted-foreground">Loading...</p>
|
|
118
|
-
) : activeTasks.length === 0 ? (
|
|
119
|
-
<p className="text-sm text-muted-foreground">No active tasks</p>
|
|
120
|
-
) : (
|
|
121
|
-
<div className="overflow-x-auto">
|
|
122
|
-
<table className="w-full">
|
|
123
|
-
<thead>
|
|
124
|
-
<tr className="border-b border-border">
|
|
125
|
-
<th className="pb-3 text-left text-sm font-medium text-muted-foreground">Task</th>
|
|
126
|
-
<th className="pb-3 text-left text-sm font-medium text-muted-foreground">Assigned to</th>
|
|
127
|
-
<th className="pb-3 text-left text-sm font-medium text-muted-foreground">Due date</th>
|
|
128
|
-
<th className="pb-3 text-left text-sm font-medium text-muted-foreground">Recurrence</th>
|
|
129
|
-
</tr>
|
|
130
|
-
</thead>
|
|
131
|
-
<tbody>
|
|
132
|
-
{activeTasks.map((task) => (
|
|
133
|
-
<tr key={task.taskId} className="border-b border-border/50 last:border-0">
|
|
134
|
-
<td className="py-4">
|
|
135
|
-
<p className="font-medium text-foreground">{task.title}</p>
|
|
136
|
-
<p className="text-sm text-muted-foreground">{task.description}</p>
|
|
137
|
-
</td>
|
|
138
|
-
<td className="py-4 text-foreground">
|
|
139
|
-
{task.assignedClerk ? getClerkName(task.assignedClerk) : "Unassigned"}
|
|
140
|
-
</td>
|
|
141
|
-
<td className="py-4 text-foreground">
|
|
142
|
-
{task.date ? format(new Date(task.date), "MM/dd/yyyy") : "–"}
|
|
143
|
-
</td>
|
|
144
|
-
<td className="py-4">
|
|
145
|
-
{task.repeats ? (
|
|
146
|
-
<Badge variant="outline">{task.repeats}</Badge>
|
|
147
|
-
) : (
|
|
148
|
-
<span className="text-sm text-muted-foreground">Once</span>
|
|
149
|
-
)}
|
|
150
|
-
</td>
|
|
151
|
-
</tr>
|
|
152
|
-
))}
|
|
153
|
-
</tbody>
|
|
154
|
-
</table>
|
|
155
|
-
</div>
|
|
156
|
-
)}
|
|
157
|
-
</CardContent>
|
|
158
|
-
</Card>
|
|
159
|
-
|
|
160
|
-
<DashboardCalendar
|
|
161
|
-
entries={calendarEntries}
|
|
162
|
-
onAppointmentClick={() => {}}
|
|
163
|
-
onNoShow={handleNoShow}
|
|
164
|
-
onShowUp={handleShowUp}
|
|
165
|
-
/>
|
|
28
|
+
</div>*/}
|
|
166
29
|
</DashboardLayout>
|
|
167
30
|
);
|
|
168
31
|
}
|
|
@@ -8,7 +8,7 @@ import { useToast } from '@/hooks/use-toast';
|
|
|
8
8
|
import { Check, ChevronLeft } from 'lucide-react';
|
|
9
9
|
import { supabase } from '@/integrations/supabase/client';
|
|
10
10
|
import { useAuth } from '@/contexts/AuthContext';
|
|
11
|
-
import { registerTenant } from "@/lib/api";
|
|
11
|
+
//import { registerTenant } from "@/lib/api";
|
|
12
12
|
import { v4 } from "uuid";
|
|
13
13
|
|
|
14
14
|
const Register = () => {
|
|
@@ -113,11 +113,11 @@ const Register = () => {
|
|
|
113
113
|
const token = sessionData?.session?.access_token ?? "";
|
|
114
114
|
const tenantId = v4();
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
/* await registerTenant({
|
|
117
117
|
tenantId,
|
|
118
118
|
name: tenantName,
|
|
119
119
|
ownerId: ownerId,
|
|
120
|
-
}, { token, userId: ownerId, tenantId })
|
|
120
|
+
}, { token, userId: ownerId, tenantId });*/
|
|
121
121
|
|
|
122
122
|
toast({
|
|
123
123
|
title: 'Success',
|
package/templates/.claude/skills/sample-slices/templates/.slices/Library/addbook/code-slice.json
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "3458764661593137476",
|
|
3
|
-
"title": "slice: Add Book",
|
|
4
|
-
"specifications": [
|
|
5
|
-
{
|
|
6
|
-
"vertical": false,
|
|
7
|
-
"id": "GENERATED-SPEC-001",
|
|
8
|
-
"sliceName": "slice: Add Book",
|
|
9
|
-
"type": "COMMAND",
|
|
10
|
-
"title": "should create BookAdded event when adding a book",
|
|
11
|
-
"given": [],
|
|
12
|
-
"when": [
|
|
13
|
-
{
|
|
14
|
-
"id": "GENERATED-WHEN-001",
|
|
15
|
-
"linkedId": "3458764661591564785",
|
|
16
|
-
"title": "Add Book",
|
|
17
|
-
"type": "COMMAND",
|
|
18
|
-
"fields": [
|
|
19
|
-
{
|
|
20
|
-
"name": "title",
|
|
21
|
-
"type": "String",
|
|
22
|
-
"cardinality": "Single",
|
|
23
|
-
"example": "Harry Potter",
|
|
24
|
-
"subfields": [],
|
|
25
|
-
"idAttribute": false
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
"name": "author",
|
|
29
|
-
"type": "String",
|
|
30
|
-
"cardinality": "Single",
|
|
31
|
-
"example": "J.K. Rowling",
|
|
32
|
-
"subfields": [],
|
|
33
|
-
"idAttribute": false
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
"name": "description",
|
|
37
|
-
"type": "String",
|
|
38
|
-
"cardinality": "Single",
|
|
39
|
-
"example": "A magical adventure",
|
|
40
|
-
"subfields": [],
|
|
41
|
-
"idAttribute": false
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"name": "isbn",
|
|
45
|
-
"type": "String",
|
|
46
|
-
"cardinality": "Single",
|
|
47
|
-
"example": "1234567890",
|
|
48
|
-
"subfields": [],
|
|
49
|
-
"idAttribute": false
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
"name": "user",
|
|
53
|
-
"type": "String",
|
|
54
|
-
"cardinality": "Single",
|
|
55
|
-
"example": "john@example.com",
|
|
56
|
-
"subfields": [],
|
|
57
|
-
"idAttribute": false
|
|
58
|
-
}
|
|
59
|
-
]
|
|
60
|
-
}
|
|
61
|
-
],
|
|
62
|
-
"then": [
|
|
63
|
-
{
|
|
64
|
-
"id": "GENERATED-THEN-001",
|
|
65
|
-
"linkedId": "3458764661586330620",
|
|
66
|
-
"title": "Book added",
|
|
67
|
-
"type": "SPEC_EVENT",
|
|
68
|
-
"fields": [
|
|
69
|
-
{
|
|
70
|
-
"name": "id",
|
|
71
|
-
"type": "String",
|
|
72
|
-
"cardinality": "Single",
|
|
73
|
-
"subfields": [],
|
|
74
|
-
"idAttribute": false,
|
|
75
|
-
"generated": true
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
"name": "title",
|
|
79
|
-
"type": "String",
|
|
80
|
-
"cardinality": "Single",
|
|
81
|
-
"example": "Harry Potter",
|
|
82
|
-
"subfields": [],
|
|
83
|
-
"idAttribute": false
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
"name": "author",
|
|
87
|
-
"type": "String",
|
|
88
|
-
"cardinality": "Single",
|
|
89
|
-
"example": "J.K. Rowling",
|
|
90
|
-
"subfields": [],
|
|
91
|
-
"idAttribute": false
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
"name": "description",
|
|
95
|
-
"type": "String",
|
|
96
|
-
"cardinality": "Single",
|
|
97
|
-
"example": "A magical adventure",
|
|
98
|
-
"subfields": [],
|
|
99
|
-
"idAttribute": false
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
"name": "isbn",
|
|
103
|
-
"type": "String",
|
|
104
|
-
"cardinality": "Single",
|
|
105
|
-
"example": "1234567890",
|
|
106
|
-
"subfields": [],
|
|
107
|
-
"idAttribute": false
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
"name": "user",
|
|
111
|
-
"type": "String",
|
|
112
|
-
"cardinality": "Single",
|
|
113
|
-
"example": "john@example.com",
|
|
114
|
-
"subfields": [],
|
|
115
|
-
"idAttribute": false
|
|
116
|
-
}
|
|
117
|
-
]
|
|
118
|
-
}
|
|
119
|
-
],
|
|
120
|
-
"comments": [],
|
|
121
|
-
"linkedId": "GENERATED-SPEC-001"
|
|
122
|
-
}
|
|
123
|
-
]
|
|
124
|
-
}
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import { useRef, useState, useEffect } from "react";
|
|
2
|
-
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
3
|
-
import { Button } from "@/components/ui/button";
|
|
4
|
-
import { Textarea } from "@/components/ui/textarea";
|
|
5
|
-
import { Label } from "@/components/ui/label";
|
|
6
|
-
import { Loader2, Mail, Phone } from "lucide-react";
|
|
7
|
-
import { toast } from "sonner";
|
|
8
|
-
import { useReservationTemplates, useSaveReservationTemplate } from "@/hooks/api/useReservationTemplates";
|
|
9
|
-
import { Skeleton } from "@/components/ui/skeleton";
|
|
10
|
-
|
|
11
|
-
const TEMPLATE_VARIABLES = [
|
|
12
|
-
{ key: "name", label: "Name" },
|
|
13
|
-
{ key: "from", label: "From" },
|
|
14
|
-
{ key: "to", label: "To" },
|
|
15
|
-
{ key: "persons", label: "Persons" },
|
|
16
|
-
{ key: "location", label: "Location" },
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
export function ReservationTemplates() {
|
|
20
|
-
const { data: templates = [], isLoading } = useReservationTemplates();
|
|
21
|
-
const saveMutation = useSaveReservationTemplate();
|
|
22
|
-
|
|
23
|
-
const [emailTemplate, setEmailTemplate] = useState("");
|
|
24
|
-
const [phoneTemplate, setPhoneTemplate] = useState("");
|
|
25
|
-
const [focusedField, setFocusedField] = useState<"email" | "phone" | null>(null);
|
|
26
|
-
|
|
27
|
-
const emailRef = useRef<HTMLTextAreaElement>(null);
|
|
28
|
-
const phoneRef = useRef<HTMLTextAreaElement>(null);
|
|
29
|
-
|
|
30
|
-
// Load templates when data is fetched
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
const emailTpl = templates.find(t => t.templateType === "EMAIL");
|
|
33
|
-
const phoneTpl = templates.find(t => t.templateType === "PHONE");
|
|
34
|
-
|
|
35
|
-
if (emailTpl) setEmailTemplate(emailTpl.template);
|
|
36
|
-
if (phoneTpl) setPhoneTemplate(phoneTpl.template);
|
|
37
|
-
}, [templates]);
|
|
38
|
-
|
|
39
|
-
const insertVariable = (variable: string) => {
|
|
40
|
-
const insertion = `{${variable}}`;
|
|
41
|
-
|
|
42
|
-
if (focusedField === "email" && emailRef.current) {
|
|
43
|
-
const textarea = emailRef.current;
|
|
44
|
-
const start = textarea.selectionStart;
|
|
45
|
-
const end = textarea.selectionEnd;
|
|
46
|
-
const newValue = emailTemplate.slice(0, start) + insertion + emailTemplate.slice(end);
|
|
47
|
-
setEmailTemplate(newValue);
|
|
48
|
-
|
|
49
|
-
// Restore cursor position after insertion
|
|
50
|
-
setTimeout(() => {
|
|
51
|
-
textarea.focus();
|
|
52
|
-
textarea.setSelectionRange(start + insertion.length, start + insertion.length);
|
|
53
|
-
}, 0);
|
|
54
|
-
} else if (focusedField === "phone" && phoneRef.current) {
|
|
55
|
-
const textarea = phoneRef.current;
|
|
56
|
-
const start = textarea.selectionStart;
|
|
57
|
-
const end = textarea.selectionEnd;
|
|
58
|
-
const newValue = phoneTemplate.slice(0, start) + insertion + phoneTemplate.slice(end);
|
|
59
|
-
setPhoneTemplate(newValue);
|
|
60
|
-
|
|
61
|
-
setTimeout(() => {
|
|
62
|
-
textarea.focus();
|
|
63
|
-
textarea.setSelectionRange(start + insertion.length, start + insertion.length);
|
|
64
|
-
}, 0);
|
|
65
|
-
} else {
|
|
66
|
-
toast.error("Please select a text field first");
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const handleSaveEmail = async () => {
|
|
71
|
-
const existingTemplate = templates.find(t => t.templateType === "EMAIL");
|
|
72
|
-
const templateId = existingTemplate?.templateId || `tpl-email-${Date.now()}`;
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
await saveMutation.mutateAsync({
|
|
76
|
-
templateId,
|
|
77
|
-
templateType: "EMAIL",
|
|
78
|
-
template: emailTemplate,
|
|
79
|
-
});
|
|
80
|
-
toast.success("Email template saved");
|
|
81
|
-
} catch (err) {
|
|
82
|
-
toast.error(`Error: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const handleSavePhone = async () => {
|
|
87
|
-
const existingTemplate = templates.find(t => t.templateType === "PHONE");
|
|
88
|
-
const templateId = existingTemplate?.templateId || `tpl-phone-${Date.now()}`;
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
await saveMutation.mutateAsync({
|
|
92
|
-
templateId,
|
|
93
|
-
templateType: "PHONE",
|
|
94
|
-
template: phoneTemplate,
|
|
95
|
-
});
|
|
96
|
-
toast.success("SMS template saved");
|
|
97
|
-
} catch (err) {
|
|
98
|
-
toast.error(`Error: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
if (isLoading) {
|
|
103
|
-
return (
|
|
104
|
-
<Card>
|
|
105
|
-
<CardHeader>
|
|
106
|
-
<Skeleton className="h-6 w-48" />
|
|
107
|
-
</CardHeader>
|
|
108
|
-
<CardContent className="space-y-4">
|
|
109
|
-
<Skeleton className="h-32 w-full" />
|
|
110
|
-
</CardContent>
|
|
111
|
-
</Card>
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
<Card>
|
|
117
|
-
<CardHeader>
|
|
118
|
-
<CardTitle>Notification Templates</CardTitle>
|
|
119
|
-
</CardHeader>
|
|
120
|
-
<CardContent className="space-y-6">
|
|
121
|
-
{/* Variable buttons */}
|
|
122
|
-
<div className="flex flex-wrap gap-2">
|
|
123
|
-
{TEMPLATE_VARIABLES.map((variable) => (
|
|
124
|
-
<Button
|
|
125
|
-
key={variable.key}
|
|
126
|
-
variant="outline"
|
|
127
|
-
size="sm"
|
|
128
|
-
onClick={() => insertVariable(variable.key)}
|
|
129
|
-
className="text-xs"
|
|
130
|
-
>
|
|
131
|
-
{variable.label}
|
|
132
|
-
</Button>
|
|
133
|
-
))}
|
|
134
|
-
</div>
|
|
135
|
-
|
|
136
|
-
{/* Templates grid */}
|
|
137
|
-
<div className="grid gap-6 md:grid-cols-2">
|
|
138
|
-
{/* Email template */}
|
|
139
|
-
<div className="space-y-3">
|
|
140
|
-
<div className="flex items-center gap-2">
|
|
141
|
-
<Mail className="h-4 w-4 text-muted-foreground" />
|
|
142
|
-
<Label>Email</Label>
|
|
143
|
-
</div>
|
|
144
|
-
<Textarea
|
|
145
|
-
ref={emailRef}
|
|
146
|
-
value={emailTemplate}
|
|
147
|
-
onChange={(e) => setEmailTemplate(e.target.value)}
|
|
148
|
-
onFocus={() => setFocusedField("email")}
|
|
149
|
-
placeholder="Enter email template..."
|
|
150
|
-
className="min-h-[150px] resize-none"
|
|
151
|
-
/>
|
|
152
|
-
<Button
|
|
153
|
-
onClick={handleSaveEmail}
|
|
154
|
-
disabled={saveMutation.isPending}
|
|
155
|
-
className="w-full"
|
|
156
|
-
>
|
|
157
|
-
{saveMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
158
|
-
Save
|
|
159
|
-
</Button>
|
|
160
|
-
</div>
|
|
161
|
-
|
|
162
|
-
{/* Phone/SMS template */}
|
|
163
|
-
<div className="space-y-3">
|
|
164
|
-
<div className="flex items-center gap-2">
|
|
165
|
-
<Phone className="h-4 w-4 text-muted-foreground" />
|
|
166
|
-
<Label>SMS</Label>
|
|
167
|
-
</div>
|
|
168
|
-
<Textarea
|
|
169
|
-
ref={phoneRef}
|
|
170
|
-
value={phoneTemplate}
|
|
171
|
-
onChange={(e) => setPhoneTemplate(e.target.value)}
|
|
172
|
-
onFocus={() => setFocusedField("phone")}
|
|
173
|
-
placeholder="Enter SMS template..."
|
|
174
|
-
className="min-h-[150px] resize-none"
|
|
175
|
-
/>
|
|
176
|
-
<Button
|
|
177
|
-
onClick={handleSavePhone}
|
|
178
|
-
disabled={saveMutation.isPending}
|
|
179
|
-
className="w-full"
|
|
180
|
-
>
|
|
181
|
-
{saveMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
182
|
-
Save
|
|
183
|
-
</Button>
|
|
184
|
-
</div>
|
|
185
|
-
</div>
|
|
186
|
-
</CardContent>
|
|
187
|
-
</Card>
|
|
188
|
-
);
|
|
189
|
-
}
|