@crmy/web 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.html +23 -0
- package/package.json +76 -0
- package/postcss.config.js +6 -0
- package/public/android-chrome-192x192.png +0 -0
- package/public/android-chrome-512x512.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +13 -0
- package/public/site.webmanifest +1 -0
- package/src/App.tsx +158 -0
- package/src/api/client.ts +82 -0
- package/src/api/hooks.ts +689 -0
- package/src/assets/crmy-logo.png +0 -0
- package/src/components/CustomFields.tsx +240 -0
- package/src/components/NavLink.tsx +28 -0
- package/src/components/crm/AIFab.tsx +37 -0
- package/src/components/crm/AccountDrawer.tsx +372 -0
- package/src/components/crm/ActivityTimeline.tsx +115 -0
- package/src/components/crm/AssignmentDrawer.tsx +396 -0
- package/src/components/crm/BriefingPanel.tsx +217 -0
- package/src/components/crm/CommandPalette.tsx +254 -0
- package/src/components/crm/ContactAvatar.tsx +49 -0
- package/src/components/crm/ContactDrawer.tsx +438 -0
- package/src/components/crm/ContextPanel.tsx +200 -0
- package/src/components/crm/CrmWidgets.tsx +417 -0
- package/src/components/crm/DrawerShell.tsx +77 -0
- package/src/components/crm/ListToolbar.tsx +252 -0
- package/src/components/crm/OpportunityDrawer.tsx +372 -0
- package/src/components/crm/PaginationBar.tsx +111 -0
- package/src/components/crm/QuickAddDrawer.tsx +652 -0
- package/src/components/crm/ShortcutsOverlay.tsx +65 -0
- package/src/components/crm/UseCaseDrawer.tsx +454 -0
- package/src/components/layout/MobileNav.tsx +49 -0
- package/src/components/layout/Sidebar.tsx +157 -0
- package/src/components/layout/TopBar.tsx +54 -0
- package/src/components/settings/ActorsSettings.tsx +1190 -0
- package/src/components/ui/accordion.tsx +52 -0
- package/src/components/ui/alert-dialog.tsx +104 -0
- package/src/components/ui/alert.tsx +43 -0
- package/src/components/ui/aspect-ratio.tsx +5 -0
- package/src/components/ui/avatar.tsx +38 -0
- package/src/components/ui/badge.tsx +29 -0
- package/src/components/ui/breadcrumb.tsx +90 -0
- package/src/components/ui/button.tsx +47 -0
- package/src/components/ui/calendar.tsx +54 -0
- package/src/components/ui/card.tsx +43 -0
- package/src/components/ui/carousel.tsx +224 -0
- package/src/components/ui/chart.tsx +303 -0
- package/src/components/ui/checkbox.tsx +26 -0
- package/src/components/ui/collapsible.tsx +9 -0
- package/src/components/ui/command.tsx +132 -0
- package/src/components/ui/context-menu.tsx +178 -0
- package/src/components/ui/date-picker.tsx +313 -0
- package/src/components/ui/dialog.tsx +95 -0
- package/src/components/ui/drawer.tsx +87 -0
- package/src/components/ui/dropdown-menu.tsx +179 -0
- package/src/components/ui/form.tsx +129 -0
- package/src/components/ui/hover-card.tsx +27 -0
- package/src/components/ui/input-otp.tsx +61 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +17 -0
- package/src/components/ui/menubar.tsx +207 -0
- package/src/components/ui/navigation-menu.tsx +120 -0
- package/src/components/ui/pagination.tsx +81 -0
- package/src/components/ui/popover.tsx +29 -0
- package/src/components/ui/progress.tsx +23 -0
- package/src/components/ui/radio-group.tsx +36 -0
- package/src/components/ui/resizable.tsx +37 -0
- package/src/components/ui/scroll-area.tsx +38 -0
- package/src/components/ui/select.tsx +143 -0
- package/src/components/ui/separator.tsx +20 -0
- package/src/components/ui/sheet.tsx +107 -0
- package/src/components/ui/sidebar.tsx +637 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/slider.tsx +23 -0
- package/src/components/ui/sonner.tsx +24 -0
- package/src/components/ui/switch.tsx +27 -0
- package/src/components/ui/table.tsx +72 -0
- package/src/components/ui/tabs.tsx +53 -0
- package/src/components/ui/textarea.tsx +21 -0
- package/src/components/ui/toast.tsx +111 -0
- package/src/components/ui/toaster.tsx +24 -0
- package/src/components/ui/toggle-group.tsx +49 -0
- package/src/components/ui/toggle.tsx +37 -0
- package/src/components/ui/tooltip.tsx +28 -0
- package/src/components/ui/use-toast.ts +1 -0
- package/src/components/ui/utils.ts +9 -0
- package/src/contexts/AgentSettingsContext.tsx +24 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/hooks/use-toast.ts +186 -0
- package/src/hooks/useKeyboardShortcuts.ts +95 -0
- package/src/hooks/useTheme.ts +24 -0
- package/src/index.css +245 -0
- package/src/lib/entityColors.ts +18 -0
- package/src/lib/stageConfig.ts +32 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +25 -0
- package/src/pages/Accounts.tsx +205 -0
- package/src/pages/Activities.tsx +251 -0
- package/src/pages/Agent.tsx +237 -0
- package/src/pages/AgentSettings.tsx +544 -0
- package/src/pages/Assignments.tsx +750 -0
- package/src/pages/Contacts.tsx +200 -0
- package/src/pages/Dashboard.tsx +143 -0
- package/src/pages/Inbox.tsx +615 -0
- package/src/pages/NotFound.tsx +24 -0
- package/src/pages/Opportunities.tsx +386 -0
- package/src/pages/SearchResults.tsx +49 -0
- package/src/pages/Settings.tsx +1884 -0
- package/src/pages/UseCases.tsx +396 -0
- package/src/pages/auth/Login.tsx +261 -0
- package/src/pages/hitl/HITL.tsx +101 -0
- package/src/store/appStore.ts +103 -0
- package/src/vite-env.d.ts +14 -0
- package/tailwind.config.js +121 -0
- package/tsconfig.json +24 -0
- package/vite.config.ts +27 -0
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
// Copyright 2026 CRMy Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { useState, useRef, useEffect } from 'react';
|
|
5
|
+
import { useAppStore } from '@/store/appStore';
|
|
6
|
+
import { useAgentSettings } from '@/contexts/AgentSettingsContext';
|
|
7
|
+
import { X, Send, Sparkles, Check, FileText, Pencil, ChevronLeft } from 'lucide-react';
|
|
8
|
+
import { useCreateContact, useCreateAccount, useCreateOpportunity, useCreateUseCase, useCreateActivity, useCreateAssignment, useAccounts, useContacts, useOpportunities, useUseCases, useActors } from '@/api/hooks';
|
|
9
|
+
import { toast } from '@/components/ui/use-toast';
|
|
10
|
+
import { DatePicker, DateTimePicker } from '@/components/ui/date-picker';
|
|
11
|
+
|
|
12
|
+
const typeLabels: Record<string, string> = {
|
|
13
|
+
contact: 'Contact',
|
|
14
|
+
opportunity: 'Opportunity',
|
|
15
|
+
'use-case': 'Use Case',
|
|
16
|
+
activity: 'Activity',
|
|
17
|
+
account: 'Account',
|
|
18
|
+
assignment: 'Assignment',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const typeGreetings: Record<string, string> = {
|
|
22
|
+
contact: "Hi! Tell me about the new contact — name, email, company, and any other details.",
|
|
23
|
+
opportunity: "Let's create a new opportunity! What's the name, amount, and who's the contact?",
|
|
24
|
+
'use-case': "Let's set up a new use case. What's the name and which client is it for?",
|
|
25
|
+
activity: "Log an activity — tell me the type (call, email, meeting, note, demo, proposal, etc.), what it's about, and any outcome or notes.",
|
|
26
|
+
account: "Let's add a new account. What's the company name and any other details?",
|
|
27
|
+
assignment: "Let's create a new assignment. What's the title, type (call, email, research, etc.), and who should it be assigned to?",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type Message = { role: 'user' | 'assistant'; content: string };
|
|
31
|
+
|
|
32
|
+
function parseFieldsFromText(text: string, type: string): Record<string, unknown> {
|
|
33
|
+
const lower = text.toLowerCase();
|
|
34
|
+
const fields: Record<string, unknown> = {};
|
|
35
|
+
|
|
36
|
+
// Extract name (first sentence or "name is X" pattern)
|
|
37
|
+
const nameMatch = text.match(/(?:name(?:\s+is)?|called|named)\s+([A-Z][^\.,\n]+)/i) || text.match(/^([A-Z][a-z]+ [A-Z][a-z]+)/);
|
|
38
|
+
if (nameMatch) fields.name = nameMatch[1].trim();
|
|
39
|
+
|
|
40
|
+
// Extract email
|
|
41
|
+
const emailMatch = text.match(/[\w.-]+@[\w.-]+\.\w+/);
|
|
42
|
+
if (emailMatch) fields.email = emailMatch[0];
|
|
43
|
+
|
|
44
|
+
// Extract phone
|
|
45
|
+
const phoneMatch = text.match(/\+?[\d\s\-().]{10,}/);
|
|
46
|
+
if (phoneMatch) fields.phone = phoneMatch[0].trim();
|
|
47
|
+
|
|
48
|
+
// Extract company
|
|
49
|
+
const companyMatch = text.match(/(?:at|from|company|works at|with)\s+([A-Z][^\.,\n]+)/i);
|
|
50
|
+
if (companyMatch) fields.company = companyMatch[1].trim();
|
|
51
|
+
|
|
52
|
+
if (type === 'opportunity') {
|
|
53
|
+
const amountMatch = text.match(/\$?([\d,]+(?:\.\d+)?)\s*[kKmM]?/);
|
|
54
|
+
if (amountMatch) {
|
|
55
|
+
let amount = parseFloat(amountMatch[1].replace(',', ''));
|
|
56
|
+
if (lower.includes('k') || lower.includes('thousand')) amount *= 1000;
|
|
57
|
+
if (lower.includes('m') || lower.includes('million')) amount *= 1000000;
|
|
58
|
+
fields.amount = Math.round(amount);
|
|
59
|
+
}
|
|
60
|
+
fields.stage = 'prospecting';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (type === 'activity') {
|
|
64
|
+
const types = ['call', 'email', 'meeting', 'note', 'task', 'demo', 'proposal', 'research', 'handoff', 'status_update'];
|
|
65
|
+
const foundType = types.find(t => lower.includes(t.replace('_', ' ')) || lower.includes(t));
|
|
66
|
+
fields.type = foundType ?? 'note';
|
|
67
|
+
fields.description = text;
|
|
68
|
+
// Extract outcome keywords
|
|
69
|
+
const outcomes = ['connected', 'voicemail', 'positive', 'negative', 'neutral', 'no show', 'no_show', 'follow up needed', 'follow_up_needed'];
|
|
70
|
+
const foundOutcome = outcomes.find(o => lower.includes(o));
|
|
71
|
+
if (foundOutcome) fields.outcome = foundOutcome.replace(/ /g, '_');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (type === 'use-case') {
|
|
75
|
+
fields.stage = 'discovery';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (type === 'assignment') {
|
|
79
|
+
// Extract assignment type
|
|
80
|
+
const assignmentTypes = ['call', 'draft', 'email', 'follow_up', 'follow up', 'research', 'review', 'send'];
|
|
81
|
+
const foundType = assignmentTypes.find(t => lower.includes(t));
|
|
82
|
+
if (foundType) fields.assignment_type = foundType.replace(' ', '_');
|
|
83
|
+
// Use first sentence as title if no explicit name
|
|
84
|
+
if (!fields.name) {
|
|
85
|
+
const firstSentence = text.split(/[.!?]/)[0].trim();
|
|
86
|
+
if (firstSentence) fields.title = firstSentence;
|
|
87
|
+
} else {
|
|
88
|
+
fields.title = fields.name;
|
|
89
|
+
delete fields.name;
|
|
90
|
+
}
|
|
91
|
+
// Extract priority
|
|
92
|
+
if (lower.includes('urgent')) fields.priority = 'urgent';
|
|
93
|
+
else if (lower.includes('high priority') || lower.includes('high-priority')) fields.priority = 'high';
|
|
94
|
+
else if (lower.includes('low priority') || lower.includes('low-priority')) fields.priority = 'low';
|
|
95
|
+
else fields.priority = 'normal';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return fields;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function ChatAddPanel({ type, onClose }: { type: string; onClose: () => void }) {
|
|
102
|
+
const [messages, setMessages] = useState<Message[]>([
|
|
103
|
+
{ role: 'assistant', content: typeGreetings[type] ?? typeGreetings.contact },
|
|
104
|
+
]);
|
|
105
|
+
const [input, setInput] = useState('');
|
|
106
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
107
|
+
const [extractedFields, setExtractedFields] = useState<Record<string, unknown> | null>(null);
|
|
108
|
+
const [confirmed, setConfirmed] = useState(false);
|
|
109
|
+
const [showForm, setShowForm] = useState(false);
|
|
110
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
111
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
112
|
+
|
|
113
|
+
const createContact = useCreateContact();
|
|
114
|
+
const createAccount = useCreateAccount();
|
|
115
|
+
const createOpportunity = useCreateOpportunity();
|
|
116
|
+
const createUseCase = useCreateUseCase();
|
|
117
|
+
const createActivity = useCreateActivity();
|
|
118
|
+
const createAssignment = useCreateAssignment();
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' });
|
|
122
|
+
}, [messages]);
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
inputRef.current?.focus();
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
const handleSend = () => {
|
|
129
|
+
if (!input.trim() || isSubmitting) return;
|
|
130
|
+
const userText = input.trim();
|
|
131
|
+
setInput('');
|
|
132
|
+
|
|
133
|
+
setMessages(prev => [...prev, { role: 'user', content: userText }]);
|
|
134
|
+
|
|
135
|
+
// Parse fields from message
|
|
136
|
+
const fields = parseFieldsFromText(userText, type);
|
|
137
|
+
setExtractedFields(prev => ({ ...(prev ?? {}), ...fields }));
|
|
138
|
+
|
|
139
|
+
// Generate assistant confirmation
|
|
140
|
+
const fieldsList = Object.entries({ ...(extractedFields ?? {}), ...fields })
|
|
141
|
+
.filter(([, v]) => v !== undefined && v !== '')
|
|
142
|
+
.map(([k, v]) => `• **${k}**: ${v}`)
|
|
143
|
+
.join('\n');
|
|
144
|
+
|
|
145
|
+
setMessages(prev => [
|
|
146
|
+
...prev,
|
|
147
|
+
{
|
|
148
|
+
role: 'assistant',
|
|
149
|
+
content: fieldsList
|
|
150
|
+
? `Got it! Here's what I'll create:\n\n${fieldsList}\n\nDoes this look right? Reply "yes" to confirm or add more details.`
|
|
151
|
+
: "Thanks! Can you share more details like name, email, or any other relevant information?",
|
|
152
|
+
},
|
|
153
|
+
]);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const handleConfirm = async () => {
|
|
157
|
+
if (!extractedFields || isSubmitting) return;
|
|
158
|
+
setIsSubmitting(true);
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
if (type === 'contact') await createContact.mutateAsync(extractedFields);
|
|
162
|
+
else if (type === 'account') await createAccount.mutateAsync(extractedFields);
|
|
163
|
+
else if (type === 'opportunity') await createOpportunity.mutateAsync(extractedFields);
|
|
164
|
+
else if (type === 'use-case') await createUseCase.mutateAsync(extractedFields);
|
|
165
|
+
else if (type === 'activity') await createActivity.mutateAsync(extractedFields);
|
|
166
|
+
else if (type === 'assignment') await createAssignment.mutateAsync(extractedFields);
|
|
167
|
+
|
|
168
|
+
setConfirmed(true);
|
|
169
|
+
toast({ title: `${typeLabels[type]} created!`, description: 'Successfully added to your CRM.' });
|
|
170
|
+
setTimeout(onClose, 1200);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
toast({ title: `Failed to create ${typeLabels[type]}`, description: err instanceof Error ? err.message : 'Please try again.', variant: 'destructive' });
|
|
173
|
+
} finally {
|
|
174
|
+
setIsSubmitting(false);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const lastMsg = messages[messages.length - 1];
|
|
179
|
+
const showConfirmButton =
|
|
180
|
+
lastMsg?.role === 'assistant' &&
|
|
181
|
+
lastMsg.content.includes("Does this look right") &&
|
|
182
|
+
extractedFields !== null &&
|
|
183
|
+
!confirmed;
|
|
184
|
+
|
|
185
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
186
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
if (input.toLowerCase().trim() === 'yes' || input.toLowerCase().trim() === 'confirm') {
|
|
189
|
+
handleConfirm();
|
|
190
|
+
} else {
|
|
191
|
+
handleSend();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<div className="flex flex-col h-full">
|
|
198
|
+
{/* Header */}
|
|
199
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
|
|
200
|
+
<div className="flex items-center gap-2">
|
|
201
|
+
<Sparkles className="w-4 h-4 text-accent" />
|
|
202
|
+
<span className="font-display font-bold text-foreground">New {typeLabels[type]}</span>
|
|
203
|
+
</div>
|
|
204
|
+
<div className="flex items-center gap-2">
|
|
205
|
+
<button
|
|
206
|
+
onClick={() => setShowForm(!showForm)}
|
|
207
|
+
className={`flex items-center gap-1.5 px-2.5 py-1 text-xs rounded-md border transition-colors ${
|
|
208
|
+
showForm
|
|
209
|
+
? 'border-border bg-muted text-foreground'
|
|
210
|
+
: 'border-border/60 text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
|
211
|
+
}`}
|
|
212
|
+
>
|
|
213
|
+
<FileText className="w-3 h-3" />
|
|
214
|
+
<span>Form</span>
|
|
215
|
+
</button>
|
|
216
|
+
<button onClick={onClose} className="p-1.5 rounded-lg hover:bg-muted transition-colors">
|
|
217
|
+
<X className="w-4 h-4 text-muted-foreground" />
|
|
218
|
+
</button>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
{showForm ? (
|
|
223
|
+
<ManualForm type={type} onClose={onClose} onBack={() => setShowForm(false)} backLabel="Back to AI chat" />
|
|
224
|
+
) : (
|
|
225
|
+
<>
|
|
226
|
+
{/* Messages */}
|
|
227
|
+
<div ref={scrollRef} className="flex-1 overflow-y-auto p-4 space-y-3">
|
|
228
|
+
{messages.map((msg, i) => (
|
|
229
|
+
<div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
|
230
|
+
<div
|
|
231
|
+
className={`max-w-[85%] rounded-2xl px-3.5 py-2.5 text-sm leading-relaxed whitespace-pre-wrap ${
|
|
232
|
+
msg.role === 'user'
|
|
233
|
+
? 'bg-primary text-primary-foreground'
|
|
234
|
+
: 'bg-muted text-foreground'
|
|
235
|
+
}`}
|
|
236
|
+
>
|
|
237
|
+
{msg.content}
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
))}
|
|
241
|
+
{confirmed && (
|
|
242
|
+
<div className="flex justify-center">
|
|
243
|
+
<div className="flex items-center gap-2 px-4 py-2 rounded-full bg-success/15 text-success text-sm font-semibold">
|
|
244
|
+
<Check className="w-4 h-4" /> Created successfully!
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{/* Confirm button */}
|
|
251
|
+
{showConfirmButton && (
|
|
252
|
+
<div className="px-4 pb-2">
|
|
253
|
+
<button
|
|
254
|
+
onClick={handleConfirm}
|
|
255
|
+
disabled={isSubmitting}
|
|
256
|
+
className="w-full py-2.5 rounded-xl bg-success text-success-foreground text-sm font-semibold hover:bg-success/90 transition-colors disabled:opacity-50"
|
|
257
|
+
>
|
|
258
|
+
{isSubmitting ? 'Creating...' : `Confirm & Create ${typeLabels[type]}`}
|
|
259
|
+
</button>
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{/* Input */}
|
|
264
|
+
<div className="flex items-end gap-2 p-3 border-t border-border">
|
|
265
|
+
<textarea
|
|
266
|
+
ref={inputRef}
|
|
267
|
+
value={input}
|
|
268
|
+
onChange={(e) => setInput(e.target.value)}
|
|
269
|
+
onKeyDown={handleKeyDown}
|
|
270
|
+
placeholder="Type details or 'yes' to confirm..."
|
|
271
|
+
rows={1}
|
|
272
|
+
className="flex-1 resize-none bg-muted rounded-xl px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground outline-none focus:ring-1 focus:ring-primary/30 min-h-[38px] max-h-28"
|
|
273
|
+
style={{ height: 'auto' }}
|
|
274
|
+
/>
|
|
275
|
+
<button
|
|
276
|
+
onClick={() => {
|
|
277
|
+
if (input.toLowerCase().trim() === 'yes' || input.toLowerCase().trim() === 'confirm') {
|
|
278
|
+
handleConfirm();
|
|
279
|
+
} else {
|
|
280
|
+
handleSend();
|
|
281
|
+
}
|
|
282
|
+
}}
|
|
283
|
+
disabled={!input.trim() || isSubmitting}
|
|
284
|
+
className="p-2 rounded-xl bg-primary text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-40"
|
|
285
|
+
>
|
|
286
|
+
<Send className="w-4 h-4" />
|
|
287
|
+
</button>
|
|
288
|
+
</div>
|
|
289
|
+
</>
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
type FieldConfig = {
|
|
296
|
+
key: string;
|
|
297
|
+
label: string;
|
|
298
|
+
placeholder?: string;
|
|
299
|
+
inputType?: 'text' | 'email' | 'tel' | 'number' | 'date' | 'url' | 'datetime-local';
|
|
300
|
+
fieldType?: 'textarea' | 'select' | 'account-select' | 'subject-type-select' | 'entity-select' | 'datalist';
|
|
301
|
+
options?: string[];
|
|
302
|
+
datalistId?: string;
|
|
303
|
+
suggestions?: string[];
|
|
304
|
+
required?: boolean;
|
|
305
|
+
dependsOn?: { key: string; values?: string[] };
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const FIELD_CONFIGS: Record<string, FieldConfig[]> = {
|
|
309
|
+
contact: [
|
|
310
|
+
{ key: 'first_name', label: 'First Name', placeholder: 'First name', required: true },
|
|
311
|
+
{ key: 'last_name', label: 'Last Name', placeholder: 'Last name' },
|
|
312
|
+
{ key: 'email', label: 'Email', placeholder: 'email@example.com', inputType: 'email' },
|
|
313
|
+
{ key: 'phone', label: 'Phone', placeholder: '(555) 123-4567', inputType: 'tel' },
|
|
314
|
+
{ key: 'company_name', label: 'Company', placeholder: 'Company name' },
|
|
315
|
+
],
|
|
316
|
+
opportunity: [
|
|
317
|
+
{ key: 'name', label: 'Opportunity Name', placeholder: 'e.g. Acme Enterprise', required: true },
|
|
318
|
+
{ key: 'amount', label: 'Amount ($)', placeholder: '850000', inputType: 'number' },
|
|
319
|
+
{ key: 'close_date', label: 'Close Date', inputType: 'date' },
|
|
320
|
+
{ key: 'description', label: 'Description', placeholder: 'Optional notes', fieldType: 'textarea' },
|
|
321
|
+
],
|
|
322
|
+
'use-case': [
|
|
323
|
+
{ key: 'name', label: 'Name', placeholder: 'e.g. Corporate Relocation', required: true },
|
|
324
|
+
{ key: 'account_id', label: 'Account', fieldType: 'account-select', required: true },
|
|
325
|
+
{ key: 'stage', label: 'Stage', fieldType: 'select', options: ['discovery', 'poc', 'production', 'scaling', 'sunset'] },
|
|
326
|
+
{ key: 'attributed_arr', label: 'Attributed ARR ($)', placeholder: '120000', inputType: 'number' },
|
|
327
|
+
{ key: 'target_prod_date', label: 'Target Prod Date', inputType: 'date' },
|
|
328
|
+
{ key: 'description', label: 'Description', placeholder: 'Any additional details', fieldType: 'textarea' },
|
|
329
|
+
],
|
|
330
|
+
activity: [
|
|
331
|
+
{ key: 'type', label: 'Type', fieldType: 'select', options: ['call', 'email', 'meeting', 'note', 'task', 'demo', 'proposal', 'research', 'handoff'], required: true },
|
|
332
|
+
{ key: 'subject', label: 'Subject', placeholder: 'What was this activity about?', required: true },
|
|
333
|
+
{ key: 'subject_type', label: 'Linked To', fieldType: 'subject-type-select', placeholder: 'Link to a CRM record (optional)' },
|
|
334
|
+
{ key: 'subject_id', label: 'Record', fieldType: 'entity-select', dependsOn: { key: 'subject_type' } },
|
|
335
|
+
{ key: 'occurred_at', label: 'When', inputType: 'datetime-local', placeholder: 'When did this happen?' },
|
|
336
|
+
{ key: 'outcome', label: 'Outcome', fieldType: 'datalist', datalistId: 'outcome-suggestions', suggestions: ['connected', 'voicemail', 'positive', 'negative', 'neutral', 'no_show', 'follow_up_needed'], placeholder: 'e.g. connected, positive, voicemail' },
|
|
337
|
+
{ key: 'body', label: 'Notes', placeholder: 'Additional details...', fieldType: 'textarea' },
|
|
338
|
+
],
|
|
339
|
+
account: [
|
|
340
|
+
{ key: 'name', label: 'Company Name', placeholder: 'e.g. Acme Corp', required: true },
|
|
341
|
+
{ key: 'industry', label: 'Industry', placeholder: 'e.g. Real Estate, Technology' },
|
|
342
|
+
{ key: 'website', label: 'Website', placeholder: 'https://acme.com', inputType: 'url' },
|
|
343
|
+
{ key: 'domain', label: 'Domain', placeholder: 'acme.com' },
|
|
344
|
+
],
|
|
345
|
+
assignment: [
|
|
346
|
+
{ key: 'title', label: 'Title', placeholder: 'e.g. Follow up with Acme about contract', required: true },
|
|
347
|
+
{ key: 'assignment_type', label: 'Type', fieldType: 'select', options: ['call', 'draft', 'email', 'follow_up', 'research', 'review', 'send'], required: true },
|
|
348
|
+
{ key: 'assigned_to', label: 'Assign To', fieldType: 'actor-select', required: true },
|
|
349
|
+
{ key: 'subject_type', label: 'Linked To', fieldType: 'subject-type-select' },
|
|
350
|
+
{ key: 'subject_id', label: 'Record', fieldType: 'entity-select', dependsOn: { key: 'subject_type' } },
|
|
351
|
+
{ key: 'priority', label: 'Priority', fieldType: 'select', options: ['low', 'normal', 'high', 'urgent'] },
|
|
352
|
+
{ key: 'due_at', label: 'Due Date', inputType: 'date' },
|
|
353
|
+
{ key: 'context', label: 'Context', placeholder: 'Brief context for the assignee', fieldType: 'textarea' },
|
|
354
|
+
{ key: 'description', label: 'Description', placeholder: 'Additional details', fieldType: 'textarea' },
|
|
355
|
+
],
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const SUBJECT_TYPE_OPTIONS = [
|
|
359
|
+
{ value: '', label: 'None (no link)' },
|
|
360
|
+
{ value: 'contact', label: 'Contact' },
|
|
361
|
+
{ value: 'account', label: 'Account' },
|
|
362
|
+
{ value: 'opportunity', label: 'Opportunity' },
|
|
363
|
+
{ value: 'use_case', label: 'Use Case' },
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
function EntitySelect({ subjectType, value, onChange }: { subjectType: string; value: string; onChange: (v: string) => void }) {
|
|
367
|
+
const { data: contactsData } = useContacts(subjectType === 'contact' ? { limit: 100 } : undefined);
|
|
368
|
+
const { data: accountsData } = useAccounts(subjectType === 'account' ? { limit: 100 } : undefined);
|
|
369
|
+
const { data: oppsData } = useOpportunities(subjectType === 'opportunity' ? { limit: 100 } : undefined);
|
|
370
|
+
const { data: ucsData } = useUseCases(subjectType === 'use_case' ? { limit: 100 } : undefined);
|
|
371
|
+
|
|
372
|
+
const inputClass = 'w-full h-10 px-3 rounded-md border border-border bg-background text-sm text-foreground placeholder:text-muted-foreground outline-none focus:ring-1 focus:ring-ring';
|
|
373
|
+
|
|
374
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
375
|
+
let entities: Array<{ id: string; label: string }> = [];
|
|
376
|
+
if (subjectType === 'contact') {
|
|
377
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
378
|
+
entities = ((contactsData?.data ?? []) as any[]).map(c => ({ id: c.id, label: `${c.first_name ?? ''} ${c.last_name ?? ''}`.trim() || c.email || c.id }));
|
|
379
|
+
} else if (subjectType === 'account') {
|
|
380
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
381
|
+
entities = ((accountsData?.data ?? []) as any[]).map(a => ({ id: a.id, label: a.name ?? a.id }));
|
|
382
|
+
} else if (subjectType === 'opportunity') {
|
|
383
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
384
|
+
entities = ((oppsData?.data ?? []) as any[]).map(o => ({ id: o.id, label: o.name ?? o.id }));
|
|
385
|
+
} else if (subjectType === 'use_case') {
|
|
386
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
387
|
+
entities = ((ucsData?.data ?? []) as any[]).map(u => ({ id: u.id, label: u.name ?? u.id }));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (!subjectType) return null;
|
|
391
|
+
|
|
392
|
+
return (
|
|
393
|
+
<select value={value} onChange={e => onChange(e.target.value)} className={`${inputClass} pr-3`}>
|
|
394
|
+
<option value="">Select {subjectType.replace('_', ' ')}…</option>
|
|
395
|
+
{entities.map(e => (
|
|
396
|
+
<option key={e.id} value={e.id}>{e.label}</option>
|
|
397
|
+
))}
|
|
398
|
+
</select>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function ActorSelect({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
|
403
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
404
|
+
const { data: actorsData } = useActors({ limit: 100, is_active: true }) as any;
|
|
405
|
+
const actors: Array<{ id: string; display_name: string; actor_type: string }> = actorsData?.data ?? actorsData?.actors ?? [];
|
|
406
|
+
const inputClass = 'w-full h-10 px-3 rounded-md border border-border bg-background text-sm text-foreground placeholder:text-muted-foreground outline-none focus:ring-1 focus:ring-ring';
|
|
407
|
+
return (
|
|
408
|
+
<select value={value} onChange={e => onChange(e.target.value)} className={`${inputClass} pr-3`}>
|
|
409
|
+
<option value="">Select actor…</option>
|
|
410
|
+
{actors.map(a => (
|
|
411
|
+
<option key={a.id} value={a.id}>
|
|
412
|
+
{a.display_name} ({a.actor_type})
|
|
413
|
+
</option>
|
|
414
|
+
))}
|
|
415
|
+
</select>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function ManualForm({ type, onClose, onBack, backLabel }: { type: string; onClose: () => void; onBack: () => void; backLabel?: string }) {
|
|
420
|
+
const [fields, setFields] = useState<Record<string, string>>({});
|
|
421
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
422
|
+
const { data: accountsData } = useAccounts({ limit: 200 });
|
|
423
|
+
const accounts = (accountsData?.data ?? []) as Array<{ id: string; name: string }>;
|
|
424
|
+
|
|
425
|
+
const createContact = useCreateContact();
|
|
426
|
+
const createAccount = useCreateAccount();
|
|
427
|
+
const createOpportunity = useCreateOpportunity();
|
|
428
|
+
const createUseCase = useCreateUseCase();
|
|
429
|
+
const createActivity = useCreateActivity();
|
|
430
|
+
const createAssignment = useCreateAssignment();
|
|
431
|
+
|
|
432
|
+
const config = FIELD_CONFIGS[type] ?? FIELD_CONFIGS.contact;
|
|
433
|
+
|
|
434
|
+
const isValid = () => {
|
|
435
|
+
if (type === 'contact') return !!fields.first_name?.trim();
|
|
436
|
+
if (type === 'activity') return !!fields.type && !!fields.subject?.trim();
|
|
437
|
+
if (type === 'use-case') return !!fields.name?.trim() && !!fields.account_id;
|
|
438
|
+
if (type === 'assignment') return !!fields.title?.trim() && !!fields.assignment_type && !!fields.assigned_to;
|
|
439
|
+
return !!fields.name?.trim();
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const set = (key: string, val: string) => setFields(prev => ({ ...prev, [key]: val }));
|
|
443
|
+
|
|
444
|
+
const handleSubmit = async () => {
|
|
445
|
+
if (!isValid() || isSubmitting) return;
|
|
446
|
+
setIsSubmitting(true);
|
|
447
|
+
try {
|
|
448
|
+
const payload: Record<string, unknown> = { ...fields };
|
|
449
|
+
|
|
450
|
+
if (type === 'contact') {
|
|
451
|
+
delete payload.name; // server uses first_name/last_name
|
|
452
|
+
}
|
|
453
|
+
if (type === 'opportunity') {
|
|
454
|
+
if (fields.amount) payload.amount = parseFloat(fields.amount) || 0;
|
|
455
|
+
payload.stage = 'prospecting';
|
|
456
|
+
}
|
|
457
|
+
if (type === 'use-case') {
|
|
458
|
+
if (!payload.stage) payload.stage = 'discovery';
|
|
459
|
+
if (fields.attributed_arr) payload.attributed_arr = parseFloat(fields.attributed_arr) || 0;
|
|
460
|
+
}
|
|
461
|
+
if (type === 'account' && fields.website) {
|
|
462
|
+
payload.website = fields.website.startsWith('http') ? fields.website : `https://${fields.website}`;
|
|
463
|
+
}
|
|
464
|
+
if (type === 'activity') {
|
|
465
|
+
// Convert occurred_at from datetime-local to ISO string
|
|
466
|
+
if (fields.occurred_at) {
|
|
467
|
+
payload.occurred_at = new Date(fields.occurred_at).toISOString();
|
|
468
|
+
}
|
|
469
|
+
// Clean up empty optional fields
|
|
470
|
+
if (!fields.subject_type) { delete payload.subject_type; delete payload.subject_id; }
|
|
471
|
+
if (!fields.subject_id) delete payload.subject_id;
|
|
472
|
+
if (!fields.outcome) delete payload.outcome;
|
|
473
|
+
if (!fields.occurred_at) delete payload.occurred_at;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (type === 'assignment') {
|
|
477
|
+
// Convert date string to ISO datetime
|
|
478
|
+
if (fields.due_at) payload.due_at = new Date(fields.due_at + 'T00:00:00').toISOString();
|
|
479
|
+
// Strip empty optional fields
|
|
480
|
+
if (!fields.subject_type) { delete payload.subject_type; delete payload.subject_id; }
|
|
481
|
+
if (!fields.subject_id) delete payload.subject_id;
|
|
482
|
+
if (!fields.context) delete payload.context;
|
|
483
|
+
if (!fields.description) delete payload.description;
|
|
484
|
+
if (!fields.due_at) delete payload.due_at;
|
|
485
|
+
if (!fields.priority) payload.priority = 'normal';
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (type === 'contact') await createContact.mutateAsync(payload);
|
|
489
|
+
else if (type === 'account') await createAccount.mutateAsync(payload);
|
|
490
|
+
else if (type === 'opportunity') await createOpportunity.mutateAsync(payload);
|
|
491
|
+
else if (type === 'use-case') await createUseCase.mutateAsync(payload);
|
|
492
|
+
else if (type === 'activity') await createActivity.mutateAsync(payload);
|
|
493
|
+
else if (type === 'assignment') await createAssignment.mutateAsync(payload);
|
|
494
|
+
|
|
495
|
+
const label = fields.first_name ?? fields.title ?? fields.name ?? fields.subject ?? typeLabels[type];
|
|
496
|
+
toast({ title: `${typeLabels[type]} created`, description: `${label} has been added.` });
|
|
497
|
+
onClose();
|
|
498
|
+
} catch (err) {
|
|
499
|
+
toast({ title: `Failed to create ${typeLabels[type]}`, description: err instanceof Error ? err.message : 'Please try again.', variant: 'destructive' });
|
|
500
|
+
} finally {
|
|
501
|
+
setIsSubmitting(false);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const inputClass = 'w-full h-10 px-3 rounded-md border border-border bg-background text-sm text-foreground placeholder:text-muted-foreground outline-none focus:ring-1 focus:ring-ring';
|
|
506
|
+
|
|
507
|
+
// Check if a field should be visible based on its dependsOn condition
|
|
508
|
+
const isFieldVisible = (f: FieldConfig) => {
|
|
509
|
+
if (!f.dependsOn) return true;
|
|
510
|
+
const depVal = fields[f.dependsOn.key];
|
|
511
|
+
if (!depVal) return false;
|
|
512
|
+
if (f.dependsOn.values) return f.dependsOn.values.includes(depVal);
|
|
513
|
+
return true;
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
return (
|
|
517
|
+
<div className="flex-1 overflow-y-auto p-4">
|
|
518
|
+
<button onClick={onBack} className="flex items-center gap-1.5 text-xs text-accent hover:underline mb-5">
|
|
519
|
+
{backLabel ? <><Sparkles className="w-3 h-3" /> {backLabel}</> : <><ChevronLeft className="w-3.5 h-3.5" /> Back</>}
|
|
520
|
+
</button>
|
|
521
|
+
<div className="space-y-4">
|
|
522
|
+
{config.filter(isFieldVisible).map(f => (
|
|
523
|
+
<div key={f.key} className="space-y-1.5">
|
|
524
|
+
<label className="text-xs font-mono text-muted-foreground uppercase tracking-wider">
|
|
525
|
+
{f.label}{f.required && <span className="text-destructive ml-0.5">*</span>}
|
|
526
|
+
</label>
|
|
527
|
+
|
|
528
|
+
{f.fieldType === 'select' ? (
|
|
529
|
+
<select
|
|
530
|
+
value={fields[f.key] || ''}
|
|
531
|
+
onChange={(e) => set(f.key, e.target.value)}
|
|
532
|
+
className={`${inputClass} pr-3`}
|
|
533
|
+
>
|
|
534
|
+
<option value="">Select {f.label.toLowerCase()}…</option>
|
|
535
|
+
{f.options?.map(o => (
|
|
536
|
+
<option key={o} value={o}>{o.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}</option>
|
|
537
|
+
))}
|
|
538
|
+
</select>
|
|
539
|
+
) : f.fieldType === 'account-select' ? (
|
|
540
|
+
<select
|
|
541
|
+
value={fields[f.key] || ''}
|
|
542
|
+
onChange={(e) => set(f.key, e.target.value)}
|
|
543
|
+
className={`${inputClass} pr-3`}
|
|
544
|
+
>
|
|
545
|
+
<option value="">Select account…</option>
|
|
546
|
+
{accounts.map(a => (
|
|
547
|
+
<option key={a.id} value={a.id}>{a.name}</option>
|
|
548
|
+
))}
|
|
549
|
+
</select>
|
|
550
|
+
) : f.fieldType === 'subject-type-select' ? (
|
|
551
|
+
<select
|
|
552
|
+
value={fields[f.key] || ''}
|
|
553
|
+
onChange={(e) => { set(f.key, e.target.value); set('subject_id', ''); }}
|
|
554
|
+
className={`${inputClass} pr-3`}
|
|
555
|
+
>
|
|
556
|
+
{SUBJECT_TYPE_OPTIONS.map(o => (
|
|
557
|
+
<option key={o.value} value={o.value}>{o.label}</option>
|
|
558
|
+
))}
|
|
559
|
+
</select>
|
|
560
|
+
) : f.fieldType === 'entity-select' ? (
|
|
561
|
+
<EntitySelect
|
|
562
|
+
subjectType={fields.subject_type || ''}
|
|
563
|
+
value={fields[f.key] || ''}
|
|
564
|
+
onChange={(v) => set(f.key, v)}
|
|
565
|
+
/>
|
|
566
|
+
) : f.fieldType === 'actor-select' ? (
|
|
567
|
+
<ActorSelect
|
|
568
|
+
value={fields[f.key] || ''}
|
|
569
|
+
onChange={(v) => set(f.key, v)}
|
|
570
|
+
/>
|
|
571
|
+
) : f.fieldType === 'datalist' ? (
|
|
572
|
+
<>
|
|
573
|
+
<input
|
|
574
|
+
type="text"
|
|
575
|
+
value={fields[f.key] || ''}
|
|
576
|
+
onChange={(e) => set(f.key, e.target.value)}
|
|
577
|
+
placeholder={f.placeholder}
|
|
578
|
+
list={f.datalistId}
|
|
579
|
+
className={`${inputClass} pr-3`}
|
|
580
|
+
/>
|
|
581
|
+
{f.datalistId && f.suggestions && (
|
|
582
|
+
<datalist id={f.datalistId}>
|
|
583
|
+
{f.suggestions.map(s => (
|
|
584
|
+
<option key={s} value={s} />
|
|
585
|
+
))}
|
|
586
|
+
</datalist>
|
|
587
|
+
)}
|
|
588
|
+
</>
|
|
589
|
+
) : f.fieldType === 'textarea' ? (
|
|
590
|
+
<textarea
|
|
591
|
+
value={fields[f.key] || ''}
|
|
592
|
+
onChange={(e) => set(f.key, e.target.value)}
|
|
593
|
+
placeholder={f.placeholder}
|
|
594
|
+
rows={3}
|
|
595
|
+
className="w-full px-3 py-2 rounded-md border border-border bg-background text-sm text-foreground placeholder:text-muted-foreground outline-none focus:ring-1 focus:ring-ring resize-none"
|
|
596
|
+
/>
|
|
597
|
+
) : f.inputType === 'date' ? (
|
|
598
|
+
<DatePicker
|
|
599
|
+
value={fields[f.key] || ''}
|
|
600
|
+
onChange={(v) => set(f.key, v)}
|
|
601
|
+
required={f.required}
|
|
602
|
+
/>
|
|
603
|
+
) : f.inputType === 'datetime-local' ? (
|
|
604
|
+
<DateTimePicker
|
|
605
|
+
value={fields[f.key] || ''}
|
|
606
|
+
onChange={(v) => set(f.key, v)}
|
|
607
|
+
required={f.required}
|
|
608
|
+
/>
|
|
609
|
+
) : (
|
|
610
|
+
<div className="relative">
|
|
611
|
+
<input
|
|
612
|
+
type={f.inputType ?? 'text'}
|
|
613
|
+
value={fields[f.key] || ''}
|
|
614
|
+
onChange={(e) => set(f.key, e.target.value)}
|
|
615
|
+
placeholder={f.placeholder}
|
|
616
|
+
className={`${inputClass} pr-8`}
|
|
617
|
+
/>
|
|
618
|
+
<Pencil className="absolute right-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground/40 pointer-events-none" />
|
|
619
|
+
</div>
|
|
620
|
+
)}
|
|
621
|
+
</div>
|
|
622
|
+
))}
|
|
623
|
+
<button
|
|
624
|
+
onClick={handleSubmit}
|
|
625
|
+
disabled={!isValid() || isSubmitting}
|
|
626
|
+
className="w-full h-10 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 disabled:opacity-40 transition-colors mt-2"
|
|
627
|
+
>
|
|
628
|
+
{isSubmitting ? 'Creating...' : `Create ${typeLabels[type]}`}
|
|
629
|
+
</button>
|
|
630
|
+
</div>
|
|
631
|
+
</div>
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
export function QuickAddDrawer() {
|
|
636
|
+
const { quickAddType, closeQuickAdd } = useAppStore();
|
|
637
|
+
const { enabled: agentEnabled } = useAgentSettings();
|
|
638
|
+
|
|
639
|
+
if (!quickAddType) return null;
|
|
640
|
+
|
|
641
|
+
return (
|
|
642
|
+
<>
|
|
643
|
+
<div className="fixed inset-0 bg-background/60 backdrop-blur-sm z-[80]" onClick={closeQuickAdd} />
|
|
644
|
+
<div className="fixed right-0 top-0 h-full w-full max-w-md bg-card border-l border-border z-[90] shadow-2xl flex flex-col animate-slide-in-right">
|
|
645
|
+
{agentEnabled
|
|
646
|
+
? <ChatAddPanel type={quickAddType} onClose={closeQuickAdd} />
|
|
647
|
+
: <ManualForm type={quickAddType} onClose={closeQuickAdd} onBack={closeQuickAdd} />
|
|
648
|
+
}
|
|
649
|
+
</div>
|
|
650
|
+
</>
|
|
651
|
+
);
|
|
652
|
+
}
|