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