@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.
Files changed (119) hide show
  1. package/index.html +23 -0
  2. package/package.json +76 -0
  3. package/postcss.config.js +6 -0
  4. package/public/android-chrome-192x192.png +0 -0
  5. package/public/android-chrome-512x512.png +0 -0
  6. package/public/apple-touch-icon.png +0 -0
  7. package/public/favicon-16x16.png +0 -0
  8. package/public/favicon-32x32.png +0 -0
  9. package/public/favicon.ico +0 -0
  10. package/public/favicon.svg +13 -0
  11. package/public/site.webmanifest +1 -0
  12. package/src/App.tsx +158 -0
  13. package/src/api/client.ts +82 -0
  14. package/src/api/hooks.ts +689 -0
  15. package/src/assets/crmy-logo.png +0 -0
  16. package/src/components/CustomFields.tsx +240 -0
  17. package/src/components/NavLink.tsx +28 -0
  18. package/src/components/crm/AIFab.tsx +37 -0
  19. package/src/components/crm/AccountDrawer.tsx +372 -0
  20. package/src/components/crm/ActivityTimeline.tsx +115 -0
  21. package/src/components/crm/AssignmentDrawer.tsx +396 -0
  22. package/src/components/crm/BriefingPanel.tsx +217 -0
  23. package/src/components/crm/CommandPalette.tsx +254 -0
  24. package/src/components/crm/ContactAvatar.tsx +49 -0
  25. package/src/components/crm/ContactDrawer.tsx +438 -0
  26. package/src/components/crm/ContextPanel.tsx +200 -0
  27. package/src/components/crm/CrmWidgets.tsx +417 -0
  28. package/src/components/crm/DrawerShell.tsx +77 -0
  29. package/src/components/crm/ListToolbar.tsx +252 -0
  30. package/src/components/crm/OpportunityDrawer.tsx +372 -0
  31. package/src/components/crm/PaginationBar.tsx +111 -0
  32. package/src/components/crm/QuickAddDrawer.tsx +652 -0
  33. package/src/components/crm/ShortcutsOverlay.tsx +65 -0
  34. package/src/components/crm/UseCaseDrawer.tsx +454 -0
  35. package/src/components/layout/MobileNav.tsx +49 -0
  36. package/src/components/layout/Sidebar.tsx +157 -0
  37. package/src/components/layout/TopBar.tsx +54 -0
  38. package/src/components/settings/ActorsSettings.tsx +1190 -0
  39. package/src/components/ui/accordion.tsx +52 -0
  40. package/src/components/ui/alert-dialog.tsx +104 -0
  41. package/src/components/ui/alert.tsx +43 -0
  42. package/src/components/ui/aspect-ratio.tsx +5 -0
  43. package/src/components/ui/avatar.tsx +38 -0
  44. package/src/components/ui/badge.tsx +29 -0
  45. package/src/components/ui/breadcrumb.tsx +90 -0
  46. package/src/components/ui/button.tsx +47 -0
  47. package/src/components/ui/calendar.tsx +54 -0
  48. package/src/components/ui/card.tsx +43 -0
  49. package/src/components/ui/carousel.tsx +224 -0
  50. package/src/components/ui/chart.tsx +303 -0
  51. package/src/components/ui/checkbox.tsx +26 -0
  52. package/src/components/ui/collapsible.tsx +9 -0
  53. package/src/components/ui/command.tsx +132 -0
  54. package/src/components/ui/context-menu.tsx +178 -0
  55. package/src/components/ui/date-picker.tsx +313 -0
  56. package/src/components/ui/dialog.tsx +95 -0
  57. package/src/components/ui/drawer.tsx +87 -0
  58. package/src/components/ui/dropdown-menu.tsx +179 -0
  59. package/src/components/ui/form.tsx +129 -0
  60. package/src/components/ui/hover-card.tsx +27 -0
  61. package/src/components/ui/input-otp.tsx +61 -0
  62. package/src/components/ui/input.tsx +22 -0
  63. package/src/components/ui/label.tsx +17 -0
  64. package/src/components/ui/menubar.tsx +207 -0
  65. package/src/components/ui/navigation-menu.tsx +120 -0
  66. package/src/components/ui/pagination.tsx +81 -0
  67. package/src/components/ui/popover.tsx +29 -0
  68. package/src/components/ui/progress.tsx +23 -0
  69. package/src/components/ui/radio-group.tsx +36 -0
  70. package/src/components/ui/resizable.tsx +37 -0
  71. package/src/components/ui/scroll-area.tsx +38 -0
  72. package/src/components/ui/select.tsx +143 -0
  73. package/src/components/ui/separator.tsx +20 -0
  74. package/src/components/ui/sheet.tsx +107 -0
  75. package/src/components/ui/sidebar.tsx +637 -0
  76. package/src/components/ui/skeleton.tsx +7 -0
  77. package/src/components/ui/slider.tsx +23 -0
  78. package/src/components/ui/sonner.tsx +24 -0
  79. package/src/components/ui/switch.tsx +27 -0
  80. package/src/components/ui/table.tsx +72 -0
  81. package/src/components/ui/tabs.tsx +53 -0
  82. package/src/components/ui/textarea.tsx +21 -0
  83. package/src/components/ui/toast.tsx +111 -0
  84. package/src/components/ui/toaster.tsx +24 -0
  85. package/src/components/ui/toggle-group.tsx +49 -0
  86. package/src/components/ui/toggle.tsx +37 -0
  87. package/src/components/ui/tooltip.tsx +28 -0
  88. package/src/components/ui/use-toast.ts +1 -0
  89. package/src/components/ui/utils.ts +9 -0
  90. package/src/contexts/AgentSettingsContext.tsx +24 -0
  91. package/src/hooks/use-mobile.tsx +19 -0
  92. package/src/hooks/use-toast.ts +186 -0
  93. package/src/hooks/useKeyboardShortcuts.ts +95 -0
  94. package/src/hooks/useTheme.ts +24 -0
  95. package/src/index.css +245 -0
  96. package/src/lib/entityColors.ts +18 -0
  97. package/src/lib/stageConfig.ts +32 -0
  98. package/src/lib/utils.ts +6 -0
  99. package/src/main.tsx +25 -0
  100. package/src/pages/Accounts.tsx +205 -0
  101. package/src/pages/Activities.tsx +251 -0
  102. package/src/pages/Agent.tsx +237 -0
  103. package/src/pages/AgentSettings.tsx +544 -0
  104. package/src/pages/Assignments.tsx +750 -0
  105. package/src/pages/Contacts.tsx +200 -0
  106. package/src/pages/Dashboard.tsx +143 -0
  107. package/src/pages/Inbox.tsx +615 -0
  108. package/src/pages/NotFound.tsx +24 -0
  109. package/src/pages/Opportunities.tsx +386 -0
  110. package/src/pages/SearchResults.tsx +49 -0
  111. package/src/pages/Settings.tsx +1884 -0
  112. package/src/pages/UseCases.tsx +396 -0
  113. package/src/pages/auth/Login.tsx +261 -0
  114. package/src/pages/hitl/HITL.tsx +101 -0
  115. package/src/store/appStore.ts +103 -0
  116. package/src/vite-env.d.ts +14 -0
  117. package/tailwind.config.js +121 -0
  118. package/tsconfig.json +24 -0
  119. package/vite.config.ts +27 -0
