@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,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
|
+
}
|