@ebowwa/crm 0.1.0

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 (187) hide show
  1. package/README.md +174 -0
  2. package/dist/cli/commands/activities.d.ts +11 -0
  3. package/dist/cli/commands/activities.d.ts.map +1 -0
  4. package/dist/cli/commands/activities.js +427 -0
  5. package/dist/cli/commands/activities.js.map +1 -0
  6. package/dist/cli/commands/contacts.d.ts +11 -0
  7. package/dist/cli/commands/contacts.d.ts.map +1 -0
  8. package/dist/cli/commands/contacts.js +458 -0
  9. package/dist/cli/commands/contacts.js.map +1 -0
  10. package/dist/cli/commands/deals.d.ts +11 -0
  11. package/dist/cli/commands/deals.d.ts.map +1 -0
  12. package/dist/cli/commands/deals.js +498 -0
  13. package/dist/cli/commands/deals.js.map +1 -0
  14. package/dist/cli/commands/media.d.ts +11 -0
  15. package/dist/cli/commands/media.d.ts.map +1 -0
  16. package/dist/cli/commands/media.js +417 -0
  17. package/dist/cli/commands/media.js.map +1 -0
  18. package/dist/cli/commands/search.d.ts +11 -0
  19. package/dist/cli/commands/search.d.ts.map +1 -0
  20. package/dist/cli/commands/search.js +346 -0
  21. package/dist/cli/commands/search.js.map +1 -0
  22. package/dist/cli/index.d.ts +13 -0
  23. package/dist/cli/index.d.ts.map +1 -0
  24. package/dist/cli/index.js +173 -0
  25. package/dist/cli/index.js.map +1 -0
  26. package/dist/cli/repl.d.ts +15 -0
  27. package/dist/cli/repl.d.ts.map +1 -0
  28. package/dist/cli/repl.js +318 -0
  29. package/dist/cli/repl.js.map +1 -0
  30. package/dist/cli/utils/config.d.ts +91 -0
  31. package/dist/cli/utils/config.d.ts.map +1 -0
  32. package/dist/cli/utils/config.js +212 -0
  33. package/dist/cli/utils/config.js.map +1 -0
  34. package/dist/cli/utils/output.d.ts +136 -0
  35. package/dist/cli/utils/output.d.ts.map +1 -0
  36. package/dist/cli/utils/output.js +323 -0
  37. package/dist/cli/utils/output.js.map +1 -0
  38. package/dist/cli/utils/prompt.d.ts +81 -0
  39. package/dist/cli/utils/prompt.d.ts.map +1 -0
  40. package/dist/cli/utils/prompt.js +341 -0
  41. package/dist/cli/utils/prompt.js.map +1 -0
  42. package/dist/cli.d.ts +3 -0
  43. package/dist/cli.d.ts.map +1 -0
  44. package/dist/cli.js +8 -0
  45. package/dist/cli.js.map +1 -0
  46. package/dist/core/index.d.ts +6 -0
  47. package/dist/core/index.d.ts.map +1 -0
  48. package/dist/core/index.js +32 -0
  49. package/dist/core/index.js.map +1 -0
  50. package/dist/core/schemas.d.ts +3050 -0
  51. package/dist/core/schemas.d.ts.map +1 -0
  52. package/dist/core/schemas.js +667 -0
  53. package/dist/core/schemas.js.map +1 -0
  54. package/dist/core/types.d.ts +597 -0
  55. package/dist/core/types.d.ts.map +1 -0
  56. package/dist/core/types.js +8 -0
  57. package/dist/core/types.js.map +1 -0
  58. package/dist/index.d.ts +7 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +8 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/mcp/index.d.ts +14 -0
  63. package/dist/mcp/index.d.ts.map +1 -0
  64. package/dist/mcp/index.js +11 -0
  65. package/dist/mcp/index.js.map +1 -0
  66. package/dist/mcp/server.d.ts +13 -0
  67. package/dist/mcp/server.d.ts.map +1 -0
  68. package/dist/mcp/server.js +18 -0
  69. package/dist/mcp/server.js.map +1 -0
  70. package/dist/mcp/storage/client.d.ts +109 -0
  71. package/dist/mcp/storage/client.d.ts.map +1 -0
  72. package/dist/mcp/storage/client.js +355 -0
  73. package/dist/mcp/storage/client.js.map +1 -0
  74. package/dist/mcp/storage/index.d.ts +7 -0
  75. package/dist/mcp/storage/index.d.ts.map +1 -0
  76. package/dist/mcp/storage/index.js +6 -0
  77. package/dist/mcp/storage/index.js.map +1 -0
  78. package/dist/mcp/storage/types.d.ts +44 -0
  79. package/dist/mcp/storage/types.d.ts.map +1 -0
  80. package/dist/mcp/storage/types.js +35 -0
  81. package/dist/mcp/storage/types.js.map +1 -0
  82. package/dist/mcp/tools/definitions.d.ts +16 -0
  83. package/dist/mcp/tools/definitions.d.ts.map +1 -0
  84. package/dist/mcp/tools/definitions.js +914 -0
  85. package/dist/mcp/tools/definitions.js.map +1 -0
  86. package/dist/mcp/tools/handlers.d.ts +50 -0
  87. package/dist/mcp/tools/handlers.d.ts.map +1 -0
  88. package/dist/mcp/tools/handlers.js +760 -0
  89. package/dist/mcp/tools/handlers.js.map +1 -0
  90. package/dist/mcp/tools/index.d.ts +7 -0
  91. package/dist/mcp/tools/index.d.ts.map +1 -0
  92. package/dist/mcp/tools/index.js +6 -0
  93. package/dist/mcp/tools/index.js.map +1 -0
  94. package/dist/mcp/tools/types.d.ts +314 -0
  95. package/dist/mcp/tools/types.d.ts.map +1 -0
  96. package/dist/mcp/tools/types.js +5 -0
  97. package/dist/mcp/tools/types.js.map +1 -0
  98. package/dist/mcp/transports/stdio.d.ts +27 -0
  99. package/dist/mcp/transports/stdio.d.ts.map +1 -0
  100. package/dist/mcp/transports/stdio.js +237 -0
  101. package/dist/mcp/transports/stdio.js.map +1 -0
  102. package/dist/telemetry/index.d.ts +58 -0
  103. package/dist/telemetry/index.d.ts.map +1 -0
  104. package/dist/telemetry/index.js +109 -0
  105. package/dist/telemetry/index.js.map +1 -0
  106. package/dist/telemetry/logger.d.ts +116 -0
  107. package/dist/telemetry/logger.d.ts.map +1 -0
  108. package/dist/telemetry/logger.js +256 -0
  109. package/dist/telemetry/logger.js.map +1 -0
  110. package/dist/telemetry/metrics.d.ts +115 -0
  111. package/dist/telemetry/metrics.d.ts.map +1 -0
  112. package/dist/telemetry/metrics.js +292 -0
  113. package/dist/telemetry/metrics.js.map +1 -0
  114. package/dist/telemetry/tracer.d.ts +227 -0
  115. package/dist/telemetry/tracer.d.ts.map +1 -0
  116. package/dist/telemetry/tracer.js +355 -0
  117. package/dist/telemetry/tracer.js.map +1 -0
  118. package/dist/web/app.d.ts +2 -0
  119. package/dist/web/app.d.ts.map +1 -0
  120. package/dist/web/app.js +115 -0
  121. package/dist/web/app.js.map +1 -0
  122. package/dist/web/components/ContactList.d.ts +3 -0
  123. package/dist/web/components/ContactList.d.ts.map +1 -0
  124. package/dist/web/components/ContactList.js +262 -0
  125. package/dist/web/components/ContactList.js.map +1 -0
  126. package/dist/web/components/Dashboard.d.ts +3 -0
  127. package/dist/web/components/Dashboard.d.ts.map +1 -0
  128. package/dist/web/components/Dashboard.js +158 -0
  129. package/dist/web/components/Dashboard.js.map +1 -0
  130. package/dist/web/components/DealPipeline.d.ts +3 -0
  131. package/dist/web/components/DealPipeline.d.ts.map +1 -0
  132. package/dist/web/components/DealPipeline.js +306 -0
  133. package/dist/web/components/DealPipeline.js.map +1 -0
  134. package/dist/web/index.d.ts +2 -0
  135. package/dist/web/index.d.ts.map +1 -0
  136. package/dist/web/index.js +269 -0
  137. package/dist/web/index.js.map +1 -0
  138. package/dist/web/types.d.ts +75 -0
  139. package/dist/web/types.d.ts.map +1 -0
  140. package/dist/web/types.js +3 -0
  141. package/dist/web/types.js.map +1 -0
  142. package/native/index.d.ts +571 -0
  143. package/native/index.js +687 -0
  144. package/package.json +105 -0
  145. package/src/cli/commands/activities.ts +543 -0
  146. package/src/cli/commands/contacts.ts +563 -0
  147. package/src/cli/commands/deals.ts +637 -0
  148. package/src/cli/commands/media.ts +521 -0
  149. package/src/cli/commands/search.ts +426 -0
  150. package/src/cli/index.ts +203 -0
  151. package/src/cli/repl.ts +379 -0
  152. package/src/cli/utils/config.ts +299 -0
  153. package/src/cli/utils/output.ts +386 -0
  154. package/src/cli/utils/prompt.ts +444 -0
  155. package/src/cli.ts +11 -0
  156. package/src/core/index.ts +184 -0
  157. package/src/core/schemas.ts +770 -0
  158. package/src/core/types.ts +969 -0
  159. package/src/index.ts +8 -0
  160. package/src/mcp/index.ts +17 -0
  161. package/src/mcp/server.ts +26 -0
  162. package/src/mcp/storage/client.ts +408 -0
  163. package/src/mcp/storage/index.ts +7 -0
  164. package/src/mcp/storage/types.ts +72 -0
  165. package/src/mcp/tools/definitions.ts +961 -0
  166. package/src/mcp/tools/handlers.ts +805 -0
  167. package/src/mcp/tools/index.ts +7 -0
  168. package/src/mcp/tools/types.ts +390 -0
  169. package/src/mcp/transports/stdio.ts +225 -0
  170. package/src/telemetry/index.ts +131 -0
  171. package/src/telemetry/logger.ts +318 -0
  172. package/src/telemetry/metrics.ts +393 -0
  173. package/src/telemetry/tracer.ts +487 -0
  174. package/src/web/api/activities.ts +41 -0
  175. package/src/web/api/contacts.ts +114 -0
  176. package/src/web/api/deals.ts +108 -0
  177. package/src/web/api/media.ts +98 -0
  178. package/src/web/app.tsx +143 -0
  179. package/src/web/components/ActivityFeed.tsx +195 -0
  180. package/src/web/components/ContactList.tsx +340 -0
  181. package/src/web/components/Dashboard.tsx +214 -0
  182. package/src/web/components/DealPipeline.tsx +405 -0
  183. package/src/web/components/MediaGallery.tsx +334 -0
  184. package/src/web/index.html +14 -0
  185. package/src/web/index.ts +326 -0
  186. package/src/web/styles/main.css +180 -0
  187. package/src/web/types.ts +311 -0