@@ -0,0 +1,65 @@
1
+ import { motion, AnimatePresence } from 'framer-motion';
2
+ import { useAppStore } from '@/store/appStore';
3
+
4
+ const shortcuts = [
5
+ { group: 'Global', items: [
6
+ { keys: ['⌘', 'K'], label: 'Command palette' },
7
+ { keys: ['⌘', '⇧', 'A'], label: 'AI Agent panel' },
8
+ { keys: ['⌘', '⇧', 'Z'], label: 'Zen mode' },
9
+ { keys: ['?'], label: 'Keyboard shortcuts' },
10
+ { keys: ['Esc'], label: 'Close drawer / modal' },
11
+ ]},
12
+ { group: 'Navigation', items: [
13
+ { keys: ['G', 'H'], label: 'Go to Dashboard' },
14
+ { keys: ['G', 'C'], label: 'Go to Contacts' },
15
+ { keys: ['G', 'D'], label: 'Go to Opportunities' },
16
+ ]},
17
+ ];
18
+
19
+ export function ShortcutsOverlay() {
20
+ const { shortcutsOpen, setShortcutsOpen } = useAppStore();
21
+
22
+ return (
23
+ <AnimatePresence>
24
+ {shortcutsOpen && (
25
+ <>
26
+ <motion.div
27
+ initial={{ opacity: 0 }}
28
+ animate={{ opacity: 1 }}
29
+ exit={{ opacity: 0 }}
30
+ className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm"
31
+ onClick={() => setShortcutsOpen(false)}
32
+ />
33
+ <motion.div
34
+ initial={{ opacity: 0, scale: 0.95 }}
35
+ animate={{ opacity: 1, scale: 1 }}
36
+ exit={{ opacity: 0, scale: 0.95 }}
37
+ className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-full max-w-md bg-card border border-border rounded-lg shadow-2xl p-6"
38
+ >
39
+ <h2 className="font-display font-bold text-lg text-foreground mb-4">Keyboard Shortcuts</h2>
40
+ {shortcuts.map((group) => (
41
+ <div key={group.group} className="mb-4">
42
+ <h3 className="text-xs font-mono text-muted-foreground uppercase tracking-wider mb-2">{group.group}</h3>
43
+ <div className="space-y-2">
44
+ {group.items.map((item) => (
45
+ <div key={item.label} className="flex items-center justify-between">
46
+ <span className="text-sm text-foreground">{item.label}</span>
47
+ <div className="flex gap-1">
48
+ {item.keys.map((key) => (
49
+ <kbd key={key} className="px-1.5 py-0.5 text-xs font-mono bg-muted text-muted-foreground rounded border border-border">
50
+ {key}
51
+ </kbd>
52
+ ))}
53
+ </div>
54
+ </div>
55
+ ))}
56
+ </div>
57
+ </div>
58
+ ))}
59
+ <p className="text-xs text-muted-foreground mt-4">Press <kbd className="px-1 py-0.5 text-xs font-mono bg-muted rounded border border-border">Esc</kbd> to close</p>
60
+ </motion.div>
61
+ </>
62
+ )}
63
+ </AnimatePresence>
64
+ );
65
+ }
@@ -0,0 +1,454 @@
1
+ // Copyright 2026 CRMy Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { useState } from 'react';
5
+ import { useUseCase, useUseCaseTimeline, useUpdateUseCase, useDeleteUseCase, useUsers, useCustomFields, useOpportunities } from '@/api/hooks';
6
+ import { useNavigate } from 'react-router-dom';
7
+ import { useAppStore } from '@/store/appStore';
8
+ import { useAgentSettings } from '@/contexts/AgentSettingsContext';
9
+ import { Sparkles, Calendar, Bot, DollarSign, Pencil, ChevronLeft, Trash2, FileText } from 'lucide-react';
10
+ import { ContextPanel } from './ContextPanel';
11
+ import { BriefingPanel } from './BriefingPanel';
12
+ import { CustomFieldsSection } from './CrmWidgets';
13
+ import { ActivityTimeline } from './ActivityTimeline';
14
+ import { useCaseStageConfig } from '@/lib/stageConfig';
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 UC_STAGES = ['discovery', 'poc', 'production', 'scaling', 'sunset'];
22
+
23
+ function UseCaseStageBadge({ stage }: { stage: string }) {
24
+ const config = useCaseStageConfig[stage] ?? { label: stage, color: '#94a3b8' };
25
+ return (
26
+ <span
27
+ className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold"
28
+ style={{ backgroundColor: config.color + '18', color: config.color }}
29
+ >
30
+ {config.label}
31
+ </span>
32
+ );
33
+ }
34
+
35
+ function UseCaseEditForm({
36
+ useCase,
37
+ onSave,
38
+ onCancel,
39
+ onDelete,
40
+ isSaving,
41
+ }: {
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ useCase: any;
44
+ onSave: (data: Record<string, unknown>) => void;
45
+ onCancel: () => void;
46
+ onDelete: () => void;
47
+ isSaving: boolean;
48
+ }) {
49
+ const [confirmDelete, setConfirmDelete] = useState(false);
50
+ const [fields, setFields] = useState<Record<string, string>>({
51
+ name: useCase.name ?? '',
52
+ stage: useCase.stage ?? 'discovery',
53
+ description: useCase.description ?? '',
54
+ attributed_arr: useCase.attributed_arr != null ? String(useCase.attributed_arr) : '',
55
+ currency_code: useCase.currency_code ?? 'USD',
56
+ expansion_potential: useCase.expansion_potential != null ? String(useCase.expansion_potential) : '',
57
+ unit_label: useCase.unit_label ?? '',
58
+ consumption_unit: useCase.consumption_unit ?? '',
59
+ consumption_capacity: useCase.consumption_capacity != null ? String(useCase.consumption_capacity) : '',
60
+ started_at: useCase.started_at ? useCase.started_at.slice(0, 10) : '',
61
+ target_prod_date: useCase.target_prod_date ? useCase.target_prod_date.slice(0, 10) : '',
62
+ sunset_date: useCase.sunset_date ? useCase.sunset_date.slice(0, 10) : '',
63
+ tags: Array.isArray(useCase.tags) ? useCase.tags.join(', ') : '',
64
+ opportunity_id: useCase.opportunity_id ?? '',
65
+ owner_id: useCase.owner_id ?? '',
66
+ });
67
+
68
+ const [customFieldValues, setCustomFieldValues] = useState<Record<string, string>>(() => {
69
+ const init: Record<string, string> = {};
70
+ if (useCase.custom_fields) {
71
+ for (const [k, v] of Object.entries(useCase.custom_fields as Record<string, unknown>)) {
72
+ init[k] = String(v ?? '');
73
+ }
74
+ }
75
+ return init;
76
+ });
77
+
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ const { data: usersData } = useUsers() as any;
80
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
+ const users: any[] = usersData?.data ?? [];
82
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
+ const { data: oppsData } = useOpportunities({ limit: 200 }) as any;
84
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
+ const opportunities: any[] = oppsData?.data ?? [];
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
+ const { data: customFieldDefs } = useCustomFields('use_case') as any;
88
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
+ const fieldDefs: any[] = customFieldDefs?.fields ?? [];
90
+
91
+ const set = (key: string, val: string) => setFields(prev => ({ ...prev, [key]: val }));
92
+ const setCF = (key: string, val: string) => setCustomFieldValues(prev => ({ ...prev, [key]: val }));
93
+
94
+ const handleSave = () => {
95
+ const payload: Record<string, unknown> = {};
96
+ const numericKeys = ['attributed_arr', 'expansion_potential', 'consumption_capacity'];
97
+ for (const [k, v] of Object.entries(fields)) {
98
+ if (k === 'tags') continue; // handled separately
99
+ if (v === '') continue;
100
+ if (numericKeys.includes(k)) payload[k] = Number(v) || 0;
101
+ else payload[k] = v;
102
+ }
103
+ // Tags: parse comma-separated string into array
104
+ const tagsRaw = fields.tags.trim();
105
+ payload.tags = tagsRaw ? tagsRaw.split(',').map(t => t.trim()).filter(Boolean) : [];
106
+ const cfPayload: Record<string, unknown> = {};
107
+ for (const def of fieldDefs) {
108
+ const val = customFieldValues[def.field_key] ?? '';
109
+ if (val === '') continue;
110
+ if (def.field_type === 'number') cfPayload[def.field_key] = Number(val);
111
+ else if (def.field_type === 'boolean') cfPayload[def.field_key] = val === 'true';
112
+ else cfPayload[def.field_key] = val;
113
+ }
114
+ if (Object.keys(cfPayload).length > 0) payload.custom_fields = cfPayload;
115
+ onSave(payload);
116
+ };
117
+
118
+ const sectionLabel = (text: string) => (
119
+ <div className="border-t border-border pt-3 mt-1">
120
+ <p className={labelClass}>{text}</p>
121
+ </div>
122
+ );
123
+
124
+ return (
125
+ <div className="flex flex-col h-full">
126
+ <div className="flex items-center gap-2 px-5 py-3 border-b border-border">
127
+ <button onClick={onCancel} className="flex items-center gap-1 text-xs text-accent hover:underline">
128
+ <ChevronLeft className="w-3.5 h-3.5" /> Back
129
+ </button>
130
+ <span className="text-xs text-muted-foreground ml-auto">Editing use case</span>
131
+ </div>
132
+ <div className="flex-1 overflow-y-auto p-5 space-y-4">
133
+
134
+ {/* Basic */}
135
+ <div className="space-y-1.5">
136
+ <label className={labelClass}>Name<span className="text-destructive ml-0.5">*</span></label>
137
+ <input type="text" value={fields.name} onChange={e => set('name', e.target.value)} placeholder="Use case name" className={inputClass} />
138
+ </div>
139
+ <div className="space-y-1.5">
140
+ <label className={labelClass}>Stage</label>
141
+ <select value={fields.stage} onChange={e => set('stage', e.target.value)} className={`${inputClass} pr-3`}>
142
+ {UC_STAGES.map(s => (
143
+ <option key={s} value={s}>{useCaseStageConfig[s]?.label ?? s}</option>
144
+ ))}
145
+ </select>
146
+ </div>
147
+ <div className="space-y-1.5">
148
+ <label className={labelClass}>Description</label>
149
+ <textarea
150
+ value={fields.description}
151
+ onChange={e => set('description', e.target.value)}
152
+ placeholder="Optional description"
153
+ rows={3}
154
+ 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"
155
+ />
156
+ </div>
157
+ <div className="space-y-1.5">
158
+ <label className={labelClass}>Tags <span className="normal-case font-normal">(comma-separated)</span></label>
159
+ <input type="text" value={fields.tags} onChange={e => set('tags', e.target.value)} placeholder="e.g. ai, billing, high-priority" className={inputClass} />
160
+ </div>
161
+
162
+ {/* Commercial */}
163
+ {sectionLabel('Commercial')}
164
+ <div className="space-y-1.5">
165
+ <label className={labelClass}>Attributed ARR ($)</label>
166
+ <input type="number" value={fields.attributed_arr} onChange={e => set('attributed_arr', e.target.value)} placeholder="e.g. 120000" className={inputClass} />
167
+ </div>
168
+ <div className="space-y-1.5">
169
+ <label className={labelClass}>Expansion Potential ($)</label>
170
+ <input type="number" value={fields.expansion_potential} onChange={e => set('expansion_potential', e.target.value)} placeholder="e.g. 50000" className={inputClass} />
171
+ </div>
172
+ <div className="space-y-1.5">
173
+ <label className={labelClass}>Currency</label>
174
+ <input type="text" value={fields.currency_code} onChange={e => set('currency_code', e.target.value.toUpperCase().slice(0, 3))} placeholder="USD" maxLength={3} className={inputClass} />
175
+ </div>
176
+
177
+ {/* Consumption */}
178
+ {sectionLabel('Consumption')}
179
+ <div className="space-y-1.5">
180
+ <label className={labelClass}>Unit Label</label>
181
+ <input type="text" value={fields.unit_label} onChange={e => set('unit_label', e.target.value)} placeholder="e.g. API calls, seats, documents" className={inputClass} />
182
+ </div>
183
+ <div className="space-y-1.5">
184
+ <label className={labelClass}>Consumption Unit</label>
185
+ <input type="text" value={fields.consumption_unit} onChange={e => set('consumption_unit', e.target.value)} placeholder="e.g. calls/month" className={inputClass} />
186
+ </div>
187
+ <div className="space-y-1.5">
188
+ <label className={labelClass}>Consumption Capacity</label>
189
+ <input type="number" value={fields.consumption_capacity} onChange={e => set('consumption_capacity', e.target.value)} placeholder="e.g. 10000" className={inputClass} />
190
+ </div>
191
+
192
+ {/* Timeline */}
193
+ {sectionLabel('Timeline')}
194
+ <div className="space-y-1.5">
195
+ <label className={labelClass}>Start Date</label>
196
+ <DatePicker value={fields.started_at} onChange={val => set('started_at', val)} placeholder="Select start date" />
197
+ </div>
198
+ <div className="space-y-1.5">
199
+ <label className={labelClass}>Production Date</label>
200
+ <DatePicker value={fields.target_prod_date} onChange={val => set('target_prod_date', val)} placeholder="Select production date" />
201
+ </div>
202
+ <div className="space-y-1.5">
203
+ <label className={labelClass}>Sunset Date</label>
204
+ <DatePicker value={fields.sunset_date} onChange={val => set('sunset_date', val)} placeholder="Select sunset date" />
205
+ </div>
206
+
207
+ {/* Ownership */}
208
+ {sectionLabel('Ownership')}
209
+ {opportunities.length > 0 && (
210
+ <div className="space-y-1.5">
211
+ <label className={labelClass}>Linked Opportunity</label>
212
+ <select value={fields.opportunity_id} onChange={e => set('opportunity_id', e.target.value)} className={`${inputClass} pr-3`}>
213
+ <option value="">None</option>
214
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
215
+ {opportunities.map((o: any) => (
216
+ <option key={o.id} value={o.id}>{o.name}</option>
217
+ ))}
218
+ </select>
219
+ </div>
220
+ )}
221
+ {users.length > 0 && (
222
+ <div className="space-y-1.5">
223
+ <label className={labelClass}>Owner</label>
224
+ <select value={fields.owner_id} onChange={e => set('owner_id', e.target.value)} className={`${inputClass} pr-3`}>
225
+ <option value="">Unassigned</option>
226
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
227
+ {users.map((u: any) => (
228
+ <option key={u.id} value={u.id}>{u.name || u.email}</option>
229
+ ))}
230
+ </select>
231
+ </div>
232
+ )}
233
+
234
+ {/* Custom Fields */}
235
+ {fieldDefs.length > 0 && (
236
+ <>
237
+ {sectionLabel('Custom Fields')}
238
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
239
+ {fieldDefs.map((def: any) => (
240
+ <div key={def.field_key} className="space-y-1.5">
241
+ <label className={labelClass}>{def.label}{def.required && <span className="text-destructive ml-0.5">*</span>}</label>
242
+ {(def.field_type === 'text' || !def.field_type) && (
243
+ <input type="text" value={customFieldValues[def.field_key] ?? ''} onChange={e => setCF(def.field_key, e.target.value)} className={inputClass} />
244
+ )}
245
+ {def.field_type === 'number' && (
246
+ <input type="number" value={customFieldValues[def.field_key] ?? ''} onChange={e => setCF(def.field_key, e.target.value)} className={inputClass} />
247
+ )}
248
+ {def.field_type === 'date' && (
249
+ <DatePicker value={customFieldValues[def.field_key] ?? ''} onChange={val => setCF(def.field_key, val)} required={def.required} />
250
+ )}
251
+ {def.field_type === 'boolean' && (
252
+ <div className="flex items-center gap-2 h-10">
253
+ <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" />
254
+ <span className="text-sm text-foreground">Yes</span>
255
+ </div>
256
+ )}
257
+ {(def.field_type === 'select' || def.field_type === 'multi_select') && (
258
+ <select value={customFieldValues[def.field_key] ?? ''} onChange={e => setCF(def.field_key, e.target.value)} className={`${inputClass} pr-3`}>
259
+ <option value="">Select…</option>
260
+ {(def.options ?? []).map((opt: string) => <option key={opt} value={opt}>{opt}</option>)}
261
+ </select>
262
+ )}
263
+ </div>
264
+ ))}
265
+ </>
266
+ )}
267
+
268
+ {useCase.created_at && (
269
+ <div className="flex items-center justify-between py-2 border-t border-border mt-2">
270
+ <span className="text-xs text-muted-foreground">Created</span>
271
+ <span className="text-xs text-muted-foreground">{new Date(useCase.created_at as string).toLocaleDateString()}</span>
272
+ </div>
273
+ )}
274
+ <button
275
+ onClick={handleSave}
276
+ disabled={!fields.name.trim() || isSaving}
277
+ 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"
278
+ >
279
+ {isSaving ? 'Saving…' : 'Save Changes'}
280
+ </button>
281
+ {!confirmDelete ? (
282
+ <button
283
+ onClick={() => setConfirmDelete(true)}
284
+ 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"
285
+ >
286
+ <Trash2 className="w-3.5 h-3.5" /> Delete Use Case
287
+ </button>
288
+ ) : (
289
+ <div className="flex gap-2">
290
+ <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">
291
+ Cancel
292
+ </button>
293
+ <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">
294
+ Confirm Delete
295
+ </button>
296
+ </div>
297
+ )}
298
+ </div>
299
+ </div>
300
+ );
301
+ }
302
+
303
+ export function UseCaseDrawer() {
304
+ const { drawerEntityId, openAIWithContext, closeDrawer } = useAppStore();
305
+ const { enabled: agentEnabled } = useAgentSettings();
306
+ const navigate = useNavigate();
307
+ const [editing, setEditing] = useState(false);
308
+ const [briefing, setBriefing] = useState(false);
309
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
310
+ const { data: useCaseData, isLoading } = useUseCase(drawerEntityId ?? '') as any;
311
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
312
+ const { data: timelineData } = useUseCaseTimeline(drawerEntityId ?? '') as any;
313
+ const updateUseCase = useUpdateUseCase(drawerEntityId ?? '');
314
+ const deleteUseCase = useDeleteUseCase(drawerEntityId ?? '');
315
+
316
+ if (isLoading) {
317
+ return (
318
+ <div className="flex flex-col gap-4 p-6 animate-pulse">
319
+ <div className="h-6 bg-muted rounded w-3/4" />
320
+ <div className="h-4 bg-muted rounded w-1/2" />
321
+ </div>
322
+ );
323
+ }
324
+
325
+ if (!useCaseData?.use_case) {
326
+ return <div className="p-4 text-muted-foreground">Use case not found</div>;
327
+ }
328
+
329
+ const useCase = useCaseData.use_case;
330
+ const name: string = useCase.name ?? '';
331
+ const stage: string = useCase.stage ?? '';
332
+ const arr: number = useCase.attributed_arr ?? 0;
333
+ const healthScore: number = useCase.health_score ?? 0;
334
+
335
+ const timeline: Array<Record<string, unknown>> = timelineData?.data ?? timelineData ?? [];
336
+
337
+ if (briefing) {
338
+ return <BriefingPanel subjectType="use_case" subjectId={drawerEntityId!} onClose={() => setBriefing(false)} />;
339
+ }
340
+
341
+ if (editing) {
342
+ return (
343
+ <UseCaseEditForm
344
+ useCase={useCase}
345
+ onSave={async (data) => {
346
+ try {
347
+ await updateUseCase.mutateAsync(data);
348
+ setEditing(false);
349
+ toast({ title: 'Use case updated' });
350
+ } catch (err) {
351
+ toast({ title: 'Failed to update use case', description: err instanceof Error ? err.message : 'Please try again.', variant: 'destructive' });
352
+ }
353
+ }}
354
+ onCancel={() => setEditing(false)}
355
+ onDelete={async () => {
356
+ try {
357
+ await deleteUseCase.mutateAsync();
358
+ closeDrawer();
359
+ toast({ title: 'Use case deleted' });
360
+ } catch (err) {
361
+ toast({ title: 'Failed to delete use case', description: err instanceof Error ? err.message : 'Please try again.', variant: 'destructive' });
362
+ }
363
+ }}
364
+ isSaving={updateUseCase.isPending}
365
+ />
366
+ );
367
+ }
368
+
369
+ return (
370
+ <div className="flex flex-col">
371
+ {/* Header */}
372
+ <div className="p-6 border-b border-border">
373
+ <h2 className="font-display font-extrabold text-xl text-foreground">{name}</h2>
374
+ <div className="flex items-center gap-2 mt-3">
375
+ {stage && <UseCaseStageBadge stage={stage} />}
376
+ </div>
377
+ <div className="flex gap-2 mt-4">
378
+ <button
379
+ onClick={() => setEditing(true)}
380
+ 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"
381
+ >
382
+ <Pencil className="w-3.5 h-3.5" /> Edit
383
+ </button>
384
+ <button
385
+ onClick={() => setBriefing(true)}
386
+ 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"
387
+ >
388
+ <FileText className="w-3.5 h-3.5" /> Brief
389
+ </button>
390
+ {agentEnabled && (
391
+ <button
392
+ onClick={() => {
393
+ openAIWithContext({ type: 'use-case', id: useCase.id, name, detail: stage });
394
+ closeDrawer();
395
+ navigate('/agent');
396
+ }}
397
+ 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"
398
+ >
399
+ <Sparkles className="w-3.5 h-3.5" /> Chat
400
+ </button>
401
+ )}
402
+ </div>
403
+ </div>
404
+
405
+ {/* Stats */}
406
+ <div className="grid grid-cols-3 gap-3 p-4 mx-4 mt-4">
407
+ {[
408
+ { icon: DollarSign, label: 'Attributed ARR', value: arr ? `$${(arr / 1000).toFixed(0)}K` : '—' },
409
+ { icon: Calendar, label: 'Stage', value: (useCaseStageConfig[stage]?.label ?? stage) || '—' },
410
+ { icon: Bot, label: 'Health', value: healthScore ? String(healthScore) : '—' },
411
+ ].map((stat) => (
412
+ <div key={stat.label} className="bg-muted/50 rounded-xl p-3 text-center">
413
+ <stat.icon className="w-4 h-4 text-muted-foreground mx-auto mb-1" />
414
+ <p className="text-sm font-display font-bold text-foreground truncate">{stat.value}</p>
415
+ <p className="text-[10px] text-muted-foreground">{stat.label}</p>
416
+ </div>
417
+ ))}
418
+ </div>
419
+
420
+ {/* Details */}
421
+ <div className="p-4 mx-4 mt-2 space-y-3">
422
+ <h3 className="text-xs font-display font-bold text-muted-foreground uppercase tracking-wide">Details</h3>
423
+ {[
424
+ { label: 'Stage', value: useCaseStageConfig[stage]?.label ?? stage },
425
+ { label: 'Health Score', value: healthScore ? String(healthScore) : undefined },
426
+ { label: 'Prod Date', value: useCase.target_prod_date ? new Date(useCase.target_prod_date as string).toLocaleDateString() : undefined },
427
+ { label: 'Created', value: useCase.created_at ? new Date(useCase.created_at as string).toLocaleDateString() : undefined },
428
+ ]
429
+ .filter((f) => f.value)
430
+ .map((field) => (
431
+ <div key={field.label} className="flex items-center justify-between">
432
+ <span className="text-xs text-muted-foreground">{field.label}</span>
433
+ <span className="text-sm text-foreground">{field.value}</span>
434
+ </div>
435
+ ))}
436
+ </div>
437
+
438
+ {/* Custom Fields */}
439
+ <CustomFieldsSection objectType="use_case" values={(useCase.custom_fields ?? {}) as Record<string, unknown>} />
440
+
441
+ {/* Context */}
442
+ <ContextPanel subjectType="use_case" subjectId={drawerEntityId!} />
443
+
444
+ {/* Timeline */}
445
+ {timeline.length > 0 && (
446
+ <div className="p-4 mx-4 mt-2 mb-6">
447
+ <h3 className="text-xs font-display font-bold text-muted-foreground uppercase tracking-wide mb-3">Timeline</h3>
448
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
449
+ <ActivityTimeline activities={timeline as any[]} />
450
+ </div>
451
+ )}
452
+ </div>
453
+ );
454
+ }
@@ -0,0 +1,49 @@
1
+ import { Link, useLocation } from 'react-router-dom';
2
+ import { LayoutDashboard, Users, Building2, Briefcase, Activity, Settings } from 'lucide-react';
3
+ import { motion } from 'framer-motion';
4
+
5
+ const tabs = [
6
+ { icon: LayoutDashboard, label: 'Home', path: '/' },
7
+ { icon: Users, label: 'Contacts', path: '/contacts' },
8
+ { icon: Building2, label: 'Accounts', path: '/accounts' },
9
+ { icon: Briefcase, label: 'Opps', path: '/opportunities' },
10
+ { icon: Activity, label: 'Activity', path: '/activities' },
11
+ { icon: Settings, label: 'Settings', path: '/settings' },
12
+ ];
13
+
14
+ export function MobileNav() {
15
+ const location = useLocation();
16
+
17
+ return (
18
+ <nav className="md:hidden fixed bottom-0 left-0 right-0 z-40 flex items-center justify-around h-16 bg-card/95 backdrop-blur-md border-t border-border safe-area-bottom">
19
+ {tabs.map((tab) => {
20
+ const active = tab.path === '/' ? location.pathname === '/' : location.pathname.startsWith(tab.path);
21
+ return (
22
+ <Link
23
+ key={tab.path}
24
+ to={tab.path}
25
+ className="relative flex flex-col items-center justify-center py-1 min-w-[56px] min-h-[44px]"
26
+ >
27
+ {active && (
28
+ <motion.div
29
+ layoutId="mobile-nav-pill"
30
+ className="absolute inset-x-1 -top-0.5 bottom-1 rounded-2xl bg-primary/10"
31
+ transition={{ type: 'spring', damping: 25, stiffness: 300 }}
32
+ />
33
+ )}
34
+ <tab.icon className={`w-6 h-6 relative z-10 transition-colors ${active ? 'text-primary' : 'text-muted-foreground'}`} />
35
+ {active && (
36
+ <motion.span
37
+ initial={{ opacity: 0, y: 4 }}
38
+ animate={{ opacity: 1, y: 0 }}
39
+ className="text-[10px] font-display font-semibold text-primary relative z-10 mt-0.5"
40
+ >
41
+ {tab.label}
42
+ </motion.span>
43
+ )}
44
+ </Link>
45
+ );
46
+ })}
47
+ </nav>
48
+ );
49
+ }