@crmy/web 0.5.5 → 0.5.9

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 (121) hide show
  1. package/dist/assets/index-CskfWp8E.js +560 -0
  2. package/dist/assets/index-D763l57m.css +1 -0
  3. package/{index.html → dist/index.html} +2 -1
  4. package/package.json +4 -1
  5. package/postcss.config.js +0 -6
  6. package/src/App.tsx +0 -158
  7. package/src/api/client.ts +0 -82
  8. package/src/api/hooks.ts +0 -689
  9. package/src/components/CustomFields.tsx +0 -240
  10. package/src/components/NavLink.tsx +0 -28
  11. package/src/components/crm/AIFab.tsx +0 -37
  12. package/src/components/crm/AccountDrawer.tsx +0 -372
  13. package/src/components/crm/ActivityTimeline.tsx +0 -115
  14. package/src/components/crm/AssignmentDrawer.tsx +0 -396
  15. package/src/components/crm/BriefingPanel.tsx +0 -217
  16. package/src/components/crm/CommandPalette.tsx +0 -254
  17. package/src/components/crm/ContactAvatar.tsx +0 -49
  18. package/src/components/crm/ContactDrawer.tsx +0 -438
  19. package/src/components/crm/ContextPanel.tsx +0 -200
  20. package/src/components/crm/CrmWidgets.tsx +0 -417
  21. package/src/components/crm/DrawerShell.tsx +0 -77
  22. package/src/components/crm/ListToolbar.tsx +0 -252
  23. package/src/components/crm/OpportunityDrawer.tsx +0 -372
  24. package/src/components/crm/PaginationBar.tsx +0 -111
  25. package/src/components/crm/QuickAddDrawer.tsx +0 -652
  26. package/src/components/crm/ShortcutsOverlay.tsx +0 -65
  27. package/src/components/crm/UseCaseDrawer.tsx +0 -454
  28. package/src/components/layout/MobileNav.tsx +0 -49
  29. package/src/components/layout/Sidebar.tsx +0 -157
  30. package/src/components/layout/TopBar.tsx +0 -54
  31. package/src/components/settings/ActorsSettings.tsx +0 -1190
  32. package/src/components/ui/accordion.tsx +0 -52
  33. package/src/components/ui/alert-dialog.tsx +0 -104
  34. package/src/components/ui/alert.tsx +0 -43
  35. package/src/components/ui/aspect-ratio.tsx +0 -5
  36. package/src/components/ui/avatar.tsx +0 -38
  37. package/src/components/ui/badge.tsx +0 -29
  38. package/src/components/ui/breadcrumb.tsx +0 -90
  39. package/src/components/ui/button.tsx +0 -47
  40. package/src/components/ui/calendar.tsx +0 -54
  41. package/src/components/ui/card.tsx +0 -43
  42. package/src/components/ui/carousel.tsx +0 -224
  43. package/src/components/ui/chart.tsx +0 -303
  44. package/src/components/ui/checkbox.tsx +0 -26
  45. package/src/components/ui/collapsible.tsx +0 -9
  46. package/src/components/ui/command.tsx +0 -132
  47. package/src/components/ui/context-menu.tsx +0 -178
  48. package/src/components/ui/date-picker.tsx +0 -313
  49. package/src/components/ui/dialog.tsx +0 -95
  50. package/src/components/ui/drawer.tsx +0 -87
  51. package/src/components/ui/dropdown-menu.tsx +0 -179
  52. package/src/components/ui/form.tsx +0 -129
  53. package/src/components/ui/hover-card.tsx +0 -27
  54. package/src/components/ui/input-otp.tsx +0 -61
  55. package/src/components/ui/input.tsx +0 -22
  56. package/src/components/ui/label.tsx +0 -17
  57. package/src/components/ui/menubar.tsx +0 -207
  58. package/src/components/ui/navigation-menu.tsx +0 -120
  59. package/src/components/ui/pagination.tsx +0 -81
  60. package/src/components/ui/popover.tsx +0 -29
  61. package/src/components/ui/progress.tsx +0 -23
  62. package/src/components/ui/radio-group.tsx +0 -36
  63. package/src/components/ui/resizable.tsx +0 -37
  64. package/src/components/ui/scroll-area.tsx +0 -38
  65. package/src/components/ui/select.tsx +0 -143
  66. package/src/components/ui/separator.tsx +0 -20
  67. package/src/components/ui/sheet.tsx +0 -107
  68. package/src/components/ui/sidebar.tsx +0 -637
  69. package/src/components/ui/skeleton.tsx +0 -7
  70. package/src/components/ui/slider.tsx +0 -23
  71. package/src/components/ui/sonner.tsx +0 -24
  72. package/src/components/ui/switch.tsx +0 -27
  73. package/src/components/ui/table.tsx +0 -72
  74. package/src/components/ui/tabs.tsx +0 -53
  75. package/src/components/ui/textarea.tsx +0 -21
  76. package/src/components/ui/toast.tsx +0 -111
  77. package/src/components/ui/toaster.tsx +0 -24
  78. package/src/components/ui/toggle-group.tsx +0 -49
  79. package/src/components/ui/toggle.tsx +0 -37
  80. package/src/components/ui/tooltip.tsx +0 -28
  81. package/src/components/ui/use-toast.ts +0 -1
  82. package/src/components/ui/utils.ts +0 -9
  83. package/src/contexts/AgentSettingsContext.tsx +0 -24
  84. package/src/hooks/use-mobile.tsx +0 -19
  85. package/src/hooks/use-toast.ts +0 -186
  86. package/src/hooks/useKeyboardShortcuts.ts +0 -95
  87. package/src/hooks/useTheme.ts +0 -24
  88. package/src/index.css +0 -245
  89. package/src/lib/entityColors.ts +0 -18
  90. package/src/lib/stageConfig.ts +0 -32
  91. package/src/lib/utils.ts +0 -6
  92. package/src/main.tsx +0 -25
  93. package/src/pages/Accounts.tsx +0 -205
  94. package/src/pages/Activities.tsx +0 -251
  95. package/src/pages/Agent.tsx +0 -237
  96. package/src/pages/AgentSettings.tsx +0 -544
  97. package/src/pages/Assignments.tsx +0 -750
  98. package/src/pages/Contacts.tsx +0 -200
  99. package/src/pages/Dashboard.tsx +0 -143
  100. package/src/pages/Inbox.tsx +0 -615
  101. package/src/pages/NotFound.tsx +0 -24
  102. package/src/pages/Opportunities.tsx +0 -386
  103. package/src/pages/SearchResults.tsx +0 -49
  104. package/src/pages/Settings.tsx +0 -1884
  105. package/src/pages/UseCases.tsx +0 -396
  106. package/src/pages/auth/Login.tsx +0 -261
  107. package/src/pages/hitl/HITL.tsx +0 -101
  108. package/src/store/appStore.ts +0 -103
  109. package/src/vite-env.d.ts +0 -14
  110. package/tailwind.config.js +0 -121
  111. package/tsconfig.json +0 -24
  112. package/vite.config.ts +0 -27
  113. /package/{public → dist}/android-chrome-192x192.png +0 -0
  114. /package/{public → dist}/android-chrome-512x512.png +0 -0
  115. /package/{public → dist}/apple-touch-icon.png +0 -0
  116. /package/{src/assets/crmy-logo.png → dist/assets/crmy-logo-DWN0xBPW.png} +0 -0
  117. /package/{public → dist}/favicon-16x16.png +0 -0
  118. /package/{public → dist}/favicon-32x32.png +0 -0
  119. /package/{public → dist}/favicon.ico +0 -0
  120. /package/{public → dist}/favicon.svg +0 -0
  121. /package/{public → dist}/site.webmanifest +0 -0
