@nextsparkjs/theme-crm 0.1.0-beta.1
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/CRM_PLAN.md +343 -0
- package/about.md +122 -0
- package/config/app.config.ts +185 -0
- package/config/billing.config.ts +187 -0
- package/config/dashboard.config.ts +372 -0
- package/config/dev.config.ts +55 -0
- package/config/features.config.ts +336 -0
- package/config/flows.config.ts +511 -0
- package/config/permissions.config.ts +297 -0
- package/config/theme.config.ts +111 -0
- package/entities/activities/activities.config.ts +61 -0
- package/entities/activities/activities.fields.ts +362 -0
- package/entities/activities/activities.service.ts +503 -0
- package/entities/activities/activities.types.ts +117 -0
- package/entities/activities/messages/en.json +123 -0
- package/entities/activities/messages/es.json +123 -0
- package/entities/activities/migrations/020_activities_table.sql +123 -0
- package/entities/activities/migrations/021_activities_metas.sql +114 -0
- package/entities/activities/migrations/022_activities_sample_data.sql +420 -0
- package/entities/campaigns/campaigns.config.ts +61 -0
- package/entities/campaigns/campaigns.fields.ts +413 -0
- package/entities/campaigns/campaigns.service.ts +426 -0
- package/entities/campaigns/campaigns.types.ts +124 -0
- package/entities/campaigns/messages/en.json +145 -0
- package/entities/campaigns/messages/es.json +145 -0
- package/entities/campaigns/migrations/001_campaigns_table.sql +127 -0
- package/entities/campaigns/migrations/002_campaigns_metas.sql +114 -0
- package/entities/campaigns/migrations/003_campaigns_sample_data.sql +364 -0
- package/entities/companies/companies.config.ts +61 -0
- package/entities/companies/companies.fields.ts +429 -0
- package/entities/companies/companies.service.ts +566 -0
- package/entities/companies/companies.types.ts +125 -0
- package/entities/companies/messages/en.json +146 -0
- package/entities/companies/messages/es.json +146 -0
- package/entities/companies/migrations/001_companies_table.sql +150 -0
- package/entities/companies/migrations/002_companies_metas.sql +114 -0
- package/entities/companies/migrations/003_companies_sample_data.sql +246 -0
- package/entities/contacts/contacts.config.ts +61 -0
- package/entities/contacts/contacts.fields.ts +359 -0
- package/entities/contacts/contacts.service.ts +509 -0
- package/entities/contacts/contacts.types.ts +108 -0
- package/entities/contacts/messages/en.json +117 -0
- package/entities/contacts/messages/es.json +117 -0
- package/entities/contacts/migrations/001_contacts_table.sql +134 -0
- package/entities/contacts/migrations/002_contacts_metas.sql +114 -0
- package/entities/contacts/migrations/003_contacts_sample_data.sql +421 -0
- package/entities/leads/leads.config.ts +61 -0
- package/entities/leads/leads.fields.ts +336 -0
- package/entities/leads/leads.service.ts +496 -0
- package/entities/leads/leads.types.ts +114 -0
- package/entities/leads/messages/en.json +132 -0
- package/entities/leads/messages/es.json +132 -0
- package/entities/leads/migrations/001_leads_table.sql +150 -0
- package/entities/leads/migrations/002_leads_metas.sql +120 -0
- package/entities/leads/migrations/003_leads_sample_data.sql +242 -0
- package/entities/notes/messages/en.json +114 -0
- package/entities/notes/messages/es.json +114 -0
- package/entities/notes/migrations/020_notes_table.sql +118 -0
- package/entities/notes/migrations/021_notes_metas.sql +114 -0
- package/entities/notes/migrations/022_notes_sample_data.sql +275 -0
- package/entities/notes/notes.config.ts +61 -0
- package/entities/notes/notes.fields.ts +283 -0
- package/entities/notes/notes.service.ts +320 -0
- package/entities/notes/notes.types.ts +102 -0
- package/entities/opportunities/messages/en.json +107 -0
- package/entities/opportunities/messages/es.json +107 -0
- package/entities/opportunities/migrations/010_opportunities_table.sql +145 -0
- package/entities/opportunities/migrations/011_opportunities_metas.sql +114 -0
- package/entities/opportunities/migrations/012_opportunities_sample_data.sql +438 -0
- package/entities/opportunities/opportunities.config.ts +61 -0
- package/entities/opportunities/opportunities.fields.ts +416 -0
- package/entities/opportunities/opportunities.service.ts +525 -0
- package/entities/opportunities/opportunities.types.ts +135 -0
- package/entities/pipelines/messages/en.json +115 -0
- package/entities/pipelines/messages/es.json +115 -0
- package/entities/pipelines/migrations/001_pipelines_table.sql +106 -0
- package/entities/pipelines/migrations/002_pipelines_metas.sql +114 -0
- package/entities/pipelines/migrations/003_pipelines_sample_data.sql +91 -0
- package/entities/pipelines/pipelines.config.ts +62 -0
- package/entities/pipelines/pipelines.fields.ts +193 -0
- package/entities/pipelines/pipelines.service.ts +383 -0
- package/entities/pipelines/pipelines.types.ts +78 -0
- package/entities/products/messages/en.json +135 -0
- package/entities/products/messages/es.json +135 -0
- package/entities/products/migrations/001_products_table.sql +117 -0
- package/entities/products/migrations/002_products_metas.sql +114 -0
- package/entities/products/migrations/003_products_sample_data.sql +247 -0
- package/entities/products/products.config.ts +62 -0
- package/entities/products/products.fields.ts +361 -0
- package/entities/products/products.service.ts +437 -0
- package/entities/products/products.types.ts +125 -0
- package/lib/crm-constants.ts +77 -0
- package/lib/crm-utils.ts +185 -0
- package/lib/selectors.ts +333 -0
- package/messages/en.json +131 -0
- package/messages/es.json +131 -0
- package/migrations/999_theme_sample_data.sql +473 -0
- package/package.json +18 -0
- package/pendings.md +205 -0
- package/permissions-matrix.md +216 -0
- package/styles/components.css +414 -0
- package/styles/crm-theme.css +358 -0
- package/styles/globals.css +576 -0
- package/styles/variables.css +111 -0
- package/templates/dashboard/(main)/activities/components/ActivityCard.tsx +169 -0
- package/templates/dashboard/(main)/activities/components/ActivityTimeline.tsx +165 -0
- package/templates/dashboard/(main)/activities/page.tsx +297 -0
- package/templates/dashboard/(main)/campaigns/page.tsx +373 -0
- package/templates/dashboard/(main)/companies/page.tsx +296 -0
- package/templates/dashboard/(main)/contacts/page.tsx +347 -0
- package/templates/dashboard/(main)/layout.tsx +98 -0
- package/templates/dashboard/(main)/leads/page.tsx +335 -0
- package/templates/dashboard/(main)/opportunities/[id]/edit/page.tsx +95 -0
- package/templates/dashboard/(main)/opportunities/create/page.tsx +94 -0
- package/templates/dashboard/(main)/opportunities/page.tsx +350 -0
- package/templates/dashboard/(main)/pipelines/[id]/edit/page.tsx +95 -0
- package/templates/dashboard/(main)/pipelines/[id]/page.tsx +143 -0
- package/templates/dashboard/(main)/pipelines/create/page.tsx +94 -0
- package/templates/dashboard/(main)/pipelines/page.tsx +234 -0
- package/templates/dashboard/(main)/products/[id]/edit/page.tsx +97 -0
- package/templates/dashboard/(main)/products/[id]/page.tsx +509 -0
- package/templates/dashboard/(main)/products/create/page.tsx +96 -0
- package/templates/dashboard/(main)/products/page.tsx +308 -0
- package/templates/shared/ActionButtons.tsx +41 -0
- package/templates/shared/CRMDashboard.tsx +519 -0
- package/templates/shared/CRMDataTable.tsx +441 -0
- package/templates/shared/CRMMetricCard.tsx +76 -0
- package/templates/shared/CRMMobileNav.tsx +172 -0
- package/templates/shared/CRMSidebar.tsx +346 -0
- package/templates/shared/CRMTopBar.tsx +265 -0
- package/templates/shared/DealCard.tsx +123 -0
- package/templates/shared/EntityCard.tsx +58 -0
- package/templates/shared/OpportunityForm.tsx +649 -0
- package/templates/shared/PipelineForm.tsx +367 -0
- package/templates/shared/PipelineKanban.tsx +194 -0
- package/templates/shared/QuickFilters.tsx +47 -0
- package/templates/shared/StageColumn.tsx +175 -0
- package/templates/shared/StageSelect.tsx +177 -0
- package/templates/shared/StagesRepeater.tsx +317 -0
- package/templates/shared/index.ts +9 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contacts Page
|
|
3
|
+
* Professional contacts management with data table and bulk actions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client'
|
|
7
|
+
|
|
8
|
+
import { useRouter } from 'next/navigation'
|
|
9
|
+
import { useEffect, useState, useMemo } from 'react'
|
|
10
|
+
import { Button } from '@nextsparkjs/core/components/ui/button'
|
|
11
|
+
import { CRMDataTable, type Column, type BulkAction } from '@/themes/crm/templates/shared/CRMDataTable'
|
|
12
|
+
import {
|
|
13
|
+
Plus,
|
|
14
|
+
Users,
|
|
15
|
+
Trash2,
|
|
16
|
+
Download,
|
|
17
|
+
Mail,
|
|
18
|
+
Phone,
|
|
19
|
+
Building2,
|
|
20
|
+
Tag,
|
|
21
|
+
Clock,
|
|
22
|
+
Send
|
|
23
|
+
} from 'lucide-react'
|
|
24
|
+
import { fetchWithTeam } from '@nextsparkjs/core/lib/api/entities'
|
|
25
|
+
import { useTeamContext } from '@nextsparkjs/core/contexts/TeamContext'
|
|
26
|
+
import { cn } from '@nextsparkjs/core/lib/utils'
|
|
27
|
+
import { usePermission } from '@nextsparkjs/core/lib/permissions/hooks'
|
|
28
|
+
|
|
29
|
+
interface Contact {
|
|
30
|
+
id: string
|
|
31
|
+
firstName: string
|
|
32
|
+
lastName: string
|
|
33
|
+
email: string
|
|
34
|
+
phone?: string
|
|
35
|
+
companyId?: string
|
|
36
|
+
companyName?: string
|
|
37
|
+
title?: string
|
|
38
|
+
department?: string
|
|
39
|
+
tags?: string[]
|
|
40
|
+
lastActivityAt?: string
|
|
41
|
+
createdAt: string
|
|
42
|
+
updatedAt: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Tags component
|
|
46
|
+
function ContactTags({ tags }: { tags?: string[] }) {
|
|
47
|
+
if (!tags || tags.length === 0) return <span className="text-muted-foreground">-</span>
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="flex flex-wrap gap-1">
|
|
51
|
+
{tags.slice(0, 3).map((tag) => (
|
|
52
|
+
<span
|
|
53
|
+
key={tag}
|
|
54
|
+
className="inline-flex items-center gap-1 px-2 py-0.5 bg-primary/10 text-primary text-xs font-medium rounded"
|
|
55
|
+
>
|
|
56
|
+
<Tag className="w-2.5 h-2.5" />
|
|
57
|
+
{tag}
|
|
58
|
+
</span>
|
|
59
|
+
))}
|
|
60
|
+
{tags.length > 3 && (
|
|
61
|
+
<span className="text-xs text-muted-foreground">
|
|
62
|
+
+{tags.length - 3} more
|
|
63
|
+
</span>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Relative time component
|
|
70
|
+
function RelativeTime({ date }: { date?: string }) {
|
|
71
|
+
if (!date) return <span className="text-muted-foreground">Never</span>
|
|
72
|
+
|
|
73
|
+
const now = new Date()
|
|
74
|
+
const past = new Date(date)
|
|
75
|
+
const diffMs = now.getTime() - past.getTime()
|
|
76
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
|
77
|
+
|
|
78
|
+
let text = ''
|
|
79
|
+
if (diffDays === 0) {
|
|
80
|
+
text = 'Today'
|
|
81
|
+
} else if (diffDays === 1) {
|
|
82
|
+
text = 'Yesterday'
|
|
83
|
+
} else if (diffDays < 7) {
|
|
84
|
+
text = `${diffDays} days ago`
|
|
85
|
+
} else if (diffDays < 30) {
|
|
86
|
+
text = `${Math.floor(diffDays / 7)} weeks ago`
|
|
87
|
+
} else {
|
|
88
|
+
text = past.toLocaleDateString()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className={cn(
|
|
93
|
+
'flex items-center gap-1.5 text-sm',
|
|
94
|
+
diffDays > 30 ? 'text-muted-foreground' : 'text-foreground'
|
|
95
|
+
)}>
|
|
96
|
+
<Clock className="w-3.5 h-3.5 text-muted-foreground" />
|
|
97
|
+
<span>{text}</span>
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default function ContactsPage() {
|
|
103
|
+
const router = useRouter()
|
|
104
|
+
const { currentTeam, isLoading: teamLoading } = useTeamContext()
|
|
105
|
+
const [contacts, setContacts] = useState<Contact[]>([])
|
|
106
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
107
|
+
|
|
108
|
+
// Permission checks for bulk actions
|
|
109
|
+
const canDeleteContacts = usePermission('contacts.delete')
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (teamLoading || !currentTeam) return
|
|
113
|
+
|
|
114
|
+
async function fetchContacts() {
|
|
115
|
+
try {
|
|
116
|
+
const response = await fetchWithTeam('/api/v1/contacts')
|
|
117
|
+
if (!response.ok) throw new Error('Failed to fetch contacts')
|
|
118
|
+
const result = await response.json()
|
|
119
|
+
setContacts(result.data || [])
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error('Error loading contacts:', error)
|
|
122
|
+
} finally {
|
|
123
|
+
setIsLoading(false)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fetchContacts()
|
|
128
|
+
}, [teamLoading, currentTeam])
|
|
129
|
+
|
|
130
|
+
// Stats
|
|
131
|
+
const stats = useMemo(() => {
|
|
132
|
+
const withCompany = contacts.filter(c => c.companyId || c.companyName).length
|
|
133
|
+
const recentlyActive = contacts.filter(c => {
|
|
134
|
+
if (!c.lastActivityAt) return false
|
|
135
|
+
const days = Math.floor((Date.now() - new Date(c.lastActivityAt).getTime()) / (1000 * 60 * 60 * 24))
|
|
136
|
+
return days <= 30
|
|
137
|
+
}).length
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
total: contacts.length,
|
|
141
|
+
withCompany,
|
|
142
|
+
recentlyActive,
|
|
143
|
+
withEmail: contacts.filter(c => c.email).length,
|
|
144
|
+
}
|
|
145
|
+
}, [contacts])
|
|
146
|
+
|
|
147
|
+
// Column definitions
|
|
148
|
+
const columns: Column<Contact>[] = [
|
|
149
|
+
{
|
|
150
|
+
key: 'name',
|
|
151
|
+
header: 'Name',
|
|
152
|
+
sortable: true,
|
|
153
|
+
render: (_, contact) => (
|
|
154
|
+
<div className="flex items-center gap-3">
|
|
155
|
+
<div className="w-9 h-9 rounded-full bg-primary/10 flex items-center justify-center text-xs font-semibold text-primary">
|
|
156
|
+
{contact.firstName?.[0]?.toUpperCase()}{contact.lastName?.[0]?.toUpperCase()}
|
|
157
|
+
</div>
|
|
158
|
+
<div>
|
|
159
|
+
<p className="font-medium text-foreground">
|
|
160
|
+
{contact.firstName} {contact.lastName}
|
|
161
|
+
</p>
|
|
162
|
+
{contact.title && (
|
|
163
|
+
<p className="text-xs text-muted-foreground">{contact.title}</p>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
),
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
key: 'contact',
|
|
171
|
+
header: 'Contact Info',
|
|
172
|
+
render: (_, contact) => (
|
|
173
|
+
<div className="space-y-1">
|
|
174
|
+
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
|
175
|
+
<Mail className="w-3.5 h-3.5" />
|
|
176
|
+
<span className="truncate max-w-[180px]">{contact.email}</span>
|
|
177
|
+
</div>
|
|
178
|
+
{contact.phone && (
|
|
179
|
+
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
|
180
|
+
<Phone className="w-3.5 h-3.5" />
|
|
181
|
+
<span>{contact.phone}</span>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
),
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
key: 'companyName',
|
|
189
|
+
header: 'Company',
|
|
190
|
+
sortable: true,
|
|
191
|
+
render: (value, contact) => value ? (
|
|
192
|
+
<button
|
|
193
|
+
onClick={(e) => {
|
|
194
|
+
e.stopPropagation()
|
|
195
|
+
if (contact.companyId) {
|
|
196
|
+
router.push(`/dashboard/companies/${contact.companyId}`)
|
|
197
|
+
}
|
|
198
|
+
}}
|
|
199
|
+
className="flex items-center gap-1.5 text-sm hover:text-primary transition-colors"
|
|
200
|
+
>
|
|
201
|
+
<Building2 className="w-3.5 h-3.5 text-muted-foreground" />
|
|
202
|
+
<span className={contact.companyId ? 'hover:underline' : ''}>{value}</span>
|
|
203
|
+
</button>
|
|
204
|
+
) : <span className="text-muted-foreground">-</span>,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
key: 'tags',
|
|
208
|
+
header: 'Tags',
|
|
209
|
+
render: (value) => <ContactTags tags={value} />,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
key: 'lastActivityAt',
|
|
213
|
+
header: 'Last Activity',
|
|
214
|
+
sortable: true,
|
|
215
|
+
render: (value) => <RelativeTime date={value} />,
|
|
216
|
+
},
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
// Bulk actions - filtered by permissions
|
|
220
|
+
const bulkActions: BulkAction[] = [
|
|
221
|
+
{
|
|
222
|
+
id: 'email',
|
|
223
|
+
label: 'Send Email',
|
|
224
|
+
icon: <Send className="w-4 h-4" />,
|
|
225
|
+
onClick: (ids) => {
|
|
226
|
+
console.log('Send email to:', ids)
|
|
227
|
+
// Implement email logic
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'export',
|
|
232
|
+
label: 'Export',
|
|
233
|
+
icon: <Download className="w-4 h-4" />,
|
|
234
|
+
onClick: (ids) => {
|
|
235
|
+
console.log('Export contacts:', ids)
|
|
236
|
+
// Implement export logic
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
// Only show delete action if user has permission
|
|
240
|
+
...(canDeleteContacts ? [{
|
|
241
|
+
id: 'delete',
|
|
242
|
+
label: 'Delete',
|
|
243
|
+
icon: <Trash2 className="w-4 h-4" />,
|
|
244
|
+
variant: 'destructive' as const,
|
|
245
|
+
onClick: async (ids: string[]) => {
|
|
246
|
+
if (confirm(`Delete ${ids.length} contact(s)?`)) {
|
|
247
|
+
console.log('Delete contacts:', ids)
|
|
248
|
+
// Implement delete logic
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
}] : []),
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
const handleRowClick = (contact: Contact) => {
|
|
255
|
+
router.push(`/dashboard/contacts/${contact.id}`)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const handleAddContact = () => {
|
|
259
|
+
router.push('/dashboard/contacts/create')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<div className="p-6 space-y-6">
|
|
264
|
+
{/* Header */}
|
|
265
|
+
<div className="flex items-start justify-between">
|
|
266
|
+
<div>
|
|
267
|
+
<h1 className="text-2xl font-bold text-foreground tracking-tight">
|
|
268
|
+
Contacts
|
|
269
|
+
</h1>
|
|
270
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
271
|
+
Manage your business contacts and relationships
|
|
272
|
+
</p>
|
|
273
|
+
</div>
|
|
274
|
+
<Button onClick={handleAddContact} className="gap-2" data-cy="contacts-add">
|
|
275
|
+
<Plus className="w-4 h-4" />
|
|
276
|
+
Add Contact
|
|
277
|
+
</Button>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
{/* Stats */}
|
|
281
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
282
|
+
<div className="bg-card border rounded-xl p-4">
|
|
283
|
+
<div className="flex items-center gap-3">
|
|
284
|
+
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
285
|
+
<Users className="w-5 h-5 text-primary" />
|
|
286
|
+
</div>
|
|
287
|
+
<div>
|
|
288
|
+
<p className="text-2xl font-bold text-foreground">{stats.total}</p>
|
|
289
|
+
<p className="text-xs text-muted-foreground">Total Contacts</p>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<div className="bg-card border rounded-xl p-4">
|
|
295
|
+
<div className="flex items-center gap-3">
|
|
296
|
+
<div className="w-10 h-10 rounded-lg bg-amber-500/10 flex items-center justify-center">
|
|
297
|
+
<Building2 className="w-5 h-5 text-amber-600" />
|
|
298
|
+
</div>
|
|
299
|
+
<div>
|
|
300
|
+
<p className="text-2xl font-bold text-foreground">{stats.withCompany}</p>
|
|
301
|
+
<p className="text-xs text-muted-foreground">With Company</p>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<div className="bg-card border rounded-xl p-4">
|
|
307
|
+
<div className="flex items-center gap-3">
|
|
308
|
+
<div className="w-10 h-10 rounded-lg bg-emerald-500/10 flex items-center justify-center">
|
|
309
|
+
<Clock className="w-5 h-5 text-emerald-600" />
|
|
310
|
+
</div>
|
|
311
|
+
<div>
|
|
312
|
+
<p className="text-2xl font-bold text-foreground">{stats.recentlyActive}</p>
|
|
313
|
+
<p className="text-xs text-muted-foreground">Recently Active</p>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<div className="bg-card border rounded-xl p-4">
|
|
319
|
+
<div className="flex items-center gap-3">
|
|
320
|
+
<div className="w-10 h-10 rounded-lg bg-muted flex items-center justify-center">
|
|
321
|
+
<Mail className="w-5 h-5 text-muted-foreground" />
|
|
322
|
+
</div>
|
|
323
|
+
<div>
|
|
324
|
+
<p className="text-2xl font-bold text-foreground">{stats.withEmail}</p>
|
|
325
|
+
<p className="text-xs text-muted-foreground">With Email</p>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
{/* Data Table */}
|
|
332
|
+
<CRMDataTable
|
|
333
|
+
data={contacts}
|
|
334
|
+
columns={columns}
|
|
335
|
+
bulkActions={bulkActions}
|
|
336
|
+
onRowClick={handleRowClick}
|
|
337
|
+
isLoading={isLoading}
|
|
338
|
+
searchPlaceholder="Search contacts..."
|
|
339
|
+
searchFields={['firstName', 'lastName', 'email', 'companyName']}
|
|
340
|
+
pageSize={15}
|
|
341
|
+
emptyMessage="No contacts yet"
|
|
342
|
+
emptyDescription="Start adding contacts to build your network."
|
|
343
|
+
entitySlug="contacts"
|
|
344
|
+
/>
|
|
345
|
+
</div>
|
|
346
|
+
)
|
|
347
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRM Dashboard Main Layout
|
|
3
|
+
* Professional layout with custom sidebar and topbar for CRM theme
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client'
|
|
7
|
+
|
|
8
|
+
import { useState, createContext, useContext, ReactNode, useEffect } from 'react'
|
|
9
|
+
import { CRMSidebar } from '@/themes/crm/templates/shared/CRMSidebar'
|
|
10
|
+
import { CRMTopBar } from '@/themes/crm/templates/shared/CRMTopBar'
|
|
11
|
+
import { CRMMobileNav } from '@/themes/crm/templates/shared/CRMMobileNav'
|
|
12
|
+
import { cn } from '@nextsparkjs/core/lib/utils'
|
|
13
|
+
|
|
14
|
+
// Context for sidebar state
|
|
15
|
+
interface SidebarContextValue {
|
|
16
|
+
expanded: boolean
|
|
17
|
+
setExpanded: (value: boolean) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SidebarContext = createContext<SidebarContextValue | undefined>(undefined)
|
|
21
|
+
|
|
22
|
+
export function useCRMSidebar() {
|
|
23
|
+
const context = useContext(SidebarContext)
|
|
24
|
+
if (!context) {
|
|
25
|
+
return { expanded: false, setExpanded: () => {} }
|
|
26
|
+
}
|
|
27
|
+
return context
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface CRMDashboardLayoutProps {
|
|
31
|
+
children: ReactNode
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function CRMDashboardLayout({ children }: CRMDashboardLayoutProps) {
|
|
35
|
+
const [expanded, setExpanded] = useState(false)
|
|
36
|
+
|
|
37
|
+
// Sync CSS variable with sidebar state
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
document.documentElement.style.setProperty(
|
|
40
|
+
'--crm-sidebar-width',
|
|
41
|
+
expanded ? '16rem' : '4rem'
|
|
42
|
+
)
|
|
43
|
+
}, [expanded])
|
|
44
|
+
|
|
45
|
+
// Initialize CSS variable on mount
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
document.documentElement.style.setProperty('--crm-sidebar-width', '4rem')
|
|
48
|
+
return () => {
|
|
49
|
+
document.documentElement.style.removeProperty('--crm-sidebar-width')
|
|
50
|
+
}
|
|
51
|
+
}, [])
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<SidebarContext.Provider value={{ expanded, setExpanded }}>
|
|
55
|
+
<div
|
|
56
|
+
className="min-h-screen bg-background"
|
|
57
|
+
onMouseMove={(e) => {
|
|
58
|
+
// Expand sidebar when mouse is near left edge
|
|
59
|
+
if (e.clientX < 64) {
|
|
60
|
+
setExpanded(true)
|
|
61
|
+
} else if (e.clientX > 280) {
|
|
62
|
+
setExpanded(false)
|
|
63
|
+
}
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
{/* Desktop: Sidebar */}
|
|
67
|
+
<CRMSidebar />
|
|
68
|
+
|
|
69
|
+
{/* Desktop: TopBar */}
|
|
70
|
+
<CRMTopBar />
|
|
71
|
+
|
|
72
|
+
{/* Mobile: Navigation */}
|
|
73
|
+
<CRMMobileNav />
|
|
74
|
+
|
|
75
|
+
{/* Main Content Area */}
|
|
76
|
+
<main
|
|
77
|
+
className={cn(
|
|
78
|
+
'min-h-screen transition-all duration-300 ease-out',
|
|
79
|
+
// Mobile: padding for bottom nav
|
|
80
|
+
'pb-20',
|
|
81
|
+
// Desktop: padding for topbar
|
|
82
|
+
'lg:pb-0 lg:pt-16',
|
|
83
|
+
// Desktop: margin for collapsed sidebar (expanded handled by CSS var)
|
|
84
|
+
'lg:ml-16'
|
|
85
|
+
)}
|
|
86
|
+
style={{
|
|
87
|
+
marginLeft: undefined, // Handled by Tailwind on desktop
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
{/* Inner wrapper with responsive padding */}
|
|
91
|
+
<div className="lg:pt-0 pt-14">
|
|
92
|
+
{children}
|
|
93
|
+
</div>
|
|
94
|
+
</main>
|
|
95
|
+
</div>
|
|
96
|
+
</SidebarContext.Provider>
|
|
97
|
+
)
|
|
98
|
+
}
|