@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.
- package/README.md +174 -0
- package/dist/cli/commands/activities.d.ts +11 -0
- package/dist/cli/commands/activities.d.ts.map +1 -0
- package/dist/cli/commands/activities.js +427 -0
- package/dist/cli/commands/activities.js.map +1 -0
- package/dist/cli/commands/contacts.d.ts +11 -0
- package/dist/cli/commands/contacts.d.ts.map +1 -0
- package/dist/cli/commands/contacts.js +458 -0
- package/dist/cli/commands/contacts.js.map +1 -0
- package/dist/cli/commands/deals.d.ts +11 -0
- package/dist/cli/commands/deals.d.ts.map +1 -0
- package/dist/cli/commands/deals.js +498 -0
- package/dist/cli/commands/deals.js.map +1 -0
- package/dist/cli/commands/media.d.ts +11 -0
- package/dist/cli/commands/media.d.ts.map +1 -0
- package/dist/cli/commands/media.js +417 -0
- package/dist/cli/commands/media.js.map +1 -0
- package/dist/cli/commands/search.d.ts +11 -0
- package/dist/cli/commands/search.d.ts.map +1 -0
- package/dist/cli/commands/search.js +346 -0
- package/dist/cli/commands/search.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +173 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/repl.d.ts +15 -0
- package/dist/cli/repl.d.ts.map +1 -0
- package/dist/cli/repl.js +318 -0
- package/dist/cli/repl.js.map +1 -0
- package/dist/cli/utils/config.d.ts +91 -0
- package/dist/cli/utils/config.d.ts.map +1 -0
- package/dist/cli/utils/config.js +212 -0
- package/dist/cli/utils/config.js.map +1 -0
- package/dist/cli/utils/output.d.ts +136 -0
- package/dist/cli/utils/output.d.ts.map +1 -0
- package/dist/cli/utils/output.js +323 -0
- package/dist/cli/utils/output.js.map +1 -0
- package/dist/cli/utils/prompt.d.ts +81 -0
- package/dist/cli/utils/prompt.d.ts.map +1 -0
- package/dist/cli/utils/prompt.js +341 -0
- package/dist/cli/utils/prompt.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +8 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +32 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/schemas.d.ts +3050 -0
- package/dist/core/schemas.d.ts.map +1 -0
- package/dist/core/schemas.js +667 -0
- package/dist/core/schemas.js.map +1 -0
- package/dist/core/types.d.ts +597 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +8 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +14 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +11 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +13 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +18 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/storage/client.d.ts +109 -0
- package/dist/mcp/storage/client.d.ts.map +1 -0
- package/dist/mcp/storage/client.js +355 -0
- package/dist/mcp/storage/client.js.map +1 -0
- package/dist/mcp/storage/index.d.ts +7 -0
- package/dist/mcp/storage/index.d.ts.map +1 -0
- package/dist/mcp/storage/index.js +6 -0
- package/dist/mcp/storage/index.js.map +1 -0
- package/dist/mcp/storage/types.d.ts +44 -0
- package/dist/mcp/storage/types.d.ts.map +1 -0
- package/dist/mcp/storage/types.js +35 -0
- package/dist/mcp/storage/types.js.map +1 -0
- package/dist/mcp/tools/definitions.d.ts +16 -0
- package/dist/mcp/tools/definitions.d.ts.map +1 -0
- package/dist/mcp/tools/definitions.js +914 -0
- package/dist/mcp/tools/definitions.js.map +1 -0
- package/dist/mcp/tools/handlers.d.ts +50 -0
- package/dist/mcp/tools/handlers.d.ts.map +1 -0
- package/dist/mcp/tools/handlers.js +760 -0
- package/dist/mcp/tools/handlers.js.map +1 -0
- package/dist/mcp/tools/index.d.ts +7 -0
- package/dist/mcp/tools/index.d.ts.map +1 -0
- package/dist/mcp/tools/index.js +6 -0
- package/dist/mcp/tools/index.js.map +1 -0
- package/dist/mcp/tools/types.d.ts +314 -0
- package/dist/mcp/tools/types.d.ts.map +1 -0
- package/dist/mcp/tools/types.js +5 -0
- package/dist/mcp/tools/types.js.map +1 -0
- package/dist/mcp/transports/stdio.d.ts +27 -0
- package/dist/mcp/transports/stdio.d.ts.map +1 -0
- package/dist/mcp/transports/stdio.js +237 -0
- package/dist/mcp/transports/stdio.js.map +1 -0
- package/dist/telemetry/index.d.ts +58 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +109 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/telemetry/logger.d.ts +116 -0
- package/dist/telemetry/logger.d.ts.map +1 -0
- package/dist/telemetry/logger.js +256 -0
- package/dist/telemetry/logger.js.map +1 -0
- package/dist/telemetry/metrics.d.ts +115 -0
- package/dist/telemetry/metrics.d.ts.map +1 -0
- package/dist/telemetry/metrics.js +292 -0
- package/dist/telemetry/metrics.js.map +1 -0
- package/dist/telemetry/tracer.d.ts +227 -0
- package/dist/telemetry/tracer.d.ts.map +1 -0
- package/dist/telemetry/tracer.js +355 -0
- package/dist/telemetry/tracer.js.map +1 -0
- package/dist/web/app.d.ts +2 -0
- package/dist/web/app.d.ts.map +1 -0
- package/dist/web/app.js +115 -0
- package/dist/web/app.js.map +1 -0
- package/dist/web/components/ContactList.d.ts +3 -0
- package/dist/web/components/ContactList.d.ts.map +1 -0
- package/dist/web/components/ContactList.js +262 -0
- package/dist/web/components/ContactList.js.map +1 -0
- package/dist/web/components/Dashboard.d.ts +3 -0
- package/dist/web/components/Dashboard.d.ts.map +1 -0
- package/dist/web/components/Dashboard.js +158 -0
- package/dist/web/components/Dashboard.js.map +1 -0
- package/dist/web/components/DealPipeline.d.ts +3 -0
- package/dist/web/components/DealPipeline.d.ts.map +1 -0
- package/dist/web/components/DealPipeline.js +306 -0
- package/dist/web/components/DealPipeline.js.map +1 -0
- package/dist/web/index.d.ts +2 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +269 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/types.d.ts +75 -0
- package/dist/web/types.d.ts.map +1 -0
- package/dist/web/types.js +3 -0
- package/dist/web/types.js.map +1 -0
- package/native/index.d.ts +571 -0
- package/native/index.js +687 -0
- package/package.json +105 -0
- package/src/cli/commands/activities.ts +543 -0
- package/src/cli/commands/contacts.ts +563 -0
- package/src/cli/commands/deals.ts +637 -0
- package/src/cli/commands/media.ts +521 -0
- package/src/cli/commands/search.ts +426 -0
- package/src/cli/index.ts +203 -0
- package/src/cli/repl.ts +379 -0
- package/src/cli/utils/config.ts +299 -0
- package/src/cli/utils/output.ts +386 -0
- package/src/cli/utils/prompt.ts +444 -0
- package/src/cli.ts +11 -0
- package/src/core/index.ts +184 -0
- package/src/core/schemas.ts +770 -0
- package/src/core/types.ts +969 -0
- package/src/index.ts +8 -0
- package/src/mcp/index.ts +17 -0
- package/src/mcp/server.ts +26 -0
- package/src/mcp/storage/client.ts +408 -0
- package/src/mcp/storage/index.ts +7 -0
- package/src/mcp/storage/types.ts +72 -0
- package/src/mcp/tools/definitions.ts +961 -0
- package/src/mcp/tools/handlers.ts +805 -0
- package/src/mcp/tools/index.ts +7 -0
- package/src/mcp/tools/types.ts +390 -0
- package/src/mcp/transports/stdio.ts +225 -0
- package/src/telemetry/index.ts +131 -0
- package/src/telemetry/logger.ts +318 -0
- package/src/telemetry/metrics.ts +393 -0
- package/src/telemetry/tracer.ts +487 -0
- package/src/web/api/activities.ts +41 -0
- package/src/web/api/contacts.ts +114 -0
- package/src/web/api/deals.ts +108 -0
- package/src/web/api/media.ts +98 -0
- package/src/web/app.tsx +143 -0
- package/src/web/components/ActivityFeed.tsx +195 -0
- package/src/web/components/ContactList.tsx +340 -0
- package/src/web/components/Dashboard.tsx +214 -0
- package/src/web/components/DealPipeline.tsx +405 -0
- package/src/web/components/MediaGallery.tsx +334 -0
- package/src/web/index.html +14 -0
- package/src/web/index.ts +326 -0
- package/src/web/styles/main.css +180 -0
- 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
|
+
}
|