@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.
Files changed (140) hide show
  1. package/CRM_PLAN.md +343 -0
  2. package/about.md +122 -0
  3. package/config/app.config.ts +185 -0
  4. package/config/billing.config.ts +187 -0
  5. package/config/dashboard.config.ts +372 -0
  6. package/config/dev.config.ts +55 -0
  7. package/config/features.config.ts +336 -0
  8. package/config/flows.config.ts +511 -0
  9. package/config/permissions.config.ts +297 -0
  10. package/config/theme.config.ts +111 -0
  11. package/entities/activities/activities.config.ts +61 -0
  12. package/entities/activities/activities.fields.ts +362 -0
  13. package/entities/activities/activities.service.ts +503 -0
  14. package/entities/activities/activities.types.ts +117 -0
  15. package/entities/activities/messages/en.json +123 -0
  16. package/entities/activities/messages/es.json +123 -0
  17. package/entities/activities/migrations/020_activities_table.sql +123 -0
  18. package/entities/activities/migrations/021_activities_metas.sql +114 -0
  19. package/entities/activities/migrations/022_activities_sample_data.sql +420 -0
  20. package/entities/campaigns/campaigns.config.ts +61 -0
  21. package/entities/campaigns/campaigns.fields.ts +413 -0
  22. package/entities/campaigns/campaigns.service.ts +426 -0
  23. package/entities/campaigns/campaigns.types.ts +124 -0
  24. package/entities/campaigns/messages/en.json +145 -0
  25. package/entities/campaigns/messages/es.json +145 -0
  26. package/entities/campaigns/migrations/001_campaigns_table.sql +127 -0
  27. package/entities/campaigns/migrations/002_campaigns_metas.sql +114 -0
  28. package/entities/campaigns/migrations/003_campaigns_sample_data.sql +364 -0
  29. package/entities/companies/companies.config.ts +61 -0
  30. package/entities/companies/companies.fields.ts +429 -0
  31. package/entities/companies/companies.service.ts +566 -0
  32. package/entities/companies/companies.types.ts +125 -0
  33. package/entities/companies/messages/en.json +146 -0
  34. package/entities/companies/messages/es.json +146 -0
  35. package/entities/companies/migrations/001_companies_table.sql +150 -0
  36. package/entities/companies/migrations/002_companies_metas.sql +114 -0
  37. package/entities/companies/migrations/003_companies_sample_data.sql +246 -0
  38. package/entities/contacts/contacts.config.ts +61 -0
  39. package/entities/contacts/contacts.fields.ts +359 -0
  40. package/entities/contacts/contacts.service.ts +509 -0
  41. package/entities/contacts/contacts.types.ts +108 -0
  42. package/entities/contacts/messages/en.json +117 -0
  43. package/entities/contacts/messages/es.json +117 -0
  44. package/entities/contacts/migrations/001_contacts_table.sql +134 -0
  45. package/entities/contacts/migrations/002_contacts_metas.sql +114 -0
  46. package/entities/contacts/migrations/003_contacts_sample_data.sql +421 -0
  47. package/entities/leads/leads.config.ts +61 -0
  48. package/entities/leads/leads.fields.ts +336 -0
  49. package/entities/leads/leads.service.ts +496 -0
  50. package/entities/leads/leads.types.ts +114 -0
  51. package/entities/leads/messages/en.json +132 -0
  52. package/entities/leads/messages/es.json +132 -0
  53. package/entities/leads/migrations/001_leads_table.sql +150 -0
  54. package/entities/leads/migrations/002_leads_metas.sql +120 -0
  55. package/entities/leads/migrations/003_leads_sample_data.sql +242 -0
  56. package/entities/notes/messages/en.json +114 -0
  57. package/entities/notes/messages/es.json +114 -0
  58. package/entities/notes/migrations/020_notes_table.sql +118 -0
  59. package/entities/notes/migrations/021_notes_metas.sql +114 -0
  60. package/entities/notes/migrations/022_notes_sample_data.sql +275 -0
  61. package/entities/notes/notes.config.ts +61 -0
  62. package/entities/notes/notes.fields.ts +283 -0
  63. package/entities/notes/notes.service.ts +320 -0
  64. package/entities/notes/notes.types.ts +102 -0
  65. package/entities/opportunities/messages/en.json +107 -0
  66. package/entities/opportunities/messages/es.json +107 -0
  67. package/entities/opportunities/migrations/010_opportunities_table.sql +145 -0
  68. package/entities/opportunities/migrations/011_opportunities_metas.sql +114 -0
  69. package/entities/opportunities/migrations/012_opportunities_sample_data.sql +438 -0
  70. package/entities/opportunities/opportunities.config.ts +61 -0
  71. package/entities/opportunities/opportunities.fields.ts +416 -0
  72. package/entities/opportunities/opportunities.service.ts +525 -0
  73. package/entities/opportunities/opportunities.types.ts +135 -0
  74. package/entities/pipelines/messages/en.json +115 -0
  75. package/entities/pipelines/messages/es.json +115 -0
  76. package/entities/pipelines/migrations/001_pipelines_table.sql +106 -0
  77. package/entities/pipelines/migrations/002_pipelines_metas.sql +114 -0
  78. package/entities/pipelines/migrations/003_pipelines_sample_data.sql +91 -0
  79. package/entities/pipelines/pipelines.config.ts +62 -0
  80. package/entities/pipelines/pipelines.fields.ts +193 -0
  81. package/entities/pipelines/pipelines.service.ts +383 -0
  82. package/entities/pipelines/pipelines.types.ts +78 -0
  83. package/entities/products/messages/en.json +135 -0
  84. package/entities/products/messages/es.json +135 -0
  85. package/entities/products/migrations/001_products_table.sql +117 -0
  86. package/entities/products/migrations/002_products_metas.sql +114 -0
  87. package/entities/products/migrations/003_products_sample_data.sql +247 -0
  88. package/entities/products/products.config.ts +62 -0
  89. package/entities/products/products.fields.ts +361 -0
  90. package/entities/products/products.service.ts +437 -0
  91. package/entities/products/products.types.ts +125 -0
  92. package/lib/crm-constants.ts +77 -0
  93. package/lib/crm-utils.ts +185 -0
  94. package/lib/selectors.ts +333 -0
  95. package/messages/en.json +131 -0
  96. package/messages/es.json +131 -0
  97. package/migrations/999_theme_sample_data.sql +473 -0
  98. package/package.json +18 -0
  99. package/pendings.md +205 -0
  100. package/permissions-matrix.md +216 -0
  101. package/styles/components.css +414 -0
  102. package/styles/crm-theme.css +358 -0
  103. package/styles/globals.css +576 -0
  104. package/styles/variables.css +111 -0
  105. package/templates/dashboard/(main)/activities/components/ActivityCard.tsx +169 -0
  106. package/templates/dashboard/(main)/activities/components/ActivityTimeline.tsx +165 -0
  107. package/templates/dashboard/(main)/activities/page.tsx +297 -0
  108. package/templates/dashboard/(main)/campaigns/page.tsx +373 -0
  109. package/templates/dashboard/(main)/companies/page.tsx +296 -0
  110. package/templates/dashboard/(main)/contacts/page.tsx +347 -0
  111. package/templates/dashboard/(main)/layout.tsx +98 -0
  112. package/templates/dashboard/(main)/leads/page.tsx +335 -0
  113. package/templates/dashboard/(main)/opportunities/[id]/edit/page.tsx +95 -0
  114. package/templates/dashboard/(main)/opportunities/create/page.tsx +94 -0
  115. package/templates/dashboard/(main)/opportunities/page.tsx +350 -0
  116. package/templates/dashboard/(main)/pipelines/[id]/edit/page.tsx +95 -0
  117. package/templates/dashboard/(main)/pipelines/[id]/page.tsx +143 -0
  118. package/templates/dashboard/(main)/pipelines/create/page.tsx +94 -0
  119. package/templates/dashboard/(main)/pipelines/page.tsx +234 -0
  120. package/templates/dashboard/(main)/products/[id]/edit/page.tsx +97 -0
  121. package/templates/dashboard/(main)/products/[id]/page.tsx +509 -0
  122. package/templates/dashboard/(main)/products/create/page.tsx +96 -0
  123. package/templates/dashboard/(main)/products/page.tsx +308 -0
  124. package/templates/shared/ActionButtons.tsx +41 -0
  125. package/templates/shared/CRMDashboard.tsx +519 -0
  126. package/templates/shared/CRMDataTable.tsx +441 -0
  127. package/templates/shared/CRMMetricCard.tsx +76 -0
  128. package/templates/shared/CRMMobileNav.tsx +172 -0
  129. package/templates/shared/CRMSidebar.tsx +346 -0
  130. package/templates/shared/CRMTopBar.tsx +265 -0
  131. package/templates/shared/DealCard.tsx +123 -0
  132. package/templates/shared/EntityCard.tsx +58 -0
  133. package/templates/shared/OpportunityForm.tsx +649 -0
  134. package/templates/shared/PipelineForm.tsx +367 -0
  135. package/templates/shared/PipelineKanban.tsx +194 -0
  136. package/templates/shared/QuickFilters.tsx +47 -0
  137. package/templates/shared/StageColumn.tsx +175 -0
  138. package/templates/shared/StageSelect.tsx +177 -0
  139. package/templates/shared/StagesRepeater.tsx +317 -0
  140. 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
+ }