@@ -1,438 +0,0 @@
1
- // Copyright 2026 CRMy Contributors
2
- // SPDX-License-Identifier: Apache-2.0
3
-
4
- import { useState, useRef } from 'react';
5
- import { useContact, useActivities, useUpdateContact, useDeleteContact, useUsers, useCustomFields, useNotes, useCreateNote } from '@/api/hooks';
6
- import { ContactAvatar } from './ContactAvatar';
7
- import { useNavigate } from 'react-router-dom';
8
- import { useAppStore } from '@/store/appStore';
9
- import { useAgentSettings } from '@/contexts/AgentSettingsContext';
10
- import { StageBadge, LeadScoreBadge, CustomFieldsSection } from './CrmWidgets';
11
- import { ActivityTimeline } from './ActivityTimeline';
12
- import { Phone, Mail, StickyNote, Sparkles, Pencil, ChevronLeft, Send, Pin, Trash2, FileText } from 'lucide-react';
13
- import { ContextPanel } from './ContextPanel';
14
- import { BriefingPanel } from './BriefingPanel';
15
- import { toast } from '@/components/ui/use-toast';
16
- import { DatePicker } from '@/components/ui/date-picker';
17
-
18
- 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';
19
- const labelClass = 'text-xs font-mono text-muted-foreground uppercase tracking-wider';
20
-
21
- const LIFECYCLE_STAGES = ['lead', 'qualified', 'opportunity', 'customer', 'churned'];
22
-
23
- function ContactEditForm({
24
- contact,
25
- onSave,
26
- onCancel,
27
- onDelete,
28
- isSaving,
29
- }: {
30
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
- contact: any;
32
- onSave: (data: Record<string, unknown>) => void;
33
- onCancel: () => void;
34
- onDelete: () => void;
35
- isSaving: boolean;
36
- }) {
37
- const [confirmDelete, setConfirmDelete] = useState(false);
38
- const [fields, setFields] = useState<Record<string, string>>({
39
- first_name: contact.first_name ?? '',
40
- last_name: contact.last_name ?? '',
41
- email: contact.email ?? '',
42
- phone: contact.phone ?? '',
43
- company_name: contact.company_name ?? '',
44
- title: contact.title ?? '',
45
- lifecycle_stage: contact.lifecycle_stage ?? 'lead',
46
- source: contact.source ?? '',
47
- owner_id: contact.owner_id ?? '',
48
- });
49
-
50
- const [customFieldValues, setCustomFieldValues] = useState<Record<string, string>>(() => {
51
- const init: Record<string, string> = {};
52
- if (contact.custom_fields) {
53
- for (const [k, v] of Object.entries(contact.custom_fields as Record<string, unknown>)) {
54
- init[k] = String(v ?? '');
55
- }
56
- }
57
- return init;
58
- });
59
-
60
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
- const { data: usersData } = useUsers() as any;
62
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
- const users: any[] = usersData?.data ?? [];
64
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
- const { data: customFieldDefs } = useCustomFields('contact') as any;
66
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
- const fieldDefs: any[] = customFieldDefs?.fields ?? [];
68
-
69
- const set = (key: string, val: string) => setFields(prev => ({ ...prev, [key]: val }));
70
- const setCF = (key: string, val: string) => setCustomFieldValues(prev => ({ ...prev, [key]: val }));
71
-
72
- const handleSave = () => {
73
- const payload: Record<string, unknown> = {};
74
- for (const [k, v] of Object.entries(fields)) {
75
- if (v !== '') payload[k] = v;
76
- }
77
- const cfPayload: Record<string, unknown> = {};
78
- for (const def of fieldDefs) {
79
- const val = customFieldValues[def.field_key] ?? '';
80
- if (val === '') continue;
81
- if (def.field_type === 'number') cfPayload[def.field_key] = Number(val);
82
- else if (def.field_type === 'boolean') cfPayload[def.field_key] = val === 'true';
83
- else cfPayload[def.field_key] = val;
84
- }
85
- if (Object.keys(cfPayload).length > 0) payload.custom_fields = cfPayload;
86
- onSave(payload);
87
- };
88
-
89
- return (
90
- <div className="flex flex-col h-full">
91
- <div className="flex items-center gap-2 px-5 py-3 border-b border-border">
92
- <button onClick={onCancel} className="flex items-center gap-1 text-xs text-accent hover:underline">
93
- <ChevronLeft className="w-3.5 h-3.5" /> Back
94
- </button>
95
- <span className="text-xs text-muted-foreground ml-auto">Editing contact</span>
96
- </div>
97
- <div className="flex-1 overflow-y-auto p-5 space-y-4">
98
- <div className="grid grid-cols-2 gap-4">
99
- <div className="space-y-1.5">
100
- <label className={labelClass}>First Name<span className="text-destructive ml-0.5">*</span></label>
101
- <input type="text" value={fields.first_name} onChange={e => set('first_name', e.target.value)} placeholder="First name" className={inputClass} />
102
- </div>
103
- <div className="space-y-1.5">
104
- <label className={labelClass}>Last Name</label>
105
- <input type="text" value={fields.last_name} onChange={e => set('last_name', e.target.value)} placeholder="Last name" className={inputClass} />
106
- </div>
107
- </div>
108
- {[
109
- { key: 'email', label: 'Email', type: 'email', placeholder: 'email@example.com' },
110
- { key: 'phone', label: 'Phone', type: 'tel', placeholder: '(555) 123-4567' },
111
- { key: 'company_name', label: 'Company', type: 'text', placeholder: 'Company name' },
112
- { key: 'title', label: 'Title', type: 'text', placeholder: 'Job title' },
113
- { key: 'source', label: 'Source', type: 'text', placeholder: 'e.g. inbound, referral' },
114
- ].map(f => (
115
- <div key={f.key} className="space-y-1.5">
116
- <label className={labelClass}>{f.label}</label>
117
- <input type={f.type} value={fields[f.key]} onChange={e => set(f.key, e.target.value)} placeholder={f.placeholder} className={inputClass} />
118
- </div>
119
- ))}
120
- <div className="space-y-1.5">
121
- <label className={labelClass}>Stage</label>
122
- <select value={fields.lifecycle_stage} onChange={e => set('lifecycle_stage', e.target.value)} className={`${inputClass} pr-3`}>
123
- {LIFECYCLE_STAGES.map(s => <option key={s} value={s}>{s.charAt(0).toUpperCase() + s.slice(1)}</option>)}
124
- </select>
125
- </div>
126
- {users.length > 0 && (
127
- <div className="space-y-1.5">
128
- <label className={labelClass}>Owner</label>
129
- <select value={fields.owner_id} onChange={e => set('owner_id', e.target.value)} className={`${inputClass} pr-3`}>
130
- <option value="">Unassigned</option>
131
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
132
- {users.map((u: any) => (
133
- <option key={u.id} value={u.id}>{u.name || u.email}</option>
134
- ))}
135
- </select>
136
- </div>
137
- )}
138
- {fieldDefs.length > 0 && (
139
- <>
140
- <div className="border-t border-border pt-2">
141
- <p className={labelClass}>Custom Fields</p>
142
- </div>
143
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
144
- {fieldDefs.map((def: any) => (
145
- <div key={def.field_key} className="space-y-1.5">
146
- <label className={labelClass}>{def.label}{def.required && <span className="text-destructive ml-0.5">*</span>}</label>
147
- {(def.field_type === 'text' || !def.field_type) && (
148
- <input type="text" value={customFieldValues[def.field_key] ?? ''} onChange={e => setCF(def.field_key, e.target.value)} className={inputClass} />
149
- )}
150
- {def.field_type === 'number' && (
151
- <input type="number" value={customFieldValues[def.field_key] ?? ''} onChange={e => setCF(def.field_key, e.target.value)} className={inputClass} />
152
- )}
153
- {def.field_type === 'date' && (
154
- <DatePicker
155
- value={customFieldValues[def.field_key] ?? ''}
156
- onChange={val => setCF(def.field_key, val)}
157
- required={def.required}
158
- />
159
- )}
160
- {def.field_type === 'boolean' && (
161
- <div className="flex items-center gap-2 h-10">
162
- <input type="checkbox" checked={customFieldValues[def.field_key] === 'true'} onChange={e => setCF(def.field_key, e.target.checked ? 'true' : 'false')} className="w-4 h-4 rounded border-border accent-primary" />
163
- <span className="text-sm text-foreground">Yes</span>
164
- </div>
165
- )}
166
- {(def.field_type === 'select' || def.field_type === 'multi_select') && (
167
- <select value={customFieldValues[def.field_key] ?? ''} onChange={e => setCF(def.field_key, e.target.value)} className={`${inputClass} pr-3`}>
168
- <option value="">Select…</option>
169
- {(def.options ?? []).map((opt: string) => <option key={opt} value={opt}>{opt}</option>)}
170
- </select>
171
- )}
172
- </div>
173
- ))}
174
- </>
175
- )}
176
- <button
177
- onClick={handleSave}
178
- disabled={!fields.first_name.trim() || isSaving}
179
- 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"
180
- >
181
- {isSaving ? 'Saving…' : 'Save Changes'}
182
- </button>
183
- {!confirmDelete ? (
184
- <button
185
- onClick={() => setConfirmDelete(true)}
186
- className="w-full h-9 rounded-md border border-destructive/40 text-destructive text-sm font-medium hover:bg-destructive/10 transition-colors flex items-center justify-center gap-1.5"
187
- >
188
- <Trash2 className="w-3.5 h-3.5" /> Delete Contact
189
- </button>
190
- ) : (
191
- <div className="flex gap-2">
192
- <button onClick={() => setConfirmDelete(false)} className="flex-1 h-9 rounded-md border border-border text-sm text-muted-foreground hover:bg-muted/50 transition-colors">
193
- Cancel
194
- </button>
195
- <button onClick={onDelete} className="flex-1 h-9 rounded-md bg-destructive text-destructive-foreground text-sm font-medium hover:bg-destructive/90 transition-colors">
196
- Confirm Delete
197
- </button>
198
- </div>
199
- )}
200
- </div>
201
- </div>
202
- );
203
- }
204
-
205
- export function ContactDrawer() {
206
- const { drawerEntityId, openAIWithContext, closeDrawer } = useAppStore();
207
- const { enabled: agentEnabled } = useAgentSettings();
208
- const navigate = useNavigate();
209
- const [editing, setEditing] = useState(false);
210
- const [briefing, setBriefing] = useState(false);
211
- const [noting, setNoting] = useState(false);
212
- const [noteBody, setNoteBody] = useState('');
213
- const noteRef = useRef<HTMLTextAreaElement>(null);
214
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
215
- const { data: contactData, isLoading } = useContact(drawerEntityId ?? '') as any;
216
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
217
- const { data: activitiesData } = useActivities({ contact_id: drawerEntityId ?? undefined, limit: 20 }) as any;
218
- const activities: any[] = activitiesData?.data ?? [];
219
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
220
- const { data: notesData } = useNotes({ object_type: 'contact', object_id: drawerEntityId ?? '' }) as any;
221
- const notes: any[] = notesData?.data ?? [];
222
- const createNote = useCreateNote();
223
- const updateContact = useUpdateContact(drawerEntityId ?? '');
224
- const deleteContact = useDeleteContact(drawerEntityId ?? '');
225
-
226
- if (isLoading) {
227
- return (
228
- <div className="flex flex-col gap-4 p-6 animate-pulse">
229
- <div className="flex gap-4">
230
- <div className="w-14 h-14 rounded-2xl bg-muted" />
231
- <div className="flex-1 space-y-2">
232
- <div className="h-4 bg-muted rounded w-3/4" />
233
- <div className="h-3 bg-muted rounded w-1/2" />
234
- </div>
235
- </div>
236
- </div>
237
- );
238
- }
239
-
240
- if (!contactData?.contact) {
241
- return <div className="p-4 text-muted-foreground">Contact not found</div>;
242
- }
243
-
244
- const contact = contactData.contact;
245
- const name: string = [contact.first_name, contact.last_name].filter(Boolean).join(' ') || contact.email || '';
246
- const company: string = contact.company_name ?? '';
247
- const stage: string = contact.lifecycle_stage ?? '';
248
- const leadScore: number = contact.lead_score ?? 0;
249
-
250
- if (briefing) {
251
- return <BriefingPanel subjectType="contact" subjectId={drawerEntityId!} onClose={() => setBriefing(false)} />;
252
- }
253
-
254
- if (editing) {
255
- return (
256
- <ContactEditForm
257
- contact={contact}
258
- onSave={async (data) => {
259
- try {
260
- await updateContact.mutateAsync(data);
261
- setEditing(false);
262
- toast({ title: 'Contact updated' });
263
- } catch (err) {
264
- toast({ title: 'Failed to update contact', description: err instanceof Error ? err.message : 'Please try again.', variant: 'destructive' });
265
- }
266
- }}
267
- onCancel={() => setEditing(false)}
268
- onDelete={async () => {
269
- try {
270
- await deleteContact.mutateAsync();
271
- closeDrawer();
272
- toast({ title: 'Contact deleted' });
273
- } catch (err) {
274
- toast({ title: 'Failed to delete contact', description: err instanceof Error ? err.message : 'Please try again.', variant: 'destructive' });
275
- }
276
- }}
277
- isSaving={updateContact.isPending}
278
- />
279
- );
280
- }
281
-
282
- return (
283
- <div className="flex flex-col">
284
- {/* Header */}
285
- <div className="p-6 border-b border-border">
286
- <div className="flex items-start gap-4">
287
- <ContactAvatar name={name} className="w-14 h-14 rounded-2xl text-lg" />
288
- <div className="flex-1">
289
- <h2 className="font-display font-extrabold text-xl text-foreground">{name}</h2>
290
- {company && <p className="text-sm text-muted-foreground">{company}</p>}
291
- <div className="flex items-center gap-2 mt-2">
292
- {stage && <StageBadge stage={stage} />}
293
- {leadScore > 0 && <LeadScoreBadge score={leadScore} />}
294
- </div>
295
- </div>
296
- </div>
297
- <div className="flex gap-2 mt-4">
298
- {contact.phone && (
299
- <a
300
- href={`tel:${contact.phone}`}
301
- className="flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-primary text-primary-foreground text-sm font-semibold hover:bg-primary/90 transition-all press-scale"
302
- >
303
- <Phone className="w-3.5 h-3.5" /> Call
304
- </a>
305
- )}
306
- {contact.email && (
307
- <a
308
- href={`mailto:${contact.email}`}
309
- className="flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-muted text-foreground text-sm font-medium hover:bg-muted/80 transition-all press-scale"
310
- >
311
- <Mail className="w-3.5 h-3.5" /> Email
312
- </a>
313
- )}
314
- <button
315
- onClick={() => { setNoting(v => !v); setTimeout(() => noteRef.current?.focus(), 50); }}
316
- className={`flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-sm font-medium transition-all press-scale ${noting ? 'bg-primary text-primary-foreground' : 'bg-muted text-foreground hover:bg-muted/80'}`}
317
- >
318
- <StickyNote className="w-3.5 h-3.5" /> Note
319
- </button>
320
- <button
321
- onClick={() => setEditing(true)}
322
- className="flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-muted text-foreground text-sm font-medium hover:bg-muted/80 transition-all press-scale"
323
- >
324
- <Pencil className="w-3.5 h-3.5" /> Edit
325
- </button>
326
- <button
327
- onClick={() => setBriefing(true)}
328
- className="flex items-center gap-1.5 px-3.5 py-2 rounded-xl bg-muted text-foreground text-sm font-medium hover:bg-muted/80 transition-all press-scale"
329
- >
330
- <FileText className="w-3.5 h-3.5" /> Brief
331
- </button>
332
- {agentEnabled && (
333
- <button
334
- onClick={() => {
335
- openAIWithContext({ type: 'contact', id: contact.id, name, detail: company });
336
- closeDrawer();
337
- navigate('/agent');
338
- }}
339
- className="flex items-center gap-1.5 px-3.5 py-2 rounded-xl border border-accent/30 bg-accent/5 text-accent text-sm font-semibold hover:bg-accent/10 transition-all ml-auto press-scale"
340
- >
341
- <Sparkles className="w-3.5 h-3.5" /> Chat
342
- </button>
343
- )}
344
- </div>
345
- </div>
346
-
347
- {/* Note compose panel */}
348
- {noting && (
349
- <div className="mx-4 mt-4 rounded-xl border border-border bg-card p-3 space-y-2">
350
- <textarea
351
- ref={noteRef}
352
- value={noteBody}
353
- onChange={e => setNoteBody(e.target.value)}
354
- placeholder="Write a note…"
355
- rows={3}
356
- 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"
357
- />
358
- <div className="flex items-center justify-end gap-2">
359
- <button onClick={() => { setNoting(false); setNoteBody(''); }} className="text-xs text-muted-foreground hover:text-foreground">Cancel</button>
360
- <button
361
- disabled={!noteBody.trim() || createNote.isPending}
362
- onClick={async () => {
363
- await createNote.mutateAsync({ object_type: 'contact', object_id: drawerEntityId, body: noteBody.trim() });
364
- setNoteBody('');
365
- setNoting(false);
366
- toast({ title: 'Note saved' });
367
- }}
368
- className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-primary text-primary-foreground text-xs font-semibold disabled:opacity-40 transition-colors"
369
- >
370
- <Send className="w-3 h-3" /> Save
371
- </button>
372
- </div>
373
- </div>
374
- )}
375
-
376
- {/* Notes list */}
377
- {notes.length > 0 && (
378
- <div className="p-4 mx-4 mt-4 space-y-3">
379
- <h3 className="text-xs font-display font-bold text-muted-foreground uppercase tracking-wide">Notes</h3>
380
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
381
- {notes.map((note: any) => (
382
- <div key={note.id} className="rounded-xl bg-muted/50 p-3 space-y-1">
383
- <div className="flex items-center gap-1.5">
384
- {note.pinned && <Pin className="w-3 h-3 text-accent" />}
385
- <span className="text-[10px] text-muted-foreground ml-auto">
386
- {new Date(note.created_at).toLocaleDateString()}
387
- </span>
388
- </div>
389
- <p className="text-sm text-foreground whitespace-pre-wrap">{note.body}</p>
390
- {note.author_type && (
391
- <p className="text-[10px] text-muted-foreground capitalize">{note.author_type === 'agent' ? 'AI Agent' : note.author_type}</p>
392
- )}
393
- </div>
394
- ))}
395
- </div>
396
- )}
397
-
398
- {/* Details */}
399
- <div className="p-4 mx-4 mt-4 space-y-3">
400
- <h3 className="text-xs font-display font-bold text-muted-foreground uppercase tracking-wide">Details</h3>
401
- {[
402
- { label: 'Email', value: contact.email },
403
- { label: 'Phone', value: contact.phone },
404
- { label: 'Company', value: company },
405
- { label: 'Source', value: contact.source },
406
- { label: 'Last Contacted', value: contact.last_contacted_at ? new Date(contact.last_contacted_at).toLocaleDateString() : undefined },
407
- { label: 'Created', value: contact.created_at ? new Date(contact.created_at).toLocaleDateString() : undefined },
408
- ]
409
- .filter((f) => f.value)
410
- .map((field) => (
411
- <div key={field.label} className="flex items-center justify-between">
412
- <span className="text-xs text-muted-foreground">{field.label}</span>
413
- <span className="text-sm text-foreground">{field.value}</span>
414
- </div>
415
- ))}
416
- {contact.tags && contact.tags.length > 0 && (
417
- <div className="flex items-center gap-1.5 flex-wrap mt-2">
418
- {(contact.tags as string[]).map((tag: string) => (
419
- <span key={tag} className="px-2.5 py-1 rounded-lg bg-muted text-xs text-muted-foreground font-medium">{tag}</span>
420
- ))}
421
- </div>
422
- )}
423
- </div>
424
-
425
- {/* Custom Fields */}
426
- <CustomFieldsSection objectType="contact" values={(contact.custom_fields ?? {}) as Record<string, unknown>} />
427
-
428
- {/* Context */}
429
- <ContextPanel subjectType="contact" subjectId={drawerEntityId!} />
430
-
431
- {/* Timeline */}
432
- <div className="p-4 mx-4 mt-4 mb-6">
433
- <h3 className="text-xs font-display font-bold text-muted-foreground uppercase tracking-wide mb-3">Timeline</h3>
434
- <ActivityTimeline activities={activities} />
435
- </div>
436
- </div>
437
- );
438
- }
@@ -1,200 +0,0 @@
1
- // Copyright 2026 CRMy Contributors
2
- // SPDX-License-Identifier: Apache-2.0
3
-
4
- import { useState } from 'react';
5
- import { useContextEntries, useReviewContextEntry, useStaleContextEntries } from '@/api/hooks';
6
- import { Brain, AlertTriangle, CheckCircle2, ChevronDown, ChevronUp, Tag } from 'lucide-react';
7
- import { toast } from '@/components/ui/use-toast';
8
-
9
- interface ContextPanelProps {
10
- subjectType: string;
11
- subjectId: string;
12
- }
13
-
14
- interface ContextEntry {
15
- id: string;
16
- context_type: string;
17
- title?: string;
18
- body: string;
19
- confidence?: number;
20
- source?: string;
21
- tags?: string[];
22
- is_current: boolean;
23
- valid_until?: string;
24
- reviewed_at?: string;
25
- created_at: string;
26
- }
27
-
28
- const TYPE_COLORS: Record<string, string> = {
29
- note: '#6366f1',
30
- research: '#8b5cf6',
31
- preference: '#ec4899',
32
- objection: '#ef4444',
33
- competitive_intel: '#f97316',
34
- relationship_map: '#14b8a6',
35
- meeting_notes: '#3b82f6',
36
- summary: '#06b6d4',
37
- transcript: '#64748b',
38
- agent_reasoning: '#a855f7',
39
- };
40
-
41
- export function ContextPanel({ subjectType, subjectId }: ContextPanelProps) {
42
- const [expanded, setExpanded] = useState(true);
43
- const [activeType, setActiveType] = useState<string | null>(null);
44
-
45
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
- const { data: contextData, isLoading } = useContextEntries({ subject_type: subjectType, subject_id: subjectId, is_current: true, limit: 50 }) as any;
47
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
- const { data: staleData } = useStaleContextEntries({ subject_type: subjectType, subject_id: subjectId, limit: 10 }) as any;
49
- const reviewMutation = useReviewContextEntry();
50
-
51
- const entries: ContextEntry[] = contextData?.data ?? [];
52
- const staleEntries: ContextEntry[] = staleData?.data ?? [];
53
-
54
- // Group entries by context_type
55
- const grouped = entries.reduce<Record<string, ContextEntry[]>>((acc, entry) => {
56
- const type = entry.context_type ?? 'note';
57
- if (!acc[type]) acc[type] = [];
58
- acc[type].push(entry);
59
- return acc;
60
- }, {});
61
-
62
- const types = Object.keys(grouped).sort();
63
- const displayed = activeType ? (grouped[activeType] ?? []) : entries;
64
-
65
- const handleReview = async (id: string) => {
66
- await reviewMutation.mutateAsync(id);
67
- toast({ title: 'Context entry reviewed' });
68
- };
69
-
70
- if (isLoading) {
71
- return (
72
- <div className="space-y-2 animate-pulse">
73
- <div className="h-4 bg-muted rounded w-1/3" />
74
- <div className="h-16 bg-muted rounded" />
75
- <div className="h-16 bg-muted rounded" />
76
- </div>
77
- );
78
- }
79
-
80
- if (entries.length === 0 && staleEntries.length === 0) {
81
- return null;
82
- }
83
-
84
- return (
85
- <div className="px-4 mx-4 mt-4">
86
- <button
87
- onClick={() => setExpanded(!expanded)}
88
- className="flex items-center gap-1.5 w-full text-left mb-2"
89
- >
90
- <Brain className="w-3.5 h-3.5 text-primary" />
91
- <h3 className="text-xs font-display font-bold text-muted-foreground uppercase tracking-wide flex-1">
92
- Context
93
- <span className="ml-1.5 font-mono font-normal text-muted-foreground/50">({entries.length})</span>
94
- </h3>
95
- {staleEntries.length > 0 && (
96
- <span className="flex items-center gap-1 text-[10px] text-warning font-medium">
97
- <AlertTriangle className="w-3 h-3" /> {staleEntries.length} stale
98
- </span>
99
- )}
100
- {expanded ? <ChevronUp className="w-3.5 h-3.5 text-muted-foreground" /> : <ChevronDown className="w-3.5 h-3.5 text-muted-foreground" />}
101
- </button>
102
-
103
- {expanded && (
104
- <div className="space-y-3">
105
- {/* Type filter tabs */}
106
- {types.length > 1 && (
107
- <div className="flex gap-1 flex-wrap">
108
- <button
109
- onClick={() => setActiveType(null)}
110
- className={`px-2 py-0.5 rounded-md text-[10px] font-semibold transition-all ${!activeType ? 'bg-primary/15 text-primary' : 'bg-muted text-muted-foreground hover:text-foreground'}`}
111
- >
112
- All
113
- </button>
114
- {types.map(type => (
115
- <button
116
- key={type}
117
- onClick={() => setActiveType(activeType === type ? null : type)}
118
- className={`px-2 py-0.5 rounded-md text-[10px] font-semibold transition-all ${activeType === type ? 'bg-primary/15 text-primary' : 'bg-muted text-muted-foreground hover:text-foreground'}`}
119
- >
120
- {type.replace(/_/g, ' ')}
121
- <span className="ml-1 opacity-50">{grouped[type].length}</span>
122
- </button>
123
- ))}
124
- </div>
125
- )}
126
-
127
- {/* Stale warnings */}
128
- {staleEntries.length > 0 && !activeType && (
129
- <div className="rounded-xl border border-warning/30 bg-warning/5 p-3 space-y-2">
130
- <p className="text-[10px] font-semibold text-warning uppercase tracking-wide">Needs Review</p>
131
- {staleEntries.map(entry => (
132
- <div key={entry.id} className="flex items-start gap-2">
133
- <div className="flex-1 min-w-0">
134
- <p className="text-xs text-foreground truncate">{entry.title ?? entry.context_type}</p>
135
- <p className="text-[10px] text-muted-foreground">
136
- Expired {entry.valid_until ? new Date(entry.valid_until).toLocaleDateString() : ''}
137
- </p>
138
- </div>
139
- <button
140
- onClick={() => handleReview(entry.id)}
141
- disabled={reviewMutation.isPending}
142
- className="flex items-center gap-1 px-2 py-1 rounded-md text-[10px] font-medium bg-primary/10 text-primary hover:bg-primary/20 transition-colors"
143
- >
144
- <CheckCircle2 className="w-3 h-3" /> Review
145
- </button>
146
- </div>
147
- ))}
148
- </div>
149
- )}
150
-
151
- {/* Context entries */}
152
- {displayed.map(entry => {
153
- const color = TYPE_COLORS[entry.context_type] ?? '#94a3b8';
154
- const isStale = entry.valid_until && new Date(entry.valid_until) < new Date();
155
- return (
156
- <div
157
- key={entry.id}
158
- className={`rounded-xl border p-3 space-y-1.5 ${isStale ? 'border-warning/30 bg-warning/5' : 'border-border bg-card'}`}
159
- >
160
- <div className="flex items-center gap-2">
161
- <span
162
- className="px-1.5 py-0.5 rounded text-[10px] font-semibold capitalize"
163
- style={{ backgroundColor: color + '18', color }}
164
- >
165
- {entry.context_type.replace(/_/g, ' ')}
166
- </span>
167
- {entry.confidence != null && (
168
- <span className="text-[10px] text-muted-foreground font-mono">
169
- {Math.round(entry.confidence * 100)}%
170
- </span>
171
- )}
172
- <span className="text-[10px] text-muted-foreground ml-auto">
173
- {new Date(entry.created_at).toLocaleDateString()}
174
- </span>
175
- </div>
176
- {entry.title && (
177
- <p className="text-sm font-medium text-foreground">{entry.title}</p>
178
- )}
179
- <p className="text-xs text-muted-foreground line-clamp-3 whitespace-pre-wrap">
180
- {entry.body}
181
- </p>
182
- {entry.tags && entry.tags.length > 0 && (
183
- <div className="flex items-center gap-1 flex-wrap">
184
- <Tag className="w-2.5 h-2.5 text-muted-foreground" />
185
- {entry.tags.map(tag => (
186
- <span key={tag} className="px-1.5 py-0.5 rounded bg-muted text-[10px] text-muted-foreground">{tag}</span>
187
- ))}
188
- </div>
189
- )}
190
- {entry.source && (
191
- <p className="text-[10px] text-muted-foreground/60">Source: {entry.source}</p>
192
- )}
193
- </div>
194
- );
195
- })}
196
- </div>
197
- )}
198
- </div>
199
- );
200
- }