@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,386 +0,0 @@
1
- // Copyright 2026 CRMy Contributors
2
- // SPDX-License-Identifier: Apache-2.0
3
-
4
- import { useState, useMemo, useEffect } from 'react';
5
- import { useNavigate } from 'react-router-dom';
6
- import { TopBar } from '@/components/layout/TopBar';
7
- import { useOpportunities } from '@/api/hooks';
8
- import { useAppStore } from '@/store/appStore';
9
- import { useAgentSettings } from '@/contexts/AgentSettingsContext';
10
- import { StageBadge } from '@/components/crm/CrmWidgets';
11
- import { ListToolbar, type FilterConfig, type SortOption } from '@/components/crm/ListToolbar';
12
- import { DatePicker } from '@/components/ui/date-picker';
13
- import { motion } from 'framer-motion';
14
- import { Columns3, List, BarChart3, Plus, Sparkles, ChevronUp, ChevronDown } from 'lucide-react';
15
- import { PaginationBar } from '@/components/crm/PaginationBar';
16
- import { ContactAvatar } from '@/components/crm/ContactAvatar';
17
- import { stageConfig } from '@/lib/stageConfig';
18
-
19
- type ViewMode = 'kanban' | 'table' | 'forecast';
20
- const kanbanStages = ['prospecting', 'qualification', 'proposal', 'negotiation', 'closed_won', 'closed_lost'];
21
-
22
- type CloseDatePreset = 'all' | 'today' | 'this_week' | 'this_month' | 'this_quarter' | 'custom';
23
-
24
- const CLOSE_DATE_OPTIONS: { value: CloseDatePreset; label: string }[] = [
25
- { value: 'all', label: 'All' },
26
- { value: 'today', label: 'Today' },
27
- { value: 'this_week', label: 'This Week' },
28
- { value: 'this_month', label: 'This Month' },
29
- { value: 'this_quarter', label: 'This Quarter' },
30
- { value: 'custom', label: 'Custom' },
31
- ];
32
-
33
- function getCloseDateRange(preset: CloseDatePreset): { start: Date; end: Date } | null {
34
- if (preset === 'all') return null;
35
- const now = new Date();
36
- if (preset === 'today') {
37
- const start = new Date(now); start.setHours(0, 0, 0, 0);
38
- const end = new Date(now); end.setHours(23, 59, 59, 999);
39
- return { start, end };
40
- }
41
- if (preset === 'this_week') {
42
- const start = new Date(now);
43
- const day = start.getDay();
44
- start.setDate(start.getDate() + (day === 0 ? -6 : 1 - day));
45
- start.setHours(0, 0, 0, 0);
46
- const end = new Date(start); end.setDate(start.getDate() + 6); end.setHours(23, 59, 59, 999);
47
- return { start, end };
48
- }
49
- if (preset === 'this_month') {
50
- const start = new Date(now.getFullYear(), now.getMonth(), 1);
51
- const end = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
52
- return { start, end };
53
- }
54
- if (preset === 'this_quarter') {
55
- const q = Math.floor(now.getMonth() / 3);
56
- const start = new Date(now.getFullYear(), q * 3, 1);
57
- const end = new Date(now.getFullYear(), q * 3 + 3, 0, 23, 59, 59, 999);
58
- return { start, end };
59
- }
60
- return null; // custom — handled separately
61
- }
62
-
63
- const filterConfigs: FilterConfig[] = [
64
- { key: 'stage', label: 'Stage', options: kanbanStages.map(k => ({ value: k, label: stageConfig[k]?.label ?? k })) },
65
- ];
66
- const sortOptions: SortOption[] = [
67
- { key: 'name', label: 'Opportunity Name' }, { key: 'amount', label: 'Amount' },
68
- { key: 'stage', label: 'Stage' }, { key: 'probability', label: 'Probability' },
69
- { key: 'created_at', label: 'Created' },
70
- ];
71
-
72
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
- type Opportunity = any;
74
-
75
- export default function Opportunities() {
76
- const navigate = useNavigate();
77
- const [view, setView] = useState<ViewMode>('table');
78
- const { openDrawer, openQuickAdd, openAIWithContext } = useAppStore();
79
- const { enabled: agentEnabled } = useAgentSettings();
80
- const [search, setSearch] = useState('');
81
- const [activeFilters, setActiveFilters] = useState<Record<string, string[]>>({});
82
- const [sort, setSort] = useState<{ key: string; dir: 'asc' | 'desc' } | null>(null);
83
- const [closeDate, setCloseDate] = useState<CloseDatePreset>('this_quarter');
84
- const [page, setPage] = useState(1);
85
- const [pageSize, setPageSize] = useState(25);
86
- const [customFrom, setCustomFrom] = useState('');
87
- const [customTo, setCustomTo] = useState('');
88
-
89
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
90
- const { data, isLoading } = useOpportunities({ q: search || undefined, limit: 200 }) as any;
91
- const allOpportunities: Opportunity[] = data?.data ?? [];
92
-
93
- const handleFilterChange = (key: string, values: string[]) => {
94
- setActiveFilters(prev => { const next = { ...prev }; if (values.length === 0) delete next[key]; else next[key] = values; return next; });
95
- };
96
- const handleSortChange = (key: string) => {
97
- setSort(prev => prev?.key === key ? { key, dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: 'asc' });
98
- };
99
-
100
- const filtered = useMemo(() => {
101
- let result = [...allOpportunities];
102
- if (activeFilters.stage?.length) result = result.filter(d => activeFilters.stage.includes(d.stage as string));
103
-
104
- // Close date filtering
105
- if (closeDate !== 'all') {
106
- let start: Date, end: Date;
107
- if (closeDate === 'custom') {
108
- start = customFrom ? new Date(customFrom + 'T00:00:00') : new Date(0);
109
- end = customTo ? new Date(customTo + 'T23:59:59') : new Date(8640000000000000);
110
- } else {
111
- const range = getCloseDateRange(closeDate);
112
- if (range) { start = range.start; end = range.end; }
113
- else { start = new Date(0); end = new Date(8640000000000000); }
114
- }
115
- result = result.filter(d => {
116
- if (!d.close_date) return false;
117
- const cd = new Date(d.close_date);
118
- return cd >= start && cd <= end;
119
- });
120
- }
121
-
122
- if (sort) {
123
- result.sort((a, b) => {
124
- const aVal = (a[sort.key] ?? '') as string | number;
125
- const bVal = (b[sort.key] ?? '') as string | number;
126
- if (typeof aVal === 'number' && typeof bVal === 'number') return sort.dir === 'asc' ? aVal - bVal : bVal - aVal;
127
- return sort.dir === 'asc' ? String(aVal).localeCompare(String(bVal)) : String(bVal).localeCompare(String(aVal));
128
- });
129
- }
130
- return result;
131
- }, [allOpportunities, activeFilters, sort]);
132
-
133
- useEffect(() => { setPage(1); }, [search, activeFilters, sort, closeDate, customFrom, customTo]);
134
- const paginated = filtered.slice((page - 1) * pageSize, page * pageSize);
135
-
136
- const SortHeader = ({ label, sortKey }: { label: string; sortKey: string }) => (
137
- <th onClick={() => handleSortChange(sortKey)} className="text-left px-4 py-3 text-xs font-display font-semibold text-muted-foreground cursor-pointer hover:text-foreground transition-colors select-none">
138
- <span className="inline-flex items-center gap-1">
139
- {label}
140
- {sort?.key === sortKey && (sort.dir === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
141
- </span>
142
- </th>
143
- );
144
-
145
- return (
146
- <div className="flex flex-col h-full">
147
- <TopBar title="Opportunities">
148
- <div className="hidden md:flex items-center gap-1 bg-muted rounded-xl p-0.5">
149
- {[
150
- { mode: 'kanban', icon: Columns3 },
151
- { mode: 'table', icon: List },
152
- { mode: 'forecast', icon: BarChart3 },
153
- ].map(({ mode, icon: Icon }) => (
154
- <button key={mode} onClick={() => setView(mode as ViewMode)}
155
- className={`p-1.5 rounded-lg text-sm transition-all ${view === mode ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground'}`}>
156
- <Icon className="w-4 h-4" />
157
- </button>
158
- ))}
159
- </div>
160
- </TopBar>
161
-
162
- {/* Close date selector */}
163
- <div className="px-4 md:px-6 pt-3 pb-1 flex flex-wrap items-center gap-2">
164
- <span className="text-xs text-muted-foreground font-semibold">Close Date:</span>
165
- <div className="inline-flex rounded-xl border border-border bg-muted/40 p-0.5 gap-0.5">
166
- {CLOSE_DATE_OPTIONS.map(opt => (
167
- <button key={opt.value} onClick={() => setCloseDate(opt.value)}
168
- className={['px-3 py-1.5 text-xs font-medium rounded-lg transition-all',
169
- closeDate === opt.value ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground',
170
- ].join(' ')}>
171
- {opt.label}
172
- </button>
173
- ))}
174
- </div>
175
- {closeDate === 'custom' && (
176
- <div className="flex items-center gap-2">
177
- <DatePicker
178
- value={customFrom}
179
- onChange={setCustomFrom}
180
- size="sm"
181
- placeholder="From"
182
- className="w-36"
183
- />
184
- <span className="text-xs text-muted-foreground">to</span>
185
- <DatePicker
186
- value={customTo}
187
- onChange={setCustomTo}
188
- size="sm"
189
- placeholder="To"
190
- className="w-36"
191
- />
192
- </div>
193
- )}
194
- </div>
195
-
196
- <ListToolbar searchValue={search} onSearchChange={setSearch} searchPlaceholder="Search opportunities..."
197
- filters={filterConfigs} activeFilters={activeFilters} onFilterChange={handleFilterChange}
198
- onClearFilters={() => setActiveFilters({})} sortOptions={sortOptions} currentSort={sort}
199
- onSortChange={handleSortChange} onAdd={() => openQuickAdd('opportunity')} addLabel="New Opportunity" entityType="opportunities" />
200
-
201
- <div className="flex-1 overflow-y-auto pb-24 md:pb-6">
202
- {isLoading ? (
203
- <div className="flex gap-4 px-4 md:px-6 pt-2">
204
- {[...Array(4)].map((_, i) => <div key={i} className="flex-shrink-0 w-72 h-64 bg-muted/50 rounded-2xl animate-pulse" />)}
205
- </div>
206
- ) : view === 'kanban' ? (
207
- <>
208
- <div className="border-t border-border mb-4" />
209
- <div className="flex gap-3 md:gap-4 px-4 md:px-6 pb-4 overflow-x-auto min-h-full snap-x snap-mandatory md:snap-none no-scrollbar">
210
- {kanbanStages.map((stage) => {
211
- const config = stageConfig[stage] ?? { label: stage, color: '#94a3b8' };
212
- const stageOpps = filtered.filter((d) => d.stage === stage);
213
- const total = stageOpps.reduce((sum, d) => sum + ((d.amount as number) || 0), 0);
214
- return (
215
- <div key={stage} className="flex-shrink-0 w-[280px] md:w-72 flex flex-col snap-center">
216
- <div className="flex items-center justify-between mb-3 px-1">
217
- <div className="flex items-center gap-2">
218
- <span className="px-2.5 py-1 rounded-lg text-xs font-semibold" style={{ backgroundColor: config.color + '20', color: config.color }}>
219
- {config.label}
220
- </span>
221
- <span className="text-xs text-muted-foreground font-mono">{stageOpps.length}</span>
222
- </div>
223
- <span className="text-xs text-muted-foreground font-mono">${(total / 1000).toFixed(0)}K</span>
224
- </div>
225
- <div className="flex-1 space-y-2">
226
- {stageOpps.map((opp, i) => {
227
- const contactName = (opp.contact_name ?? opp.contactName ?? '') as string;
228
- const amount = (opp.amount as number) ?? 0;
229
- const daysInStage = (opp.days_in_stage ?? opp.daysInStage ?? 0) as number;
230
- return (
231
- <motion.div key={opp.id as string} initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: i * 0.03 }}
232
- onClick={() => openDrawer('opportunity', opp.id as string)}
233
- className="bg-card border border-border rounded-2xl p-3.5 cursor-pointer hover:border-primary/30 hover:shadow-md transition-all press-scale group">
234
- <div className="flex items-start justify-between">
235
- <p className="text-sm font-display font-bold text-foreground">{opp.name as string}</p>
236
- {agentEnabled && (
237
- <button onClick={(e) => { e.stopPropagation(); openAIWithContext({ type: 'opportunity', id: opp.id as string, name: opp.name as string, detail: `$${(amount / 1000).toFixed(0)}K` }); navigate('/agent'); }}
238
- className="p-0.5 rounded-lg md:opacity-0 md:group-hover:opacity-100 hover:bg-accent/10 transition-all">
239
- <Sparkles className="w-3.5 h-3.5 text-accent" />
240
- </button>
241
- )}
242
- </div>
243
- {contactName && (
244
- <div className="flex items-center gap-2 mt-2">
245
- <ContactAvatar name={contactName} className="w-5 h-5 rounded-full text-[8px]" />
246
- <span className="text-xs text-muted-foreground">{contactName}</span>
247
- </div>
248
- )}
249
- <div className="flex items-center justify-between mt-2.5">
250
- <span className="text-sm font-display font-extrabold text-foreground">
251
- ${amount >= 1000 ? `${(amount / 1000).toFixed(0)}K` : amount}
252
- </span>
253
- {daysInStage > 0 && (
254
- daysInStage > 14 ? (
255
- <span className="px-2 py-0.5 rounded-lg text-xs bg-destructive/15 text-destructive font-semibold">{daysInStage}d</span>
256
- ) : (
257
- <span className="text-xs text-muted-foreground">{daysInStage}d</span>
258
- )
259
- )}
260
- </div>
261
- </motion.div>
262
- );
263
- })}
264
- <button onClick={() => openQuickAdd('opportunity')}
265
- className="w-full flex items-center justify-center gap-1 py-2.5 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/50 rounded-xl transition-colors press-scale">
266
- <Plus className="w-3.5 h-3.5" /> Add Opportunity
267
- </button>
268
- </div>
269
- </div>
270
- );
271
- })}
272
- </div>
273
- </>
274
- ) : view === 'table' ? (
275
- <div className="px-4 md:px-6">
276
- {filtered.length === 0 ? (
277
- <div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
278
- <p className="text-sm">No opportunities match your filters.</p>
279
- <button onClick={() => { setSearch(''); setActiveFilters({}); setCloseDate('all'); }} className="mt-2 text-xs text-primary font-semibold hover:underline">Clear all filters</button>
280
- </div>
281
- ) : (
282
- <div className="bg-card border border-border rounded-2xl overflow-hidden shadow-sm">
283
- <div className="overflow-x-auto">
284
- <table className="w-full text-sm">
285
- <thead>
286
- <tr className="border-b border-border bg-surface-sunken/50">
287
- <SortHeader label="Opportunity" sortKey="name" />
288
- <th className="text-left px-4 py-3 text-xs font-display font-semibold text-muted-foreground">Contact</th>
289
- <SortHeader label="Amount" sortKey="amount" />
290
- <SortHeader label="Stage" sortKey="stage" />
291
- <SortHeader label="Probability" sortKey="probability" />
292
- <SortHeader label="Created" sortKey="created_at" />
293
- {agentEnabled && <th className="px-2 py-3 w-8"></th>}
294
- </tr>
295
- </thead>
296
- <tbody>
297
- {paginated.map((d, i) => {
298
- const contactName = (d.contact_name ?? d.contactName ?? '') as string;
299
- const amount = (d.amount as number) ?? 0;
300
- return (
301
- <tr key={d.id as string} onClick={() => openDrawer('opportunity', d.id as string)}
302
- className={`border-b border-border last:border-0 hover:bg-primary/5 cursor-pointer transition-colors group ${i % 2 === 1 ? 'bg-surface-sunken/30' : ''}`}>
303
- <td className="px-4 py-3 font-display font-bold text-foreground">{d.name as string}</td>
304
- <td className="px-4 py-3">
305
- {contactName && (
306
- <div className="flex items-center gap-2">
307
- <ContactAvatar name={contactName} className="w-5 h-5 rounded-full text-[8px]" />
308
- <span className="text-muted-foreground">{contactName}</span>
309
- </div>
310
- )}
311
- </td>
312
- <td className="px-4 py-3 font-display font-bold text-foreground">
313
- ${amount >= 1000 ? `${(amount / 1000).toFixed(0)}K` : amount}
314
- </td>
315
- <td className="px-4 py-3">{d.stage && <StageBadge stage={d.stage as string} />}</td>
316
- <td className="px-4 py-3 text-muted-foreground">{d.probability ? `${d.probability}%` : '—'}</td>
317
- <td className="px-4 py-3 text-muted-foreground text-xs">
318
- {d.created_at ? new Date(d.created_at as string).toLocaleDateString() : '—'}
319
- </td>
320
- {agentEnabled && (
321
- <td className="px-2 py-3">
322
- <button onClick={(e) => { e.stopPropagation(); openAIWithContext({ type: 'opportunity', id: d.id as string, name: d.name as string, detail: `$${(amount / 1000).toFixed(0)}K` }); navigate('/agent'); }}
323
- className="p-1.5 rounded-lg opacity-0 group-hover:opacity-100 hover:bg-accent/10 transition-all">
324
- <Sparkles className="w-3.5 h-3.5 text-accent" />
325
- </button>
326
- </td>
327
- )}
328
- </tr>
329
- );
330
- })}
331
- </tbody>
332
- </table>
333
- </div>
334
- <div className="px-4">
335
- <PaginationBar page={page} pageSize={pageSize} total={filtered.length} onPageChange={setPage} onPageSizeChange={setPageSize} />
336
- </div>
337
- </div>
338
- )}
339
- </div>
340
- ) : (
341
- <div className="px-4 md:px-6">
342
- <div className="grid grid-cols-1 md:grid-cols-3 gap-3 mb-6">
343
- {[
344
- { label: 'Weighted Pipeline', value: filtered.filter(d => d.stage !== 'closed_won' && d.stage !== 'closed_lost').reduce((s, d) => s + ((d.amount as number) || 0) * ((d.probability as number) || 0) / 100, 0) },
345
- { label: 'Best Case', value: filtered.filter(d => d.stage !== 'closed_lost').reduce((s, d) => s + ((d.amount as number) || 0), 0) },
346
- { label: 'Closed Won', value: filtered.filter(d => d.stage === 'closed_won').reduce((s, d) => s + ((d.amount as number) || 0), 0) },
347
- ].map((card) => (
348
- <div key={card.label} className="bg-card border border-border rounded-2xl p-5 shadow-sm">
349
- <p className="text-xs text-muted-foreground font-display font-semibold">{card.label}</p>
350
- <p className="text-2xl font-display font-extrabold text-foreground mt-1">
351
- ${card.value >= 1_000_000 ? `${(card.value / 1_000_000).toFixed(2)}M` : `${(card.value / 1000).toFixed(0)}K`}
352
- </p>
353
- </div>
354
- ))}
355
- </div>
356
- <div className="bg-card border border-border rounded-2xl p-5 shadow-sm">
357
- <h3 className="font-display font-bold text-foreground mb-4">Pipeline by stage</h3>
358
- <div className="space-y-4">
359
- {kanbanStages.filter(s => s !== 'closed_lost').map((stage) => {
360
- const config = stageConfig[stage] ?? { label: stage, color: '#94a3b8' };
361
- const total = filtered.filter(d => d.stage === stage).reduce((s, d) => s + ((d.amount as number) || 0), 0);
362
- const max = Math.max(...kanbanStages.map(s => filtered.filter(d => d.stage === s).reduce((sum, d) => sum + ((d.amount as number) || 0), 0)), 1);
363
- return (
364
- <div key={stage} className="flex items-center gap-3">
365
- <span className="text-xs w-28 truncate font-semibold" style={{ color: config.color }}>{config.label}</span>
366
- <div className="flex-1 h-8 bg-muted rounded-xl overflow-hidden">
367
- <motion.div initial={{ width: 0 }} animate={{ width: `${(total / max) * 100}%` }}
368
- transition={{ duration: 0.8, ease: 'easeOut' }}
369
- className="h-full rounded-xl flex items-center px-3"
370
- style={{ backgroundColor: config.color + '30' }}>
371
- <span className="text-xs font-display font-bold" style={{ color: config.color }}>
372
- ${(total / 1000).toFixed(0)}K
373
- </span>
374
- </motion.div>
375
- </div>
376
- </div>
377
- );
378
- })}
379
- </div>
380
- </div>
381
- </div>
382
- )}
383
- </div>
384
- </div>
385
- );
386
- }
@@ -1,49 +0,0 @@
1
- // Copyright 2026 CRMy Contributors
2
- // SPDX-License-Identifier: Apache-2.0
3
-
4
- import { useSearchParams, Link } from 'react-router-dom';
5
- import { Card, CardContent } from '../components/ui/card';
6
- import { Badge } from '../components/ui/badge';
7
- import { useSearch } from '../api/hooks';
8
-
9
- export function SearchResultsPage() {
10
- const [searchParams] = useSearchParams();
11
- const q = searchParams.get('q') ?? '';
12
- const { data, isLoading } = useSearch(q);
13
-
14
- const results = (data as any)?.data ?? (data as any)?.results ?? [];
15
-
16
- return (
17
- <div className="space-y-4">
18
- <h1 className="font-display text-2xl font-bold">Search: "{q}"</h1>
19
- {isLoading ? (
20
- <p className="text-muted-foreground">Searching...</p>
21
- ) : results.length === 0 ? (
22
- <p className="text-muted-foreground">No results found</p>
23
- ) : (
24
- <div className="space-y-2">
25
- {results.map((r: any) => {
26
- const type = r.object_type ?? r.type ?? 'unknown';
27
- const href =
28
- type === 'contact' ? `/app/contacts/${r.id}`
29
- : type === 'account' ? `/app/accounts/${r.id}`
30
- : type === 'opportunity' ? `/app/opportunities/${r.id}`
31
- : type === 'use_case' ? `/app/use-cases/${r.id}`
32
- : '#';
33
- return (
34
- <Link key={`${type}-${r.id}`} to={href}>
35
- <Card className="hover:bg-accent transition-colors cursor-pointer">
36
- <CardContent className="flex items-center gap-3 p-4">
37
- <Badge variant="outline">{type}</Badge>
38
- <span className="font-medium">{r.name ?? r.first_name ? `${r.first_name ?? ''} ${r.last_name ?? ''}` : r.id}</span>
39
- {r.email && <span className="text-sm text-muted-foreground">{r.email}</span>}
40
- </CardContent>
41
- </Card>
42
- </Link>
43
- );
44
- })}
45
- </div>
46
- )}
47
- </div>
48
- );
49
- }