@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,234 @@
1
+ /**
2
+ * Pipelines List Page
3
+ * Professional landing page for sales pipelines
4
+ */
5
+
6
+ 'use client'
7
+
8
+ import { useRouter } from 'next/navigation'
9
+ import { useEffect, useState } from 'react'
10
+ import { Button } from '@nextsparkjs/core/components/ui/button'
11
+ import {
12
+ Plus,
13
+ TrendingUp,
14
+ Target,
15
+ Layers,
16
+ ArrowRight,
17
+ Inbox
18
+ } from 'lucide-react'
19
+ import { fetchWithTeam } from '@nextsparkjs/core/lib/api/entities'
20
+ import { useTeamContext } from '@nextsparkjs/core/contexts/TeamContext'
21
+ import { cn } from '@nextsparkjs/core/lib/utils'
22
+ import { PermissionGate } from '@nextsparkjs/core/components/permissions/PermissionGate'
23
+
24
+ interface Pipeline {
25
+ id: string
26
+ name: string
27
+ description?: string
28
+ stages?: any[]
29
+ isActive?: boolean
30
+ totalValue?: number
31
+ dealCount?: number
32
+ }
33
+
34
+ export default function PipelinesPage() {
35
+ const router = useRouter()
36
+ const { currentTeam, isLoading: teamLoading } = useTeamContext()
37
+ const [pipelines, setPipelines] = useState<Pipeline[]>([])
38
+ const [isLoading, setIsLoading] = useState(true)
39
+
40
+ useEffect(() => {
41
+ if (teamLoading || !currentTeam) return
42
+
43
+ async function fetchPipelines() {
44
+ try {
45
+ const response = await fetchWithTeam('/api/v1/pipelines')
46
+ if (!response.ok) throw new Error('Failed to fetch pipelines')
47
+ const result = await response.json()
48
+ setPipelines(result.data || [])
49
+ } catch (error) {
50
+ console.error('Error loading pipelines:', error)
51
+ } finally {
52
+ setIsLoading(false)
53
+ }
54
+ }
55
+
56
+ fetchPipelines()
57
+ }, [teamLoading, currentTeam])
58
+
59
+ const handlePipelineClick = (pipelineId: string) => {
60
+ router.push(`/dashboard/pipelines/${pipelineId}`)
61
+ }
62
+
63
+ const handleCreatePipeline = () => {
64
+ router.push('/dashboard/pipelines/create')
65
+ }
66
+
67
+ // Stats
68
+ const totalPipelines = pipelines.length
69
+ const activePipelines = pipelines.filter(p => p.isActive !== false).length
70
+
71
+ if (isLoading) {
72
+ return (
73
+ <div className="p-6 space-y-6">
74
+ {/* Header skeleton */}
75
+ <div className="flex items-center justify-between">
76
+ <div className="space-y-2">
77
+ <div className="h-8 w-48 bg-muted animate-pulse rounded-lg" />
78
+ <div className="h-4 w-64 bg-muted animate-pulse rounded-md" />
79
+ </div>
80
+ <div className="h-10 w-36 bg-muted animate-pulse rounded-lg" />
81
+ </div>
82
+
83
+ {/* Stats skeleton */}
84
+ <div className="grid grid-cols-2 gap-4">
85
+ {[1, 2].map(i => (
86
+ <div key={i} className="h-20 bg-muted animate-pulse rounded-xl" />
87
+ ))}
88
+ </div>
89
+
90
+ {/* Cards skeleton */}
91
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
92
+ {[1, 2, 3].map(i => (
93
+ <div key={i} className="h-48 bg-muted animate-pulse rounded-xl" />
94
+ ))}
95
+ </div>
96
+ </div>
97
+ )
98
+ }
99
+
100
+ return (
101
+ <div className="p-6 space-y-6">
102
+ {/* Header */}
103
+ <div className="flex items-start justify-between">
104
+ <div>
105
+ <h1 className="text-2xl font-bold text-foreground tracking-tight">
106
+ Sales Pipelines
107
+ </h1>
108
+ <p className="text-sm text-muted-foreground mt-1">
109
+ Manage your sales pipelines and track opportunities
110
+ </p>
111
+ </div>
112
+ <PermissionGate permission="pipelines.create">
113
+ <Button onClick={handleCreatePipeline} className="gap-2" data-cy="pipelines-add">
114
+ <Plus className="w-4 h-4" />
115
+ New Pipeline
116
+ </Button>
117
+ </PermissionGate>
118
+ </div>
119
+
120
+ {/* Stats */}
121
+ <div className="grid grid-cols-2 gap-4">
122
+ <div className="bg-card border rounded-xl p-4">
123
+ <div className="flex items-center gap-3">
124
+ <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
125
+ <Layers className="w-5 h-5 text-primary" />
126
+ </div>
127
+ <div>
128
+ <p className="text-2xl font-bold text-foreground">{totalPipelines}</p>
129
+ <p className="text-xs text-muted-foreground">Total Pipelines</p>
130
+ </div>
131
+ </div>
132
+ </div>
133
+
134
+ <div className="bg-card border rounded-xl p-4">
135
+ <div className="flex items-center gap-3">
136
+ <div className="w-10 h-10 rounded-lg bg-emerald-500/10 flex items-center justify-center">
137
+ <Target className="w-5 h-5 text-emerald-600" />
138
+ </div>
139
+ <div>
140
+ <p className="text-2xl font-bold text-foreground">{activePipelines}</p>
141
+ <p className="text-xs text-muted-foreground">Active</p>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </div>
146
+
147
+ {/* Pipeline Cards */}
148
+ {pipelines.length > 0 ? (
149
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" data-cy="pipelines-list">
150
+ {pipelines.map((pipeline, index) => (
151
+ <div
152
+ key={pipeline.id}
153
+ data-cy={`pipelines-row-${pipeline.id}`}
154
+ onClick={() => handlePipelineClick(pipeline.id)}
155
+ className={cn(
156
+ 'bg-card border rounded-xl p-5 cursor-pointer transition-all duration-200',
157
+ 'hover:shadow-md hover:border-primary/30 group',
158
+ 'animate-in fade-in slide-in-from-bottom-3'
159
+ )}
160
+ style={{ animationDelay: `${index * 50}ms`, animationFillMode: 'backwards' }}
161
+ >
162
+ {/* Header */}
163
+ <div className="flex items-start justify-between mb-4">
164
+ <div className="flex-1 min-w-0">
165
+ <h3 className="font-semibold text-foreground truncate mb-1">
166
+ {pipeline.name}
167
+ </h3>
168
+ {pipeline.description && (
169
+ <p className="text-sm text-muted-foreground line-clamp-2">
170
+ {pipeline.description}
171
+ </p>
172
+ )}
173
+ </div>
174
+ <span className={cn(
175
+ 'px-2.5 py-1 rounded-md text-xs font-medium shrink-0 ml-3',
176
+ pipeline.isActive !== false
177
+ ? 'bg-emerald-500/10 text-emerald-600'
178
+ : 'bg-muted text-muted-foreground'
179
+ )}>
180
+ {pipeline.isActive !== false ? 'Active' : 'Inactive'}
181
+ </span>
182
+ </div>
183
+
184
+ {/* Stats */}
185
+ <div className="flex items-center gap-4 mb-4">
186
+ <div className="flex items-center gap-2 text-sm">
187
+ <Layers className="w-4 h-4 text-muted-foreground" />
188
+ <span className="text-muted-foreground">
189
+ {pipeline.stages?.length || 0} stages
190
+ </span>
191
+ </div>
192
+ {pipeline.dealCount !== undefined && (
193
+ <div className="flex items-center gap-2 text-sm">
194
+ <Target className="w-4 h-4 text-muted-foreground" />
195
+ <span className="text-muted-foreground">
196
+ {pipeline.dealCount} deals
197
+ </span>
198
+ </div>
199
+ )}
200
+ </div>
201
+
202
+ {/* View button */}
203
+ <div className="flex items-center justify-between pt-4 border-t border-border/50">
204
+ <div className="flex items-center gap-2 text-sm font-medium text-primary group-hover:underline">
205
+ <TrendingUp className="w-4 h-4" />
206
+ View Kanban
207
+ </div>
208
+ <ArrowRight className="w-4 h-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all" />
209
+ </div>
210
+ </div>
211
+ ))}
212
+ </div>
213
+ ) : (
214
+ <div className="flex flex-col items-center justify-center py-16 px-4" data-cy="pipelines-empty">
215
+ <div className="w-16 h-16 rounded-2xl bg-muted flex items-center justify-center mb-4">
216
+ <Inbox className="w-8 h-8 text-muted-foreground" />
217
+ </div>
218
+ <h3 className="text-lg font-semibold text-foreground mb-2">
219
+ No pipelines yet
220
+ </h3>
221
+ <p className="text-sm text-muted-foreground text-center max-w-sm mb-6">
222
+ Create your first sales pipeline to start tracking opportunities and managing your deals.
223
+ </p>
224
+ <PermissionGate permission="pipelines.create">
225
+ <Button onClick={handleCreatePipeline} className="gap-2">
226
+ <Plus className="w-4 h-4" />
227
+ Create Pipeline
228
+ </Button>
229
+ </PermissionGate>
230
+ </div>
231
+ )}
232
+ </div>
233
+ )
234
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Product Edit Page
3
+ * Form for editing existing products - Owner only
4
+ */
5
+
6
+ 'use client'
7
+
8
+ import { useRouter, useParams } from 'next/navigation'
9
+ import { useState, useEffect } from 'react'
10
+ import { Button } from '@nextsparkjs/core/components/ui/button'
11
+ import { EntityFormWrapper } from '@nextsparkjs/core/components/entities/wrappers/EntityFormWrapper'
12
+ import { useTeamContext } from '@nextsparkjs/core/contexts/TeamContext'
13
+ import { usePermission } from '@nextsparkjs/core/lib/permissions/hooks'
14
+ import { ShieldAlert, ArrowLeft } from 'lucide-react'
15
+
16
+ // Access Denied component for when user doesn't have permission
17
+ function AccessDeniedView({
18
+ title = 'Access Denied',
19
+ message = "You don't have permission to perform this action",
20
+ backUrl = '/dashboard/products'
21
+ }: {
22
+ title?: string
23
+ message?: string
24
+ backUrl?: string
25
+ }) {
26
+ const router = useRouter()
27
+
28
+ return (
29
+ <div className="flex flex-col items-center justify-center min-h-[400px] gap-4 p-6">
30
+ <div className="w-16 h-16 rounded-full bg-destructive/10 flex items-center justify-center">
31
+ <ShieldAlert className="w-8 h-8 text-destructive" />
32
+ </div>
33
+ <div className="text-center space-y-2">
34
+ <h2 className="text-xl font-semibold">{title}</h2>
35
+ <p className="text-sm text-muted-foreground max-w-md">{message}</p>
36
+ </div>
37
+ <Button variant="outline" onClick={() => router.push(backUrl)}>
38
+ <ArrowLeft className="w-4 h-4 mr-2" />
39
+ Back to Products
40
+ </Button>
41
+ </div>
42
+ )
43
+ }
44
+
45
+ export default function ProductEditPage() {
46
+ const router = useRouter()
47
+ const params = useParams()
48
+ const productId = params.id as string
49
+
50
+ const { currentTeam, isLoading: teamLoading } = useTeamContext()
51
+ const [permissionChecked, setPermissionChecked] = useState(false)
52
+
53
+ // Permission check - only owner can update products
54
+ const canUpdate = usePermission('products.update')
55
+
56
+ // Wait for team context to load before checking permissions
57
+ useEffect(() => {
58
+ if (!teamLoading && currentTeam) {
59
+ setPermissionChecked(true)
60
+ }
61
+ }, [teamLoading, currentTeam])
62
+
63
+ // Loading state while checking permissions
64
+ if (!permissionChecked) {
65
+ return (
66
+ <div className="flex items-center justify-center min-h-[400px]">
67
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
68
+ </div>
69
+ )
70
+ }
71
+
72
+ // Permission denied - show access denied page
73
+ if (!canUpdate) {
74
+ return (
75
+ <AccessDeniedView
76
+ title="Cannot Edit Product"
77
+ message="Only the team owner can edit products in the catalog. Please contact your team owner if you need to make changes."
78
+ backUrl={`/dashboard/products/${productId}`}
79
+ />
80
+ )
81
+ }
82
+
83
+ // Has permission - show the form
84
+ return (
85
+ <EntityFormWrapper
86
+ entityType="products"
87
+ mode="edit"
88
+ id={productId}
89
+ onSuccess={() => {
90
+ router.push(`/dashboard/products/${productId}`)
91
+ }}
92
+ onError={(error) => {
93
+ console.error('Error updating product:', error)
94
+ }}
95
+ />
96
+ )
97
+ }