@@ -0,0 +1,340 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import type { Contact, APIResponse } from '../types';
3
+
4
+ const statusColors: Record<Contact['status'], string> = {
5
+ lead: 'crm-tag-info',
6
+ prospect: 'crm-tag-warning',
7
+ customer: 'crm-tag-success',
8
+ churned: 'crm-tag-danger',
9
+ };
10
+
11
+ export default function ContactList() {
12
+ const [contacts, setContacts] = useState<Contact[]>([]);
13
+ const [loading, setLoading] = useState(true);
14
+ const [search, setSearch] = useState('');
15
+ const [statusFilter, setStatusFilter] = useState<string>('');
16
+ const [showModal, setShowModal] = useState(false);
17
+ const [selectedContact, setSelectedContact] = useState<Contact | null>(null);
18
+ const [formData, setFormData] = useState({
19
+ name: '',
20
+ email: '',
21
+ phone: '',
22
+ company: '',
23
+ title: '',
24
+ status: 'lead' as Contact['status'],
25
+ tags: [] as string[],
26
+ notes: '',
27
+ });
28
+
29
+ const fetchContacts = async () => {
30
+ try {
31
+ const params = new URLSearchParams();
32
+ if (search) params.set('search', search);
33
+ if (statusFilter) params.set('status', statusFilter);
34
+
35
+ const response = await fetch(`/api/contacts?${params}`);
36
+ const data: APIResponse<Contact[]> = await response.json();
37
+
38
+ if (data.success) {
39
+ setContacts(data.data || []);
40
+ }
41
+ } catch (error) {
42
+ console.error('Failed to fetch contacts:', error);
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ };
47
+
48
+ useEffect(() => {
49
+ fetchContacts();
50
+ }, [search, statusFilter]);
51
+
52
+ const handleCreate = () => {
53
+ setSelectedContact(null);
54
+ setFormData({
55
+ name: '',
56
+ email: '',
57
+ phone: '',
58
+ company: '',
59
+ title: '',
60
+ status: 'lead',
61
+ tags: [],
62
+ notes: '',
63
+ });
64
+ setShowModal(true);
65
+ };
66
+
67
+ const handleEdit = (contact: Contact) => {
68
+ setSelectedContact(contact);
69
+ setFormData({
70
+ name: contact.name,
71
+ email: contact.email,
72
+ phone: contact.phone || '',
73
+ company: contact.company || '',
74
+ title: contact.title || '',
75
+ status: contact.status,
76
+ tags: contact.tags,
77
+ notes: contact.notes || '',
78
+ });
79
+ setShowModal(true);
80
+ };
81
+
82
+ const handleDelete = async (id: string) => {
83
+ if (!confirm('Are you sure you want to delete this contact?')) return;
84
+
85
+ try {
86
+ await fetch(`/api/contacts/${id}`, { method: 'DELETE' });
87
+ setContacts(contacts.filter(c => c.id !== id));
88
+ } catch (error) {
89
+ console.error('Failed to delete contact:', error);
90
+ }
91
+ };
92
+
93
+ const handleSubmit = async (e: React.FormEvent) => {
94
+ e.preventDefault();
95
+
96
+ try {
97
+ const url = selectedContact
98
+ ? `/api/contacts/${selectedContact.id}`
99
+ : '/api/contacts';
100
+ const method = selectedContact ? 'PUT' : 'POST';
101
+
102
+ const response = await fetch(url, {
103
+ method,
104
+ headers: { 'Content-Type': 'application/json' },
105
+ body: JSON.stringify(formData),
106
+ });
107
+
108
+ const data: APIResponse<Contact> = await response.json();
109
+
110
+ if (data.success && data.data) {
111
+ if (selectedContact) {
112
+ setContacts(contacts.map(c => c.id === data.data!.id ? data.data! : c));
113
+ } else {
114
+ setContacts([data.data, ...contacts]);
115
+ }
116
+ setShowModal(false);
117
+ }
118
+ } catch (error) {
119
+ console.error('Failed to save contact:', error);
120
+ }
121
+ };
122
+
123
+ if (loading) {
124
+ return (
125
+ <div className="flex items-center justify-center h-64">
126
+ <div className="crm-spinner" />
127
+ </div>
128
+ );
129
+ }
130
+
131
+ return (
132
+ <div className="space-y-6">
133
+ {/* Header */}
134
+ <div className="flex items-center justify-between">
135
+ <div>
136
+ <h2 className="text-2xl font-bold">Contacts</h2>
137
+ <p className="text-gray-400">{contacts.length} total contacts</p>
138
+ </div>
139
+ <button onClick={handleCreate} className="crm-btn crm-btn-primary">
140
+ + Add Contact
141
+ </button>
142
+ </div>
143
+
144
+ {/* Filters */}
145
+ <div className="flex gap-4">
146
+ <div className="flex-1">
147
+ <input
148
+ type="text"
149
+ placeholder="Search contacts..."
150
+ value={search}
151
+ onChange={(e) => setSearch(e.target.value)}
152
+ className="crm-input"
153
+ />
154
+ </div>
155
+ <select
156
+ value={statusFilter}
157
+ onChange={(e) => setStatusFilter(e.target.value)}
158
+ className="crm-input w-48"
159
+ >
160
+ <option value="">All Statuses</option>
161
+ <option value="lead">Lead</option>
162
+ <option value="prospect">Prospect</option>
163
+ <option value="customer">Customer</option>
164
+ <option value="churned">Churned</option>
165
+ </select>
166
+ </div>
167
+
168
+ {/* Contact Table */}
169
+ <div className="bg-gray-800 rounded-xl border border-gray-700 overflow-hidden">
170
+ <table className="w-full">
171
+ <thead className="bg-gray-700/50">
172
+ <tr>
173
+ <th className="text-left px-4 py-3 text-sm font-medium text-gray-400">Name</th>
174
+ <th className="text-left px-4 py-3 text-sm font-medium text-gray-400">Email</th>
175
+ <th className="text-left px-4 py-3 text-sm font-medium text-gray-400">Company</th>
176
+ <th className="text-left px-4 py-3 text-sm font-medium text-gray-400">Status</th>
177
+ <th className="text-right px-4 py-3 text-sm font-medium text-gray-400">Actions</th>
178
+ </tr>
179
+ </thead>
180
+ <tbody className="divide-y divide-gray-700">
181
+ {contacts.length === 0 ? (
182
+ <tr>
183
+ <td colSpan={5} className="text-center py-12 text-gray-400">
184
+ No contacts found
185
+ </td>
186
+ </tr>
187
+ ) : (
188
+ contacts.map(contact => (
189
+ <tr key={contact.id} className="hover:bg-gray-700/30 transition-colors">
190
+ <td className="px-4 py-3">
191
+ <div>
192
+ <p className="font-medium">{contact.name}</p>
193
+ {contact.title && (
194
+ <p className="text-sm text-gray-400">{contact.title}</p>
195
+ )}
196
+ </div>
197
+ </td>
198
+ <td className="px-4 py-3 text-gray-300">{contact.email}</td>
199
+ <td className="px-4 py-3 text-gray-300">{contact.company || '-'}</td>
200
+ <td className="px-4 py-3">
201
+ <span className={`crm-tag ${statusColors[contact.status]}`}>
202
+ {contact.status}
203
+ </span>
204
+ </td>
205
+ <td className="px-4 py-3">
206
+ <div className="flex justify-end gap-2">
207
+ <button
208
+ onClick={() => handleEdit(contact)}
209
+ className="px-3 py-1 text-sm bg-gray-700 hover:bg-gray-600 rounded transition-colors"
210
+ >
211
+ Edit
212
+ </button>
213
+ <button
214
+ onClick={() => handleDelete(contact.id)}
215
+ className="px-3 py-1 text-sm bg-red-900/50 hover:bg-red-800/50 text-red-400 rounded transition-colors"
216
+ >
217
+ Delete
218
+ </button>
219
+ </div>
220
+ </td>
221
+ </tr>
222
+ ))
223
+ )}
224
+ </tbody>
225
+ </table>
226
+ </div>
227
+
228
+ {/* Modal */}
229
+ {showModal && (
230
+ <div className="crm-modal-backdrop" onClick={() => setShowModal(false)}>
231
+ <div className="crm-modal" onClick={(e) => e.stopPropagation()}>
232
+ <h3 className="text-xl font-bold mb-4">
233
+ {selectedContact ? 'Edit Contact' : 'Add Contact'}
234
+ </h3>
235
+ <form onSubmit={handleSubmit} className="space-y-4">
236
+ <div>
237
+ <label className="block text-sm font-medium text-gray-400 mb-1">
238
+ Name *
239
+ </label>
240
+ <input
241
+ type="text"
242
+ required
243
+ value={formData.name}
244
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
245
+ className="crm-input"
246
+ />
247
+ </div>
248
+ <div>
249
+ <label className="block text-sm font-medium text-gray-400 mb-1">
250
+ Email *
251
+ </label>
252
+ <input
253
+ type="email"
254
+ required
255
+ value={formData.email}
256
+ onChange={(e) => setFormData({ ...formData, email: e.target.value })}
257
+ className="crm-input"
258
+ />
259
+ </div>
260
+ <div className="grid grid-cols-2 gap-4">
261
+ <div>
262
+ <label className="block text-sm font-medium text-gray-400 mb-1">
263
+ Phone
264
+ </label>
265
+ <input
266
+ type="tel"
267
+ value={formData.phone}
268
+ onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
269
+ className="crm-input"
270
+ />
271
+ </div>
272
+ <div>
273
+ <label className="block text-sm font-medium text-gray-400 mb-1">
274
+ Status
275
+ </label>
276
+ <select
277
+ value={formData.status}
278
+ onChange={(e) => setFormData({ ...formData, status: e.target.value as Contact['status'] })}
279
+ className="crm-input"
280
+ >
281
+ <option value="lead">Lead</option>
282
+ <option value="prospect">Prospect</option>
283
+ <option value="customer">Customer</option>
284
+ <option value="churned">Churned</option>
285
+ </select>
286
+ </div>
287
+ </div>
288
+ <div className="grid grid-cols-2 gap-4">
289
+ <div>
290
+ <label className="block text-sm font-medium text-gray-400 mb-1">
291
+ Company
292
+ </label>
293
+ <input
294
+ type="text"
295
+ value={formData.company}
296
+ onChange={(e) => setFormData({ ...formData, company: e.target.value })}
297
+ className="crm-input"
298
+ />
299
+ </div>
300
+ <div>
301
+ <label className="block text-sm font-medium text-gray-400 mb-1">
302
+ Title
303
+ </label>
304
+ <input
305
+ type="text"
306
+ value={formData.title}
307
+ onChange={(e) => setFormData({ ...formData, title: e.target.value })}
308
+ className="crm-input"
309
+ />
310
+ </div>
311
+ </div>
312
+ <div>
313
+ <label className="block text-sm font-medium text-gray-400 mb-1">
314
+ Notes
315
+ </label>
316
+ <textarea
317
+ value={formData.notes}
318
+ onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
319
+ className="crm-input h-24 resize-none"
320
+ />
321
+ </div>
322
+ <div className="flex gap-3 pt-4">
323
+ <button type="submit" className="crm-btn crm-btn-primary flex-1">
324
+ {selectedContact ? 'Update' : 'Create'}
325
+ </button>
326
+ <button
327
+ type="button"
328
+ onClick={() => setShowModal(false)}
329
+ className="crm-btn crm-btn-secondary flex-1"
330
+ >
331
+ Cancel
332
+ </button>
333
+ </div>
334
+ </form>
335
+ </div>
336
+ </div>
337
+ )}
338
+ </div>
339
+ );
340
+ }
@@ -0,0 +1,214 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import type { DashboardStats, Activity, Deal } from '../types';
3
+
4
+ interface StatCardProps {
5
+ title: string;
6
+ value: string | number;
7
+ change?: string;
8
+ icon: string;
9
+ color: string;
10
+ }
11
+
12
+ function StatCard({ title, value, change, icon, color }: StatCardProps) {
13
+ return (
14
+ <div className="crm-card">
15
+ <div className="flex items-center justify-between">
16
+ <div>
17
+ <p className="text-gray-400 text-sm">{title}</p>
18
+ <p className="text-3xl font-bold mt-1">{value}</p>
19
+ {change && (
20
+ <p className={`text-sm mt-1 ${change.startsWith('+') ? 'text-green-400' : 'text-red-400'}`}>
21
+ {change}
22
+ </p>
23
+ )}
24
+ </div>
25
+ <div className={`text-4xl ${color}`}>{icon}</div>
26
+ </div>
27
+ </div>
28
+ );
29
+ }
30
+
31
+ export default function Dashboard() {
32
+ const [stats, setStats] = useState<DashboardStats | null>(null);
33
+ const [recentActivities, setRecentActivities] = useState<Activity[]>([]);
34
+ const [topDeals, setTopDeals] = useState<Deal[]>([]);
35
+ const [loading, setLoading] = useState(true);
36
+
37
+ useEffect(() => {
38
+ async function fetchData() {
39
+ try {
40
+ const [statsRes, activitiesRes, dealsRes] = await Promise.all([
41
+ fetch('/api/dashboard/stats'),
42
+ fetch('/api/activities?limit=5'),
43
+ fetch('/api/deals'),
44
+ ]);
45
+
46
+ const statsData = await statsRes.json();
47
+ const activitiesData = await activitiesRes.json();
48
+ const dealsData = await dealsRes.json();
49
+
50
+ if (statsData.success) setStats(statsData.data);
51
+ if (activitiesData.success) setRecentActivities(activitiesData.data);
52
+ if (dealsData.success) {
53
+ const sorted = dealsData.data
54
+ .filter((d: Deal) => !d.stage.startsWith('closed_'))
55
+ .sort((a: Deal, b: Deal) => b.value - a.value)
56
+ .slice(0, 5);
57
+ setTopDeals(sorted);
58
+ }
59
+ } catch (error) {
60
+ console.error('Failed to fetch dashboard data:', error);
61
+ } finally {
62
+ setLoading(false);
63
+ }
64
+ }
65
+
66
+ fetchData();
67
+ }, []);
68
+
69
+ if (loading) {
70
+ return (
71
+ <div className="flex items-center justify-center h-64">
72
+ <div className="crm-spinner" />
73
+ </div>
74
+ );
75
+ }
76
+
77
+ const formatCurrency = (value: number) => {
78
+ return new Intl.NumberFormat('en-US', {
79
+ style: 'currency',
80
+ currency: 'USD',
81
+ minimumFractionDigits: 0,
82
+ maximumFractionDigits: 0,
83
+ }).format(value);
84
+ };
85
+
86
+ return (
87
+ <div className="space-y-6">
88
+ {/* Stats Grid */}
89
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
90
+ <StatCard
91
+ title="Total Contacts"
92
+ value={stats?.totalContacts || 0}
93
+ icon="👥"
94
+ color="text-blue-400"
95
+ />
96
+ <StatCard
97
+ title="Active Deals"
98
+ value={stats?.activeDeals || 0}
99
+ icon="💼"
100
+ color="text-indigo-400"
101
+ />
102
+ <StatCard
103
+ title="Pipeline Value"
104
+ value={formatCurrency(stats?.pipelineValue || 0)}
105
+ icon="📈"
106
+ color="text-green-400"
107
+ />
108
+ <StatCard
109
+ title="Won This Month"
110
+ value={formatCurrency(stats?.wonThisMonth || 0)}
111
+ change="+12%"
112
+ icon="🏆"
113
+ color="text-yellow-400"
114
+ />
115
+ </div>
116
+
117
+ {/* Secondary Stats */}
118
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
119
+ <div className="crm-card">
120
+ <h3 className="text-lg font-semibold mb-4">Activities Today</h3>
121
+ <div className="flex items-center justify-between">
122
+ <span className="text-4xl font-bold text-indigo-400">
123
+ {stats?.activitiesToday || 0}
124
+ </span>
125
+ <div className="text-right">
126
+ <p className="text-sm text-gray-400">vs yesterday</p>
127
+ <p className="text-green-400">+5</p>
128
+ </div>
129
+ </div>
130
+ </div>
131
+
132
+ <div className="crm-card">
133
+ <h3 className="text-lg font-semibold mb-4">Conversion Rate</h3>
134
+ <div className="flex items-center justify-between">
135
+ <span className="text-4xl font-bold text-green-400">
136
+ {(stats?.conversionRate || 0).toFixed(1)}%
137
+ </span>
138
+ <div className="text-right">
139
+ <p className="text-sm text-gray-400">30-day avg</p>
140
+ <p className="text-green-400">+2.3%</p>
141
+ </div>
142
+ </div>
143
+ </div>
144
+
145
+ <div className="crm-card">
146
+ <h3 className="text-lg font-semibold mb-4">Quick Actions</h3>
147
+ <div className="flex gap-2">
148
+ <button className="crm-btn crm-btn-primary flex-1">
149
+ + Contact
150
+ </button>
151
+ <button className="crm-btn crm-btn-secondary flex-1">
152
+ + Deal
153
+ </button>
154
+ </div>
155
+ </div>
156
+ </div>
157
+
158
+ {/* Recent Activity & Top Deals */}
159
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
160
+ {/* Recent Activities */}
161
+ <div className="crm-card">
162
+ <h3 className="text-lg font-semibold mb-4">Recent Activities</h3>
163
+ <div className="space-y-3">
164
+ {recentActivities.length === 0 ? (
165
+ <p className="text-gray-400 text-center py-8">No recent activities</p>
166
+ ) : (
167
+ recentActivities.map(activity => (
168
+ <div key={activity.id} className="activity-item">
169
+ <div className={`activity-dot ${
170
+ activity.type === 'deal' ? 'bg-green-500' :
171
+ activity.type === 'contact' ? 'bg-blue-500' :
172
+ 'bg-gray-500'
173
+ }`} />
174
+ <div className="flex-1">
175
+ <p className="text-sm">{activity.description}</p>
176
+ <p className="text-xs text-gray-500 mt-1">
177
+ {new Date(activity.timestamp).toLocaleString()}
178
+ </p>
179
+ </div>
180
+ </div>
181
+ ))
182
+ )}
183
+ </div>
184
+ </div>
185
+
186
+ {/* Top Deals */}
187
+ <div className="crm-card">
188
+ <h3 className="text-lg font-semibold mb-4">Top Deals</h3>
189
+ <div className="space-y-3">
190
+ {topDeals.length === 0 ? (
191
+ <p className="text-gray-400 text-center py-8">No active deals</p>
192
+ ) : (
193
+ topDeals.map((deal, index) => (
194
+ <div key={deal.id} className="flex items-center gap-4 p-3 bg-gray-700/50 rounded-lg">
195
+ <span className="text-lg font-bold text-gray-500">#{index + 1}</span>
196
+ <div className="flex-1">
197
+ <p className="font-medium">{deal.title}</p>
198
+ <p className="text-sm text-gray-400">{deal.stage}</p>
199
+ </div>
200
+ <div className="text-right">
201
+ <p className="font-bold text-green-400">
202
+ {formatCurrency(deal.value)}
203
+ </p>
204
+ <p className="text-xs text-gray-500">{deal.probability}% prob</p>
205
+ </div>
206
+ </div>
207
+ ))
208
+ )}
209
+ </div>
210
+ </div>
211
+ </div>
212
+ </div>
213
+ );
214
+ }