@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,308 @@
1
+ /**
2
+ * Products Page
3
+ * Professional products catalog management with data table
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
+ Package,
15
+ Trash2,
16
+ Download,
17
+ DollarSign,
18
+ Tag,
19
+ Archive,
20
+ TrendingUp,
21
+ Percent
22
+ } from 'lucide-react'
23
+ import { fetchWithTeam } from '@nextsparkjs/core/lib/api/entities'
24
+ import { useTeamContext } from '@nextsparkjs/core/contexts/TeamContext'
25
+ import { cn } from '@nextsparkjs/core/lib/utils'
26
+ import { PermissionGate } from '@nextsparkjs/core/components/permissions/PermissionGate'
27
+ import { usePermission } from '@nextsparkjs/core/lib/permissions/hooks'
28
+
29
+ interface Product {
30
+ id: string
31
+ name: string
32
+ description?: string
33
+ sku?: string
34
+ category?: string
35
+ price: number
36
+ cost?: number
37
+ status: 'active' | 'inactive' | 'discontinued'
38
+ stock?: number
39
+ salesCount?: number
40
+ createdAt: string
41
+ }
42
+
43
+ // Status badge component
44
+ function StatusBadge({ status }: { status: Product['status'] }) {
45
+ const config = {
46
+ active: { label: 'Active', className: 'bg-emerald-500/10 text-emerald-600' },
47
+ inactive: { label: 'Inactive', className: 'bg-amber-500/10 text-amber-600' },
48
+ discontinued: { label: 'Discontinued', className: 'bg-muted text-muted-foreground' },
49
+ }
50
+
51
+ const { label, className } = config[status] || config.active
52
+
53
+ return (
54
+ <span className={cn('px-2.5 py-1 rounded-md text-xs font-medium', className)}>
55
+ {label}
56
+ </span>
57
+ )
58
+ }
59
+
60
+ // Margin indicator
61
+ function MarginIndicator({ price, cost }: { price: number; cost?: number }) {
62
+ if (!cost || cost === 0) return <span className="text-muted-foreground">-</span>
63
+
64
+ const margin = ((price - cost) / price) * 100
65
+
66
+ let colorClass = 'text-muted-foreground'
67
+ if (margin >= 50) colorClass = 'text-emerald-600'
68
+ else if (margin >= 30) colorClass = 'text-amber-600'
69
+ else if (margin >= 0) colorClass = 'text-orange-600'
70
+ else colorClass = 'text-destructive'
71
+
72
+ return (
73
+ <div className={cn('flex items-center gap-1.5 text-sm font-medium', colorClass)}>
74
+ <Percent className="w-3.5 h-3.5" />
75
+ <span>{margin.toFixed(1)}%</span>
76
+ </div>
77
+ )
78
+ }
79
+
80
+ export default function ProductsPage() {
81
+ const router = useRouter()
82
+ const { currentTeam, isLoading: teamLoading } = useTeamContext()
83
+ const [products, setProducts] = useState<Product[]>([])
84
+ const [isLoading, setIsLoading] = useState(true)
85
+
86
+ // Permission checks for bulk actions
87
+ const canDeleteProducts = usePermission('products.delete')
88
+ const canUpdateProducts = usePermission('products.update')
89
+
90
+ useEffect(() => {
91
+ if (teamLoading || !currentTeam) return
92
+
93
+ async function fetchProducts() {
94
+ try {
95
+ const response = await fetchWithTeam('/api/v1/products')
96
+ if (!response.ok) throw new Error('Failed to fetch products')
97
+ const result = await response.json()
98
+ setProducts(result.data || [])
99
+ } catch (error) {
100
+ console.error('Error loading products:', error)
101
+ } finally {
102
+ setIsLoading(false)
103
+ }
104
+ }
105
+
106
+ fetchProducts()
107
+ }, [teamLoading, currentTeam])
108
+
109
+ // Stats
110
+ const stats = useMemo(() => {
111
+ const active = products.filter(p => p.status === 'active').length
112
+ const totalRevenue = products.reduce((sum, p) => sum + (p.price * (p.salesCount || 0)), 0)
113
+ const avgPrice = products.length > 0
114
+ ? products.reduce((sum, p) => sum + p.price, 0) / products.length
115
+ : 0
116
+
117
+ return {
118
+ total: products.length,
119
+ active,
120
+ totalRevenue,
121
+ avgPrice,
122
+ }
123
+ }, [products])
124
+
125
+ const formatCurrency = (value: number) => {
126
+ return new Intl.NumberFormat('en-US', {
127
+ style: 'currency',
128
+ currency: 'USD',
129
+ minimumFractionDigits: 0,
130
+ maximumFractionDigits: 0,
131
+ }).format(value)
132
+ }
133
+
134
+ // Column definitions
135
+ const columns: Column<Product>[] = [
136
+ {
137
+ key: 'name',
138
+ header: 'Product',
139
+ sortable: true,
140
+ render: (_, product) => (
141
+ <div className="flex items-center gap-3">
142
+ <div className="w-9 h-9 rounded-lg bg-primary/10 flex items-center justify-center">
143
+ <Package className="w-4 h-4 text-primary" />
144
+ </div>
145
+ <div>
146
+ <p className="font-medium text-foreground">{product.name}</p>
147
+ {product.sku && (
148
+ <p className="text-xs text-muted-foreground">SKU: {product.sku}</p>
149
+ )}
150
+ </div>
151
+ </div>
152
+ ),
153
+ },
154
+ {
155
+ key: 'category',
156
+ header: 'Category',
157
+ sortable: true,
158
+ render: (value) => value ? (
159
+ <div className="flex items-center gap-1.5 text-sm">
160
+ <Tag className="w-3.5 h-3.5 text-muted-foreground" />
161
+ <span>{value}</span>
162
+ </div>
163
+ ) : <span className="text-muted-foreground">-</span>,
164
+ },
165
+ {
166
+ key: 'price',
167
+ header: 'Price',
168
+ sortable: true,
169
+ render: (value) => (
170
+ <span className="font-medium text-primary">
171
+ {formatCurrency(value)}
172
+ </span>
173
+ ),
174
+ },
175
+ {
176
+ key: 'margin',
177
+ header: 'Margin',
178
+ render: (_, product) => (
179
+ <MarginIndicator price={product.price} cost={product.cost} />
180
+ ),
181
+ },
182
+ {
183
+ key: 'salesCount',
184
+ header: 'Sales',
185
+ sortable: true,
186
+ render: (value) => (
187
+ <div className="flex items-center gap-1.5 text-sm">
188
+ <TrendingUp className="w-3.5 h-3.5 text-muted-foreground" />
189
+ <span>{value || 0}</span>
190
+ </div>
191
+ ),
192
+ },
193
+ {
194
+ key: 'status',
195
+ header: 'Status',
196
+ sortable: true,
197
+ render: (value) => <StatusBadge status={value} />,
198
+ },
199
+ ]
200
+
201
+ // Bulk actions - filtered by permissions
202
+ const bulkActions: BulkAction[] = [
203
+ {
204
+ id: 'export',
205
+ label: 'Export',
206
+ icon: <Download className="w-4 h-4" />,
207
+ onClick: (ids) => console.log('Export:', ids),
208
+ },
209
+ // Only show archive action if user has update permission
210
+ ...(canUpdateProducts ? [{
211
+ id: 'archive',
212
+ label: 'Archive',
213
+ icon: <Archive className="w-4 h-4" />,
214
+ onClick: (ids: string[]) => { if (confirm(`Archive ${ids.length} products?`)) console.log('Archive:', ids) },
215
+ }] : []),
216
+ // Only show delete action if user has permission
217
+ ...(canDeleteProducts ? [{
218
+ id: 'delete',
219
+ label: 'Delete',
220
+ icon: <Trash2 className="w-4 h-4" />,
221
+ variant: 'destructive' as const,
222
+ onClick: (ids: string[]) => { if (confirm(`Delete ${ids.length} products?`)) console.log('Delete:', ids) },
223
+ }] : []),
224
+ ]
225
+
226
+ return (
227
+ <div className="p-6 space-y-6">
228
+ {/* Header */}
229
+ <div className="flex items-start justify-between">
230
+ <div>
231
+ <h1 className="text-2xl font-bold text-foreground tracking-tight">Products</h1>
232
+ <p className="text-sm text-muted-foreground mt-1">
233
+ Manage your product catalog and pricing
234
+ </p>
235
+ </div>
236
+ <PermissionGate permission="products.create">
237
+ <Button onClick={() => router.push('/dashboard/products/create')} className="gap-2" data-cy="products-add">
238
+ <Plus className="w-4 h-4" />
239
+ Add Product
240
+ </Button>
241
+ </PermissionGate>
242
+ </div>
243
+
244
+ {/* Stats */}
245
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
246
+ <div className="bg-card border rounded-xl p-4">
247
+ <div className="flex items-center gap-3">
248
+ <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
249
+ <Package className="w-5 h-5 text-primary" />
250
+ </div>
251
+ <div>
252
+ <p className="text-2xl font-bold text-foreground">{stats.total}</p>
253
+ <p className="text-xs text-muted-foreground">Products</p>
254
+ </div>
255
+ </div>
256
+ </div>
257
+ <div className="bg-card border rounded-xl p-4">
258
+ <div className="flex items-center gap-3">
259
+ <div className="w-10 h-10 rounded-lg bg-emerald-500/10 flex items-center justify-center">
260
+ <TrendingUp className="w-5 h-5 text-emerald-600" />
261
+ </div>
262
+ <div>
263
+ <p className="text-2xl font-bold text-foreground">{stats.active}</p>
264
+ <p className="text-xs text-muted-foreground">Active</p>
265
+ </div>
266
+ </div>
267
+ </div>
268
+ <div className="bg-card border rounded-xl p-4">
269
+ <div className="flex items-center gap-3">
270
+ <div className="w-10 h-10 rounded-lg bg-amber-500/10 flex items-center justify-center">
271
+ <DollarSign className="w-5 h-5 text-amber-600" />
272
+ </div>
273
+ <div>
274
+ <p className="text-2xl font-bold text-foreground">{formatCurrency(stats.totalRevenue)}</p>
275
+ <p className="text-xs text-muted-foreground">Total Revenue</p>
276
+ </div>
277
+ </div>
278
+ </div>
279
+ <div className="bg-card border rounded-xl p-4">
280
+ <div className="flex items-center gap-3">
281
+ <div className="w-10 h-10 rounded-lg bg-muted flex items-center justify-center">
282
+ <Tag className="w-5 h-5 text-muted-foreground" />
283
+ </div>
284
+ <div>
285
+ <p className="text-2xl font-bold text-foreground">{formatCurrency(stats.avgPrice)}</p>
286
+ <p className="text-xs text-muted-foreground">Avg. Price</p>
287
+ </div>
288
+ </div>
289
+ </div>
290
+ </div>
291
+
292
+ {/* Data Table */}
293
+ <CRMDataTable
294
+ data={products}
295
+ columns={columns}
296
+ bulkActions={bulkActions}
297
+ onRowClick={(p) => router.push(`/dashboard/products/${p.id}`)}
298
+ isLoading={isLoading}
299
+ searchPlaceholder="Search products..."
300
+ searchFields={['name', 'sku', 'category']}
301
+ pageSize={15}
302
+ emptyMessage="No products yet"
303
+ emptyDescription="Start adding products to build your catalog."
304
+ entitySlug="products"
305
+ />
306
+ </div>
307
+ )
308
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Action Buttons Component
3
+ * Reusable action button group for entities
4
+ */
5
+
6
+ import React from 'react'
7
+ import { Button } from '@nextsparkjs/core/components/ui/button'
8
+
9
+ export interface ActionButton {
10
+ label: string
11
+ onClick: () => void
12
+ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
13
+ icon?: React.ReactNode
14
+ disabled?: boolean
15
+ }
16
+
17
+ interface ActionButtonsProps {
18
+ actions: ActionButton[]
19
+ className?: string
20
+ }
21
+
22
+ export function ActionButtons({ actions, className = '' }: ActionButtonsProps) {
23
+ return (
24
+ <div className={`flex gap-2 ${className}`}>
25
+ {actions.map((action, index) => (
26
+ <Button
27
+ key={index}
28
+ onClick={action.onClick}
29
+ variant={action.variant || 'default'}
30
+ disabled={action.disabled}
31
+ className="gap-2"
32
+ >
33
+ {action.icon}
34
+ {action.label}
35
+ </Button>
36
+ ))}
37
+ </div>
38
+ )
39
+ }
40
+
41
+ export default ActionButtons