@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,108 @@
1
+ import type { Deal } from '../types';
2
+ import { generateId } from './contacts';
3
+
4
+ // In-memory storage (would be replaced with database in production)
5
+ export const dealsStore = new Map<string, Deal>();
6
+
7
+ // List deals with optional filters
8
+ export async function listDeals(request: Request): Promise<Response> {
9
+ const url = new URL(request.url);
10
+ const stage = url.searchParams.get('stage') || '';
11
+ const contactId = url.searchParams.get('contactId') || '';
12
+
13
+ let results = Array.from(dealsStore.values());
14
+
15
+ if (stage) {
16
+ results = results.filter(d => d.stage === stage);
17
+ }
18
+
19
+ if (contactId) {
20
+ results = results.filter(d => d.contactId === contactId);
21
+ }
22
+
23
+ // Sort by value descending
24
+ results.sort((a, b) => b.value - a.value);
25
+
26
+ return Response.json({ success: true, data: results });
27
+ }
28
+
29
+ // Create a new deal
30
+ export async function createDeal(request: Request): Promise<Response> {
31
+ try {
32
+ const body = await request.json();
33
+ const id = generateId();
34
+ const now = new Date().toISOString();
35
+
36
+ const deal: Deal = {
37
+ id,
38
+ contactId: body.contactId,
39
+ title: body.title,
40
+ value: body.value,
41
+ currency: body.currency || 'USD',
42
+ stage: body.stage || 'discovery',
43
+ probability: body.probability || 10,
44
+ expectedCloseDate: body.expectedCloseDate,
45
+ description: body.description,
46
+ createdAt: now,
47
+ updatedAt: now,
48
+ };
49
+
50
+ dealsStore.set(id, deal);
51
+
52
+ return Response.json({ success: true, data: deal });
53
+ } catch (error) {
54
+ return Response.json(
55
+ { success: false, error: 'Invalid request body' },
56
+ { status: 400 }
57
+ );
58
+ }
59
+ }
60
+
61
+ // Get a single deal
62
+ export async function getDeal(request: Request, params: Record<string, string>): Promise<Response> {
63
+ const deal = dealsStore.get(params.id);
64
+ if (!deal) {
65
+ return Response.json({ success: false, error: 'Deal not found' }, { status: 404 });
66
+ }
67
+ return Response.json({ success: true, data: deal });
68
+ }
69
+
70
+ // Update a deal
71
+ export async function updateDeal(request: Request, params: Record<string, string>): Promise<Response> {
72
+ const deal = dealsStore.get(params.id);
73
+ if (!deal) {
74
+ return Response.json({ success: false, error: 'Deal not found' }, { status: 404 });
75
+ }
76
+
77
+ try {
78
+ const body = await request.json();
79
+ const updated: Deal = {
80
+ ...deal,
81
+ ...body,
82
+ id: deal.id,
83
+ createdAt: deal.createdAt,
84
+ updatedAt: new Date().toISOString(),
85
+ };
86
+
87
+ dealsStore.set(params.id, updated);
88
+
89
+ return Response.json({ success: true, data: updated });
90
+ } catch (error) {
91
+ return Response.json(
92
+ { success: false, error: 'Invalid request body' },
93
+ { status: 400 }
94
+ );
95
+ }
96
+ }
97
+
98
+ // Delete a deal
99
+ export async function deleteDeal(request: Request, params: Record<string, string>): Promise<Response> {
100
+ const deal = dealsStore.get(params.id);
101
+ if (!deal) {
102
+ return Response.json({ success: false, error: 'Deal not found' }, { status: 404 });
103
+ }
104
+
105
+ dealsStore.delete(params.id);
106
+
107
+ return Response.json({ success: true });
108
+ }
@@ -0,0 +1,98 @@
1
+ import type { Media } from '../types';
2
+ import { generateId } from './contacts';
3
+
4
+ // In-memory storage (would be replaced with database/S3 in production)
5
+ export const mediaStore = new Map<string, Media>();
6
+
7
+ // Upload media file
8
+ export async function uploadMedia(request: Request): Promise<Response> {
9
+ try {
10
+ const formData = await request.formData();
11
+ const file = formData.get('file') as File;
12
+
13
+ if (!file) {
14
+ return Response.json({ success: false, error: 'No file provided' }, { status: 400 });
15
+ }
16
+
17
+ const id = generateId();
18
+ const now = new Date().toISOString();
19
+
20
+ // Store file
21
+ const uploadsDir = './uploads';
22
+ const buffer = await file.arrayBuffer();
23
+ await Bun.write(`${uploadsDir}/${id}-${file.name}`, buffer);
24
+
25
+ const media: Media = {
26
+ id,
27
+ filename: file.name,
28
+ mimetype: file.type,
29
+ size: file.size,
30
+ url: `/media/${id}-${file.name}`,
31
+ thumbnailUrl: undefined,
32
+ contactId: formData.get('contactId') as string || undefined,
33
+ dealId: formData.get('dealId') as string || undefined,
34
+ uploadedBy: formData.get('userId') as string || 'system',
35
+ uploadedAt: now,
36
+ };
37
+
38
+ // Generate thumbnail for images
39
+ if (file.type.startsWith('image/')) {
40
+ media.thumbnailUrl = media.url;
41
+ }
42
+
43
+ mediaStore.set(id, media);
44
+
45
+ return Response.json({ success: true, data: media });
46
+ } catch (error) {
47
+ console.error('Upload error:', error);
48
+ return Response.json(
49
+ { success: false, error: 'Failed to upload file' },
50
+ { status: 500 }
51
+ );
52
+ }
53
+ }
54
+
55
+ // List media files
56
+ export async function listMedia(request: Request): Promise<Response> {
57
+ const url = new URL(request.url);
58
+ const contactId = url.searchParams.get('contactId');
59
+ const dealId = url.searchParams.get('dealId');
60
+ const type = url.searchParams.get('type');
61
+
62
+ let results = Array.from(mediaStore.values());
63
+
64
+ if (contactId) {
65
+ results = results.filter(m => m.contactId === contactId);
66
+ }
67
+
68
+ if (dealId) {
69
+ results = results.filter(m => m.dealId === dealId);
70
+ }
71
+
72
+ if (type) {
73
+ results = results.filter(m => m.mimetype.startsWith(type));
74
+ }
75
+
76
+ return Response.json({ success: true, data: results });
77
+ }
78
+
79
+ // Delete media file
80
+ export async function deleteMedia(request: Request, params: Record<string, string>): Promise<Response> {
81
+ const media = mediaStore.get(params.id);
82
+ if (!media) {
83
+ return Response.json({ success: false, error: 'Media not found' }, { status: 404 });
84
+ }
85
+
86
+ // Delete file from disk
87
+ try {
88
+ const filePath = `./uploads/${params.id}-${media.filename}`;
89
+ await Bun.write(filePath, ''); // Overwrite with empty
90
+ // Note: In production, you'd use proper file deletion
91
+ } catch (error) {
92
+ console.error('Failed to delete file:', error);
93
+ }
94
+
95
+ mediaStore.delete(params.id);
96
+
97
+ return Response.json({ success: true });
98
+ }
@@ -0,0 +1,143 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import Dashboard from './components/Dashboard';
4
+ import ContactList from './components/ContactList';
5
+ import DealPipeline from './components/DealPipeline';
6
+ import ActivityFeed from './components/ActivityFeed';
7
+ import MediaGallery from './components/MediaGallery';
8
+ import type { WSMessage } from './types';
9
+
10
+ type Tab = 'dashboard' | 'contacts' | 'deals' | 'activity' | 'media';
11
+
12
+ function App() {
13
+ const [activeTab, setActiveTab] = useState<Tab>('dashboard');
14
+ const [ws, setWs] = useState<WebSocket | null>(null);
15
+ const [isConnected, setIsConnected] = useState(false);
16
+ const [notifications, setNotifications] = useState<string[]>([]);
17
+
18
+ // WebSocket connection
19
+ useEffect(() => {
20
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
21
+ const wsUrl = `${protocol}//${window.location.host}/ws`;
22
+ const websocket = new WebSocket(wsUrl);
23
+
24
+ websocket.onopen = () => {
25
+ setIsConnected(true);
26
+ console.log('WebSocket connected');
27
+ };
28
+
29
+ websocket.onmessage = (event) => {
30
+ const message: WSMessage = JSON.parse(event.data);
31
+ console.log('WebSocket message:', message);
32
+
33
+ if (message.type === 'activity') {
34
+ addNotification('New activity update');
35
+ } else if (message.type === 'deal_update') {
36
+ addNotification('Deal updated');
37
+ } else if (message.type === 'contact_update') {
38
+ addNotification('Contact updated');
39
+ }
40
+ };
41
+
42
+ websocket.onclose = () => {
43
+ setIsConnected(false);
44
+ console.log('WebSocket disconnected');
45
+ };
46
+
47
+ websocket.onerror = (error) => {
48
+ console.error('WebSocket error:', error);
49
+ };
50
+
51
+ setWs(websocket);
52
+
53
+ return () => {
54
+ websocket.close();
55
+ };
56
+ }, []);
57
+
58
+ const addNotification = useCallback((message: string) => {
59
+ setNotifications(prev => [...prev, message]);
60
+ setTimeout(() => {
61
+ setNotifications(prev => prev.slice(1));
62
+ }, 3000);
63
+ }, []);
64
+
65
+ const tabs: { id: Tab; label: string; icon: string }[] = [
66
+ { id: 'dashboard', label: 'Dashboard', icon: '📊' },
67
+ { id: 'contacts', label: 'Contacts', icon: '👥' },
68
+ { id: 'deals', label: 'Deals', icon: '💼' },
69
+ { id: 'activity', label: 'Activity', icon: '📝' },
70
+ { id: 'media', label: 'Media', icon: '🖼️' },
71
+ ];
72
+
73
+ return (
74
+ <div className="min-h-screen bg-gray-900 text-gray-100">
75
+ {/* Header */}
76
+ <header className="bg-gray-800 border-b border-gray-700 px-6 py-4">
77
+ <div className="flex items-center justify-between">
78
+ <div className="flex items-center gap-4">
79
+ <h1 className="text-2xl font-bold text-indigo-400">CRM Dashboard</h1>
80
+ <div className="flex items-center gap-2">
81
+ <div className={`w-2 h-2 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
82
+ <span className="text-sm text-gray-400">
83
+ {isConnected ? 'Connected' : 'Disconnected'}
84
+ </span>
85
+ </div>
86
+ </div>
87
+ <div className="flex items-center gap-4">
88
+ <button className="px-4 py-2 bg-gray-700 rounded-lg hover:bg-gray-600 transition-colors">
89
+ Settings
90
+ </button>
91
+ </div>
92
+ </div>
93
+ </header>
94
+
95
+ {/* Navigation */}
96
+ <nav className="bg-gray-800 border-b border-gray-700">
97
+ <div className="flex px-6">
98
+ {tabs.map(tab => (
99
+ <button
100
+ key={tab.id}
101
+ onClick={() => setActiveTab(tab.id)}
102
+ className={`px-6 py-4 flex items-center gap-2 border-b-2 transition-colors ${
103
+ activeTab === tab.id
104
+ ? 'border-indigo-500 text-indigo-400'
105
+ : 'border-transparent text-gray-400 hover:text-gray-200'
106
+ }`}
107
+ >
108
+ <span>{tab.icon}</span>
109
+ <span>{tab.label}</span>
110
+ </button>
111
+ ))}
112
+ </div>
113
+ </nav>
114
+
115
+ {/* Main Content */}
116
+ <main className="p-6">
117
+ {activeTab === 'dashboard' && <Dashboard />}
118
+ {activeTab === 'contacts' && <ContactList />}
119
+ {activeTab === 'deals' && <DealPipeline />}
120
+ {activeTab === 'activity' && <ActivityFeed />}
121
+ {activeTab === 'media' && <MediaGallery />}
122
+ </main>
123
+
124
+ {/* Notifications */}
125
+ <div className="fixed bottom-4 right-4 space-y-2 z-50">
126
+ {notifications.map((notification, index) => (
127
+ <div
128
+ key={index}
129
+ className="bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 shadow-lg animate-slide-in"
130
+ >
131
+ <p className="text-sm">{notification}</p>
132
+ </div>
133
+ ))}
134
+ </div>
135
+ </div>
136
+ );
137
+ }
138
+
139
+ const container = document.getElementById('root');
140
+ if (container) {
141
+ const root = createRoot(container);
142
+ root.render(<App />);
143
+ }
@@ -0,0 +1,195 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import type { Activity, APIResponse } from '../types';
3
+
4
+ const activityIcons: Record<Activity['type'], string> = {
5
+ call: '📞',
6
+ email: '📧',
7
+ meeting: '🗓️',
8
+ note: '📝',
9
+ deal: '💼',
10
+ contact: '👤',
11
+ };
12
+
13
+ const activityColors: Record<Activity['type'], string> = {
14
+ call: 'bg-blue-500',
15
+ email: 'bg-purple-500',
16
+ meeting: 'bg-orange-500',
17
+ note: 'bg-gray-500',
18
+ deal: 'bg-green-500',
19
+ contact: 'bg-cyan-500',
20
+ };
21
+
22
+ export default function ActivityFeed() {
23
+ const [activities, setActivities] = useState<Activity[]>([]);
24
+ const [loading, setLoading] = useState(true);
25
+ const [filter, setFilter] = useState<string>('');
26
+
27
+ useEffect(() => {
28
+ fetchActivities();
29
+ }, []);
30
+
31
+ const fetchActivities = async () => {
32
+ try {
33
+ const response = await fetch('/api/activities?limit=100');
34
+ const data: APIResponse<Activity[]> = await response.json();
35
+
36
+ if (data.success) {
37
+ setActivities(data.data || []);
38
+ }
39
+ } catch (error) {
40
+ console.error('Failed to fetch activities:', error);
41
+ } finally {
42
+ setLoading(false);
43
+ }
44
+ };
45
+
46
+ const filteredActivities = filter
47
+ ? activities.filter(a => a.type === filter)
48
+ : activities;
49
+
50
+ const groupedActivities = filteredActivities.reduce((groups, activity) => {
51
+ const date = new Date(activity.timestamp).toDateString();
52
+ if (!groups[date]) {
53
+ groups[date] = [];
54
+ }
55
+ groups[date].push(activity);
56
+ return groups;
57
+ }, {} as Record<string, Activity[]>);
58
+
59
+ const formatTime = (timestamp: string) => {
60
+ return new Date(timestamp).toLocaleTimeString('en-US', {
61
+ hour: 'numeric',
62
+ minute: '2-digit',
63
+ });
64
+ };
65
+
66
+ const formatDate = (dateString: string) => {
67
+ const date = new Date(dateString);
68
+ const today = new Date();
69
+ const yesterday = new Date(today);
70
+ yesterday.setDate(yesterday.getDate() - 1);
71
+
72
+ if (date.toDateString() === today.toDateString()) {
73
+ return 'Today';
74
+ } else if (date.toDateString() === yesterday.toDateString()) {
75
+ return 'Yesterday';
76
+ } else {
77
+ return date.toLocaleDateString('en-US', {
78
+ weekday: 'long',
79
+ month: 'long',
80
+ day: 'numeric',
81
+ });
82
+ }
83
+ };
84
+
85
+ if (loading) {
86
+ return (
87
+ <div className="flex items-center justify-center h-64">
88
+ <div className="crm-spinner" />
89
+ </div>
90
+ );
91
+ }
92
+
93
+ return (
94
+ <div className="space-y-6">
95
+ {/* Header */}
96
+ <div className="flex items-center justify-between">
97
+ <div>
98
+ <h2 className="text-2xl font-bold">Activity Feed</h2>
99
+ <p className="text-gray-400">{activities.length} total activities</p>
100
+ </div>
101
+ </div>
102
+
103
+ {/* Filters */}
104
+ <div className="flex gap-2 flex-wrap">
105
+ <button
106
+ onClick={() => setFilter('')}
107
+ className={`px-4 py-2 rounded-lg text-sm transition-colors ${
108
+ filter === '' ? 'bg-indigo-600 text-white' : 'bg-gray-700 text-gray-300 hover:bg-gray-600'
109
+ }`}
110
+ >
111
+ All
112
+ </button>
113
+ {Object.entries(activityIcons).map(([type, icon]) => (
114
+ <button
115
+ key={type}
116
+ onClick={() => setFilter(type)}
117
+ className={`px-4 py-2 rounded-lg text-sm transition-colors flex items-center gap-2 ${
118
+ filter === type ? 'bg-indigo-600 text-white' : 'bg-gray-700 text-gray-300 hover:bg-gray-600'
119
+ }`}
120
+ >
121
+ <span>{icon}</span>
122
+ <span className="capitalize">{type}</span>
123
+ </button>
124
+ ))}
125
+ </div>
126
+
127
+ {/* Activity List */}
128
+ <div className="space-y-6">
129
+ {Object.entries(groupedActivities).map(([date, dateActivities]) => (
130
+ <div key={date}>
131
+ {/* Date Header */}
132
+ <div className="flex items-center gap-4 mb-4">
133
+ <h3 className="text-lg font-semibold text-gray-300">
134
+ {formatDate(date)}
135
+ </h3>
136
+ <div className="flex-1 h-px bg-gray-700" />
137
+ <span className="text-sm text-gray-500">
138
+ {dateActivities.length} activities
139
+ </span>
140
+ </div>
141
+
142
+ {/* Activities for this date */}
143
+ <div className="space-y-3">
144
+ {dateActivities.map(activity => (
145
+ <div
146
+ key={activity.id}
147
+ className="activity-item bg-gray-800/50 rounded-lg border border-gray-700/50"
148
+ >
149
+ {/* Icon */}
150
+ <div className={`w-10 h-10 rounded-full ${activityColors[activity.type]} flex items-center justify-center text-lg flex-shrink-0`}>
151
+ {activityIcons[activity.type]}
152
+ </div>
153
+
154
+ {/* Content */}
155
+ <div className="flex-1 min-w-0">
156
+ <div className="flex items-start justify-between gap-4">
157
+ <div>
158
+ <p className="font-medium">{activity.description}</p>
159
+ <div className="flex items-center gap-2 mt-1 text-sm text-gray-400">
160
+ <span className="capitalize">{activity.type}</span>
161
+ {activity.contactId && (
162
+ <>
163
+ <span className="text-gray-600">•</span>
164
+ <span>Contact: {activity.contactId}</span>
165
+ </>
166
+ )}
167
+ {activity.dealId && (
168
+ <>
169
+ <span className="text-gray-600">•</span>
170
+ <span>Deal: {activity.dealId}</span>
171
+ </>
172
+ )}
173
+ </div>
174
+ </div>
175
+ <span className="text-sm text-gray-500 flex-shrink-0">
176
+ {formatTime(activity.timestamp)}
177
+ </span>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ ))}
182
+ </div>
183
+ </div>
184
+ ))}
185
+
186
+ {filteredActivities.length === 0 && (
187
+ <div className="text-center py-12 text-gray-400">
188
+ <p className="text-lg">No activities found</p>
189
+ <p className="text-sm mt-2">Activities will appear here as you interact with contacts and deals</p>
190
+ </div>
191
+ )}
192
+ </div>
193
+ </div>
194
+ );
195
+ }