@crmy/web 0.5.1 → 0.5.6

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,417 +0,0 @@
1
- // Copyright 2026 CRMy Contributors
2
- // SPDX-License-Identifier: Apache-2.0
3
-
4
- import { useState } from 'react';
5
- import { Phone, Mail, StickyNote, CheckSquare, MessageSquare, ChevronDown, ChevronUp, Presentation, FileText, Search, ArrowRightLeft, RefreshCw, Activity, Heart, Bot, User } from 'lucide-react';
6
- import { ContactAvatar } from './ContactAvatar';
7
- import { useOpportunities, useUseCases, useAccounts, useActivities, useCustomFields } from '@/api/hooks';
8
- import { useAppStore } from '@/store/appStore';
9
- import { stageConfig, useCaseStageConfig } from '@/lib/stageConfig';
10
-
11
- export function StageBadge({ stage }: { stage: string }) {
12
- const config = stageConfig[stage] ?? { label: stage, color: '#94a3b8' };
13
- return (
14
- <span
15
- className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold"
16
- style={{ backgroundColor: config.color + '18', color: config.color }}
17
- >
18
- {config.label}
19
- </span>
20
- );
21
- }
22
-
23
- export function LeadScoreBadge({ score }: { score: number }) {
24
- const color = score >= 80 ? 'hsl(152, 55%, 42%)' : score >= 50 ? 'hsl(38, 92%, 50%)' : 'hsl(var(--muted-foreground))';
25
- return (
26
- <span
27
- className="inline-flex items-center justify-center w-8 h-6 rounded-lg text-xs font-mono font-bold"
28
- style={{ backgroundColor: color + '18', color }}
29
- >
30
- {score}
31
- </span>
32
- );
33
- }
34
-
35
- export function AgentStatusDot() {
36
- return <div className="w-2.5 h-2.5 rounded-full bg-success animate-pulse-dot" />;
37
- }
38
-
39
- export function activityIcon(type: string) {
40
- switch (type) {
41
- case 'call': return <Phone className="w-3.5 h-3.5" />;
42
- case 'email': return <Mail className="w-3.5 h-3.5" />;
43
- case 'meeting': return <MessageSquare className="w-3.5 h-3.5" />;
44
- case 'task': return <CheckSquare className="w-3.5 h-3.5" />;
45
- case 'demo': return <Presentation className="w-3.5 h-3.5" />;
46
- case 'proposal': return <FileText className="w-3.5 h-3.5" />;
47
- case 'research': return <Search className="w-3.5 h-3.5" />;
48
- case 'handoff': return <ArrowRightLeft className="w-3.5 h-3.5" />;
49
- case 'status_update': return <Activity className="w-3.5 h-3.5" />;
50
- case 'stage_change': return <RefreshCw className="w-3.5 h-3.5" />;
51
- case 'health_update': return <Heart className="w-3.5 h-3.5" />;
52
- default: return <StickyNote className="w-3.5 h-3.5" />;
53
- }
54
- }
55
-
56
- const OUTCOME_COLORS: Record<string, string> = {
57
- connected: 'hsl(152, 55%, 42%)',
58
- positive: 'hsl(152, 55%, 42%)',
59
- voicemail: 'hsl(38, 92%, 50%)',
60
- neutral: 'hsl(38, 92%, 50%)',
61
- follow_up_needed: 'hsl(38, 92%, 50%)',
62
- negative: 'hsl(var(--destructive))',
63
- no_show: 'hsl(var(--destructive))',
64
- };
65
-
66
- function OutcomeBadge({ outcome }: { outcome: string }) {
67
- const color = OUTCOME_COLORS[outcome] ?? 'hsl(var(--muted-foreground))';
68
- const label = outcome.replace(/_/g, ' ');
69
- return (
70
- <span
71
- className="inline-flex items-center px-1.5 py-0.5 rounded-md text-[10px] font-medium capitalize"
72
- style={{ backgroundColor: color + '18', color }}
73
- >
74
- {label}
75
- </span>
76
- );
77
- }
78
-
79
- const SUBJECT_TYPE_LABELS: Record<string, string> = {
80
- contact: 'Contact',
81
- account: 'Account',
82
- opportunity: 'Opp',
83
- use_case: 'Use Case',
84
- };
85
-
86
- type DrawerType = 'contact' | 'opportunity' | 'use-case' | 'account';
87
- const SUBJECT_TYPE_DRAWER: Record<string, DrawerType> = {
88
- contact: 'contact',
89
- account: 'account',
90
- opportunity: 'opportunity',
91
- use_case: 'use-case',
92
- };
93
-
94
- export interface ActivityItem {
95
- id: string;
96
- type: string;
97
- subject?: string;
98
- description?: string;
99
- body?: string;
100
- contact_name?: string;
101
- contactName?: string;
102
- created_at?: string;
103
- timestamp?: string;
104
- // Context Engine fields
105
- subject_type?: string;
106
- subject_id?: string;
107
- occurred_at?: string;
108
- outcome?: string;
109
- performed_by?: string;
110
- performer_name?: string;
111
- contact_id?: string;
112
- account_id?: string;
113
- opportunity_id?: string;
114
- }
115
-
116
- interface ActivityFeedProps {
117
- limit?: number;
118
- activities?: ActivityItem[];
119
- filterWindow?: 'today' | 'week';
120
- }
121
-
122
- export function ActivityFeed({ limit, activities: propActivities, filterWindow }: ActivityFeedProps) {
123
- const { openDrawer } = useAppStore();
124
- const fetchLimit = filterWindow ? 100 : (limit ?? 20);
125
- const { data, isLoading } = useActivities(propActivities ? undefined : { limit: fetchLimit });
126
- let items: ActivityItem[] = (propActivities ?? data?.data ?? []) as ActivityItem[];
127
-
128
- if (filterWindow) {
129
- const cutoff = new Date();
130
- if (filterWindow === 'today') {
131
- cutoff.setHours(0, 0, 0, 0);
132
- } else {
133
- cutoff.setDate(cutoff.getDate() - 7);
134
- cutoff.setHours(0, 0, 0, 0);
135
- }
136
- items = items.filter((a) => {
137
- const ts = a.occurred_at ?? a.created_at ?? a.timestamp;
138
- return ts ? new Date(ts) >= cutoff : false;
139
- });
140
- }
141
-
142
- const displayed = limit ? items.slice(0, limit) : items;
143
-
144
- if (isLoading && !propActivities) {
145
- return (
146
- <div className="space-y-3">
147
- {[...Array(4)].map((_, i) => (
148
- <div key={i} className="flex gap-3 animate-pulse">
149
- <div className="w-7 h-7 rounded-xl bg-muted flex-shrink-0" />
150
- <div className="flex-1 space-y-1">
151
- <div className="h-3 bg-muted rounded w-3/4" />
152
- <div className="h-2.5 bg-muted rounded w-1/2" />
153
- </div>
154
- </div>
155
- ))}
156
- </div>
157
- );
158
- }
159
-
160
- if (displayed.length === 0) {
161
- return <p className="text-sm text-muted-foreground">No activity yet.</p>;
162
- }
163
-
164
- return (
165
- <div className="space-y-1">
166
- {displayed.map((a) => {
167
- const name = a.contact_name ?? a.contactName ?? '';
168
- const desc = a.description ?? a.body ?? a.subject ?? '';
169
- const ts = a.occurred_at ?? a.created_at ?? a.timestamp ?? '';
170
- const contactId = a.contact_id;
171
- const hasSubjectLink = a.subject_type && a.subject_id;
172
- const isClickable = hasSubjectLink || contactId;
173
-
174
- const handleClick = () => {
175
- if (hasSubjectLink) {
176
- const drawerType = SUBJECT_TYPE_DRAWER[a.subject_type!];
177
- if (drawerType) openDrawer(drawerType, a.subject_id!);
178
- } else if (contactId) {
179
- openDrawer('contact', contactId);
180
- }
181
- };
182
-
183
- // Build metadata segments
184
- const meta: string[] = [];
185
- if (a.performer_name) meta.push(a.performer_name);
186
- else if (name) meta.push(name);
187
- if (ts) meta.push(new Date(ts).toLocaleDateString());
188
-
189
- return (
190
- <div
191
- key={a.id}
192
- className={`flex gap-3 py-2 ${isClickable ? 'cursor-pointer hover:bg-muted/40 rounded-xl px-2 -mx-2 transition-colors' : ''}`}
193
- onClick={isClickable ? handleClick : undefined}
194
- >
195
- <div className="w-7 h-7 rounded-xl bg-muted flex items-center justify-center text-muted-foreground flex-shrink-0 mt-0.5">
196
- {activityIcon(a.type)}
197
- </div>
198
- <div className="flex-1 min-w-0">
199
- <div className="flex items-start gap-2">
200
- <p className="text-sm text-foreground flex-1 min-w-0 truncate">
201
- {desc || `${a.type}${name ? ` with ${name}` : ''}`}
202
- </p>
203
- {a.outcome && <OutcomeBadge outcome={a.outcome} />}
204
- </div>
205
- <div className="flex items-center gap-1.5 mt-0.5 flex-wrap">
206
- {hasSubjectLink && (
207
- <span className="inline-flex items-center gap-0.5 text-[10px] font-medium text-primary/80 bg-primary/8 px-1.5 py-0.5 rounded">
208
- {SUBJECT_TYPE_LABELS[a.subject_type!] ?? a.subject_type}
209
- </span>
210
- )}
211
- {a.performer_name && (
212
- <span className="inline-flex items-center gap-0.5 text-[10px] text-muted-foreground">
213
- {a.performed_by ? <Bot className="w-2.5 h-2.5" /> : <User className="w-2.5 h-2.5" />}
214
- {a.performer_name}
215
- </span>
216
- )}
217
- <span className="text-xs text-muted-foreground">
218
- {meta.filter(Boolean).join(' · ')}
219
- </span>
220
- </div>
221
- </div>
222
- </div>
223
- );
224
- })}
225
- </div>
226
- );
227
- }
228
-
229
- export function PipelineSnapshot() {
230
- const [view, setView] = useState<'opportunities' | 'use-cases'>('opportunities');
231
- const { data: oppsData } = useOpportunities({ limit: 200 });
232
- const { data: ucData } = useUseCases({ limit: 200 });
233
-
234
- const opps = (oppsData?.data ?? []) as Record<string, unknown>[];
235
- const ucs = (ucData?.data ?? []) as Record<string, unknown>[];
236
-
237
- const dealStages = ['prospecting', 'qualification', 'proposal', 'negotiation', 'closed_won', 'closed_lost'];
238
- const ucStages = ['discovery', 'poc', 'production', 'scaling', 'sunset'];
239
-
240
- const dealData = dealStages.map((stage) => {
241
- const stageDeals = opps.filter((d: Record<string, unknown>) => d.stage === stage);
242
- const cfg = stageConfig[stage] ?? { label: stage, color: '#94a3b8' };
243
- return {
244
- key: stage,
245
- label: cfg.label,
246
- color: cfg.color,
247
- count: stageDeals.length,
248
- total: stageDeals.reduce((sum: number, d: Record<string, unknown>) => sum + (parseFloat(String(d.amount ?? 0)) || 0), 0),
249
- };
250
- });
251
-
252
- const ucStateData = ucStages.map((stage) => {
253
- const stageUCs = ucs.filter((u: Record<string, unknown>) => u.stage === stage);
254
- const cfg = useCaseStageConfig[stage] ?? { label: stage, color: '#94a3b8' };
255
- return {
256
- key: stage,
257
- label: cfg.label,
258
- color: cfg.color,
259
- count: stageUCs.length,
260
- total: stageUCs.reduce((sum: number, u: Record<string, unknown>) => sum + (parseFloat(String(u.attributed_arr ?? 0)) || 0), 0),
261
- };
262
- });
263
-
264
- const data = view === 'opportunities' ? dealData : ucStateData;
265
- const maxTotal = Math.max(...data.map((s) => s.total), 1);
266
- const valueLabel = (v: number): string => {
267
- if (!isFinite(v) || isNaN(v) || v <= 0) return '$0';
268
- if (v >= 1e12) return `$${(v / 1e12).toFixed(1)}T`;
269
- if (v >= 1e9) return `$${v >= 10e9 ? Math.round(v / 1e9) : (v / 1e9).toFixed(1)}B`;
270
- if (v >= 1e6) return `$${v >= 10e6 ? Math.round(v / 1e6) : (v / 1e6).toFixed(1)}M`;
271
- if (v >= 1e3) return `$${v >= 10e3 ? Math.round(v / 1e3) : (v / 1e3).toFixed(1)}K`;
272
- return `$${Math.round(v)}`;
273
- };
274
-
275
- return (
276
- <div className="bg-card border border-border rounded-2xl p-5 shadow-sm">
277
- <div className="flex items-center justify-between mb-4">
278
- <h3 className="font-display font-bold text-foreground">Pipeline health</h3>
279
- <div className="flex items-center gap-0.5 bg-muted rounded-lg p-0.5">
280
- <button
281
- onClick={() => setView('opportunities')}
282
- className={`px-2.5 py-1 rounded-md text-[11px] font-semibold transition-all ${view === 'opportunities' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
283
- >
284
- Opps
285
- </button>
286
- <button
287
- onClick={() => setView('use-cases')}
288
- className={`px-2.5 py-1 rounded-md text-[11px] font-semibold transition-all ${view === 'use-cases' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
289
- >
290
- Use Cases
291
- </button>
292
- </div>
293
- </div>
294
- <div className="flex gap-3 overflow-x-auto no-scrollbar md:hidden pb-1 -mx-1 px-1">
295
- {data.map((s) => (
296
- <div key={s.key} className="flex-shrink-0 w-28 rounded-xl p-3 border border-border" style={{ backgroundColor: s.color + '08' }}>
297
- <div className="w-3 h-3 rounded-full mb-2" style={{ backgroundColor: s.color }} />
298
- <p className="text-[10px] text-muted-foreground font-medium truncate">{s.label}</p>
299
- <p className="text-lg font-display font-bold text-foreground">{s.count}</p>
300
- <p className="text-[10px] text-muted-foreground font-mono truncate">{valueLabel(s.total)}</p>
301
- </div>
302
- ))}
303
- </div>
304
- <div className="hidden md:block space-y-3">
305
- {data.map((s) => (
306
- <div key={s.key} className="flex items-center gap-2">
307
- <span className="text-xs w-24 flex-shrink-0 truncate font-medium" style={{ color: s.color }}>{s.label}</span>
308
- <div className="flex-1 min-w-0 h-2.5 bg-muted rounded-full overflow-hidden">
309
- <div className="h-full rounded-full transition-all duration-700" style={{ width: `${(s.total / maxTotal) * 100}%`, backgroundColor: s.color }} />
310
- </div>
311
- <span className="text-xs text-muted-foreground font-mono w-6 flex-shrink-0 text-right">{s.count}</span>
312
- <span className="text-xs text-foreground font-mono w-[4.5rem] flex-shrink-0 text-right truncate">{valueLabel(s.total)}</span>
313
- </div>
314
- ))}
315
- </div>
316
- </div>
317
- );
318
- }
319
-
320
- export function AccountHealth() {
321
- const { data } = useAccounts({ limit: 200 });
322
- const accounts = (data?.data ?? []) as Record<string, unknown>[];
323
-
324
- const green = accounts.filter((a: Record<string, unknown>) => ((a.health_score as number) ?? 0) >= 80);
325
- const yellow = accounts.filter((a: Record<string, unknown>) => {
326
- const s = (a.health_score as number) ?? 0;
327
- return s >= 50 && s < 80;
328
- });
329
- const red = accounts.filter((a: Record<string, unknown>) => ((a.health_score as number) ?? 0) < 50);
330
- const total = accounts.length;
331
-
332
- const segments = [
333
- { label: 'Healthy', count: green.length, color: 'hsl(152, 55%, 42%)' },
334
- { label: 'At Risk', count: yellow.length, color: 'hsl(38, 92%, 50%)' },
335
- { label: 'Critical', count: red.length, color: 'hsl(var(--destructive))' },
336
- ];
337
-
338
- return (
339
- <div className="bg-card border border-border rounded-2xl p-5 shadow-sm">
340
- <h3 className="font-display font-bold text-foreground mb-4">Account health</h3>
341
- <div className="flex gap-3 overflow-x-auto no-scrollbar md:hidden pb-1 -mx-1 px-1">
342
- {segments.map((s) => (
343
- <div key={s.label} className="flex-shrink-0 w-28 rounded-xl p-3 border border-border" style={{ backgroundColor: s.color + '08' }}>
344
- <div className="w-3 h-3 rounded-full mb-2" style={{ backgroundColor: s.color }} />
345
- <p className="text-[10px] text-muted-foreground font-medium truncate">{s.label}</p>
346
- <p className="text-lg font-display font-bold text-foreground">{s.count}</p>
347
- <p className="text-[10px] text-muted-foreground font-mono">{total > 0 ? Math.round((s.count / total) * 100) : 0}%</p>
348
- </div>
349
- ))}
350
- </div>
351
- <div className="hidden md:block space-y-3">
352
- {segments.map((s) => (
353
- <div key={s.label} className="flex items-center gap-3">
354
- <span className="text-xs w-28 truncate font-medium" style={{ color: s.color }}>{s.label}</span>
355
- <div className="flex-1 h-2.5 bg-muted rounded-full overflow-hidden">
356
- <div className="h-full rounded-full transition-all duration-700" style={{ width: total > 0 ? `${(s.count / total) * 100}%` : '0%', backgroundColor: s.color }} />
357
- </div>
358
- <span className="text-xs text-foreground font-mono">{s.count}</span>
359
- </div>
360
- ))}
361
- </div>
362
- </div>
363
- );
364
- }
365
-
366
- function formatCustomFieldValue(fieldType: string, val: unknown): string {
367
- if (val === null || val === undefined) return '—';
368
- if (fieldType === 'boolean') return val ? 'Yes' : 'No';
369
- if (fieldType === 'date') { try { return new Date(String(val)).toLocaleDateString(); } catch { return String(val); } }
370
- if (Array.isArray(val)) return val.join(', ');
371
- return String(val);
372
- }
373
-
374
- export function CustomFieldsSection({ objectType, values }: { objectType: string; values: Record<string, unknown> }) {
375
- const [open, setOpen] = useState(false);
376
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
377
- const { data } = useCustomFields(objectType) as any;
378
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
379
- const fieldDefs: any[] = data?.fields ?? [];
380
-
381
- if (fieldDefs.length === 0) return null;
382
-
383
- return (
384
- <div className="px-4 mx-4 mt-2 border-t border-border pt-3">
385
- <button
386
- onClick={() => setOpen(!open)}
387
- className="flex items-center gap-1.5 w-full text-left"
388
- >
389
- <h3 className="text-xs font-display font-bold text-muted-foreground uppercase tracking-wide flex-1">
390
- Custom Fields
391
- <span className="ml-1.5 font-mono font-normal text-muted-foreground/50">({fieldDefs.length})</span>
392
- </h3>
393
- {open ? <ChevronUp className="w-3.5 h-3.5 text-muted-foreground" /> : <ChevronDown className="w-3.5 h-3.5 text-muted-foreground" />}
394
- </button>
395
- {open && (
396
- <div className="mt-3 space-y-2">
397
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
398
- {fieldDefs.map((def: any) => {
399
- const val = values?.[def.field_key];
400
- const hasValue = val !== undefined && val !== null && val !== '';
401
- return (
402
- <div key={def.field_key} className="flex items-center justify-between gap-4">
403
- <span className="text-xs text-muted-foreground shrink-0">
404
- {def.label}
405
- {def.is_required && <span className="text-destructive ml-0.5">*</span>}
406
- </span>
407
- <span className={hasValue ? 'text-sm text-foreground text-right' : 'text-xs text-muted-foreground/50'}>
408
- {hasValue ? formatCustomFieldValue(def.field_type, val) : '—'}
409
- </span>
410
- </div>
411
- );
412
- })}
413
- </div>
414
- )}
415
- </div>
416
- );
417
- }
@@ -1,77 +0,0 @@
1
- import { motion, AnimatePresence } from 'framer-motion';
2
- import { X, GripHorizontal } from 'lucide-react';
3
- import { useAppStore } from '@/store/appStore';
4
-
5
- interface DrawerShellProps {
6
- children: React.ReactNode;
7
- title?: string;
8
- }
9
-
10
- export function DrawerShell({ children, title }: DrawerShellProps) {
11
- const { drawerOpen, closeDrawer } = useAppStore();
12
-
13
- return (
14
- <AnimatePresence>
15
- {drawerOpen && (
16
- <>
17
- {/* Overlay */}
18
- <motion.div
19
- initial={{ opacity: 0 }}
20
- animate={{ opacity: 1 }}
21
- exit={{ opacity: 0 }}
22
- className="fixed inset-0 z-[65] bg-foreground/20 backdrop-blur-sm"
23
- onClick={closeDrawer}
24
- />
25
-
26
- {/* Desktop: slide from right */}
27
- <motion.div
28
- initial={{ x: '100%' }}
29
- animate={{ x: 0 }}
30
- exit={{ x: '100%' }}
31
- transition={{ type: 'spring', damping: 30, stiffness: 300 }}
32
- className="hidden md:flex fixed right-0 top-0 h-full w-[480px] z-[70] bg-card border-l border-border shadow-2xl flex-col rounded-l-2xl"
33
- >
34
- <div className="flex items-center justify-between h-14 px-5 border-b border-border">
35
- {title && <h2 className="font-display font-bold text-foreground">{title}</h2>}
36
- <button
37
- onClick={closeDrawer}
38
- className="ml-auto p-2 rounded-xl hover:bg-muted text-muted-foreground transition-colors"
39
- >
40
- <X className="w-4 h-4" />
41
- </button>
42
- </div>
43
- <div className="flex-1 overflow-y-auto">
44
- {children}
45
- </div>
46
- </motion.div>
47
-
48
- {/* Mobile: slide from bottom */}
49
- <motion.div
50
- initial={{ y: '100%' }}
51
- animate={{ y: 0 }}
52
- exit={{ y: '100%' }}
53
- transition={{ type: 'spring', damping: 30, stiffness: 300 }}
54
- className="md:hidden fixed left-0 right-0 bottom-0 z-[70] bg-card border-t border-border shadow-2xl flex flex-col rounded-t-3xl max-h-[90vh]"
55
- >
56
- {/* Drag handle */}
57
- <div className="flex justify-center py-2">
58
- <div className="w-10 h-1 rounded-full bg-muted-foreground/20" />
59
- </div>
60
- <div className="flex items-center justify-between px-5 pb-3 border-b border-border">
61
- {title && <h2 className="font-display font-bold text-foreground">{title}</h2>}
62
- <button
63
- onClick={closeDrawer}
64
- className="ml-auto p-2 rounded-xl hover:bg-muted text-muted-foreground transition-colors"
65
- >
66
- <X className="w-4 h-4" />
67
- </button>
68
- </div>
69
- <div className="flex-1 overflow-y-auto pb-safe">
70
- {children}
71
- </div>
72
- </motion.div>
73
- </>
74
- )}
75
- </AnimatePresence>
76
- );
77
- }