@fayz-ai/plugin-shop 0.2.0
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/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/ShopPage-65PPVKBD.js +271 -0
- package/dist/ShopPage-65PPVKBD.js.map +1 -0
- package/dist/ShopPage-PG5GKOZU.cjs +273 -0
- package/dist/ShopPage-PG5GKOZU.cjs.map +1 -0
- package/dist/ShopPage.d.ts +8 -0
- package/dist/ShopPage.d.ts.map +1 -0
- package/dist/index.cjs +119 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +113 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
- package/src/ShopPage.tsx +323 -0
- package/src/index.ts +137 -0
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fayz-ai/plugin-shop",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Fayz SDK — plugin-shop plugin",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"source": "./src/index.ts",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
23
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@tanstack/react-table": "^8.20.0",
|
|
27
|
+
"lucide-react": "^0.400.0",
|
|
28
|
+
"zustand": "^4.5.0",
|
|
29
|
+
"@fayz-ai/core": "^0.2.0",
|
|
30
|
+
"@fayz-ai/shop": "^0.2.0",
|
|
31
|
+
"@fayz-ai/saas": "^0.2.0",
|
|
32
|
+
"@fayz-ai/ui": "^0.2.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/react": "^18.3.0",
|
|
36
|
+
"react": "^18.3.0",
|
|
37
|
+
"tsup": "^8.2.0",
|
|
38
|
+
"typescript": "^5.5.0"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"keywords": [
|
|
42
|
+
"fayz",
|
|
43
|
+
"fayz-plugin",
|
|
44
|
+
"fayz-sdk"
|
|
45
|
+
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsup && tsc --emitDeclarationOnly --declaration --declarationMap --noEmit false",
|
|
51
|
+
"dev": "tsup --watch",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"clean": "rm -rf dist"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/ShopPage.tsx
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import React, { useEffect, useState, useCallback, useMemo } from 'react'
|
|
2
|
+
import type { ColumnDef } from '@tanstack/react-table'
|
|
3
|
+
import { DataTable } from '@fayz-ai/ui'
|
|
4
|
+
import { getShopProvider } from '@fayz-ai/shop'
|
|
5
|
+
import type { Product, Order, ShopCustomer, Discount, ListProductsOptions, ShopProvider } from '@fayz-ai/shop'
|
|
6
|
+
|
|
7
|
+
export type ShopProviderResolver = ShopProvider | (() => ShopProvider)
|
|
8
|
+
|
|
9
|
+
export interface ShopPageProps {
|
|
10
|
+
provider?: ShopProviderResolver
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Shared helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
function currency(value: number, code = 'BRL', locale = 'pt-BR') {
|
|
18
|
+
return new Intl.NumberFormat(locale, { style: 'currency', currency: code }).format(value)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function badge(label: string, color: string) {
|
|
22
|
+
return (
|
|
23
|
+
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${color}`}>
|
|
24
|
+
{label}
|
|
25
|
+
</span>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const statusColors: Record<string, string> = {
|
|
30
|
+
active: 'bg-emerald-100 text-emerald-800',
|
|
31
|
+
draft: 'bg-gray-100 text-gray-700',
|
|
32
|
+
archived: 'bg-orange-100 text-orange-700',
|
|
33
|
+
open: 'bg-blue-100 text-blue-800',
|
|
34
|
+
cancelled: 'bg-red-100 text-red-700',
|
|
35
|
+
paid: 'bg-emerald-100 text-emerald-800',
|
|
36
|
+
pending: 'bg-yellow-100 text-yellow-800',
|
|
37
|
+
fulfilled: 'bg-emerald-100 text-emerald-800',
|
|
38
|
+
unfulfilled: 'bg-gray-100 text-gray-700',
|
|
39
|
+
'partially_fulfilled': 'bg-blue-100 text-blue-800',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveShopProvider(provider?: ShopProviderResolver): ShopProvider {
|
|
43
|
+
return typeof provider === 'function' ? provider() : provider ?? getShopProvider()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Products tab
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
function ProductsTab({ provider }: ShopPageProps) {
|
|
51
|
+
const [products, setProducts] = useState<Product[]>([])
|
|
52
|
+
const [loading, setLoading] = useState(true)
|
|
53
|
+
const [search, setSearch] = useState('')
|
|
54
|
+
|
|
55
|
+
const load = useCallback(async (opts?: ListProductsOptions) => {
|
|
56
|
+
setLoading(true)
|
|
57
|
+
try {
|
|
58
|
+
const data = await resolveShopProvider(provider).listProducts(opts)
|
|
59
|
+
setProducts(data)
|
|
60
|
+
} finally {
|
|
61
|
+
setLoading(false)
|
|
62
|
+
}
|
|
63
|
+
}, [provider])
|
|
64
|
+
|
|
65
|
+
useEffect(() => { load() }, [load])
|
|
66
|
+
|
|
67
|
+
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
68
|
+
const q = e.target.value
|
|
69
|
+
setSearch(q)
|
|
70
|
+
load(q ? { search: q } : undefined)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const columns = useMemo<ColumnDef<Product, any>[]>(() => [
|
|
74
|
+
{ accessorKey: 'name', header: 'Produto', cell: ({ getValue }) => <span className="font-medium">{getValue() as string}</span> },
|
|
75
|
+
{ accessorKey: 'sku', header: 'SKU', cell: ({ getValue }) => <span className="text-muted-foreground">{(getValue() as string | undefined) ?? '—'}</span> },
|
|
76
|
+
{
|
|
77
|
+
accessorKey: 'price', header: () => <span className="block text-right">Preço</span>,
|
|
78
|
+
cell: ({ getValue, row }) => <div className="text-right">{currency(getValue() as number, row.original.currency)}</div>,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
accessorKey: 'inventoryCount', header: () => <span className="block text-right">Estoque</span>,
|
|
82
|
+
cell: ({ getValue }) => {
|
|
83
|
+
const count = getValue() as number
|
|
84
|
+
return <div className="text-right"><span className={count === 0 ? 'text-red-600 font-medium' : ''}>{count}</span></div>
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
accessorKey: 'status', header: 'Status',
|
|
89
|
+
cell: ({ getValue }) => {
|
|
90
|
+
const status = getValue() as string
|
|
91
|
+
return badge(status, statusColors[status] ?? 'bg-gray-100 text-gray-700')
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
], [])
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div>
|
|
98
|
+
<div className="flex items-center justify-between mb-4">
|
|
99
|
+
<input
|
|
100
|
+
type="search" value={search} onChange={handleSearch}
|
|
101
|
+
placeholder="Buscar produtos..."
|
|
102
|
+
className="border rounded-lg px-3 py-2 text-sm w-64 focus:outline-none focus:ring-2 focus:ring-primary/50"
|
|
103
|
+
/>
|
|
104
|
+
<span className="text-sm text-muted-foreground">{products.length} produtos</span>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{loading ? (
|
|
108
|
+
<div className="py-12 text-center text-sm text-muted-foreground">Carregando...</div>
|
|
109
|
+
) : (
|
|
110
|
+
<DataTable
|
|
111
|
+
columns={columns}
|
|
112
|
+
data={products}
|
|
113
|
+
variant="card"
|
|
114
|
+
emptyMessage="Nenhum produto encontrado."
|
|
115
|
+
/>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Orders tab
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
function OrdersTab({ provider }: ShopPageProps) {
|
|
126
|
+
const [orders, setOrders] = useState<Order[]>([])
|
|
127
|
+
const [loading, setLoading] = useState(true)
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
resolveShopProvider(provider).listOrders({ limit: 50 }).then(setOrders).finally(() => setLoading(false))
|
|
131
|
+
}, [provider])
|
|
132
|
+
|
|
133
|
+
const columns = useMemo<ColumnDef<Order, any>[]>(() => [
|
|
134
|
+
{ accessorKey: 'orderNumber', header: 'Pedido', cell: ({ getValue }) => <span className="font-medium">#{getValue() as string}</span> },
|
|
135
|
+
{
|
|
136
|
+
id: 'customer', accessorFn: (o) => o.customerName ?? o.customerEmail ?? '—', header: 'Cliente',
|
|
137
|
+
cell: ({ getValue }) => <span>{getValue() as string}</span>,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
accessorKey: 'total', header: () => <span className="block text-right">Total</span>,
|
|
141
|
+
cell: ({ getValue, row }) => <div className="text-right font-medium">{currency(getValue() as number, row.original.currency)}</div>,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
accessorKey: 'financialStatus', header: 'Pagamento',
|
|
145
|
+
cell: ({ getValue }) => {
|
|
146
|
+
const status = getValue() as string
|
|
147
|
+
return badge(status, statusColors[status] ?? 'bg-gray-100 text-gray-700')
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
accessorKey: 'fulfillmentStatus', header: 'Entrega',
|
|
152
|
+
cell: ({ getValue }) => {
|
|
153
|
+
const status = getValue() as string
|
|
154
|
+
return badge(status, statusColors[status] ?? 'bg-gray-100 text-gray-700')
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
accessorKey: 'createdAt', header: 'Data',
|
|
159
|
+
cell: ({ getValue }) => <span className="text-muted-foreground">{new Date(getValue() as string).toLocaleDateString('pt-BR')}</span>,
|
|
160
|
+
},
|
|
161
|
+
], [])
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div>
|
|
165
|
+
{loading ? (
|
|
166
|
+
<div className="py-12 text-center text-sm text-muted-foreground">Carregando...</div>
|
|
167
|
+
) : (
|
|
168
|
+
<DataTable
|
|
169
|
+
columns={columns}
|
|
170
|
+
data={orders}
|
|
171
|
+
variant="card"
|
|
172
|
+
emptyMessage="Nenhum pedido ainda."
|
|
173
|
+
/>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Customers tab
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
function CustomersTab({ provider }: ShopPageProps) {
|
|
184
|
+
const [customers, setCustomers] = useState<ShopCustomer[]>([])
|
|
185
|
+
const [loading, setLoading] = useState(true)
|
|
186
|
+
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
resolveShopProvider(provider).listCustomers({ limit: 50 }).then(setCustomers).finally(() => setLoading(false))
|
|
189
|
+
}, [provider])
|
|
190
|
+
|
|
191
|
+
const columns = useMemo<ColumnDef<ShopCustomer, any>[]>(() => [
|
|
192
|
+
{
|
|
193
|
+
id: 'name', accessorFn: (c) => `${c.firstName} ${c.lastName}`, header: 'Nome',
|
|
194
|
+
cell: ({ row }) => <span className="font-medium">{row.original.firstName} {row.original.lastName}</span>,
|
|
195
|
+
},
|
|
196
|
+
{ accessorKey: 'email', header: 'Email', cell: ({ getValue }) => <span className="text-muted-foreground">{(getValue() as string | undefined) ?? '—'}</span> },
|
|
197
|
+
{ accessorKey: 'phone', header: 'Telefone', cell: ({ getValue }) => <span className="text-muted-foreground">{(getValue() as string | undefined) ?? '—'}</span> },
|
|
198
|
+
{
|
|
199
|
+
accessorKey: 'ordersCount', header: () => <span className="block text-right">Pedidos</span>,
|
|
200
|
+
cell: ({ getValue }) => <div className="text-right">{getValue() as number}</div>,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
accessorKey: 'totalSpent', header: () => <span className="block text-right">Total Gasto</span>,
|
|
204
|
+
cell: ({ getValue }) => <div className="text-right">{currency(getValue() as number)}</div>,
|
|
205
|
+
},
|
|
206
|
+
], [])
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div>
|
|
210
|
+
{loading ? (
|
|
211
|
+
<div className="py-12 text-center text-sm text-muted-foreground">Carregando...</div>
|
|
212
|
+
) : (
|
|
213
|
+
<DataTable
|
|
214
|
+
columns={columns}
|
|
215
|
+
data={customers}
|
|
216
|
+
variant="card"
|
|
217
|
+
emptyMessage="Nenhum cliente ainda."
|
|
218
|
+
/>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Discounts tab
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
function DiscountsTab({ provider }: ShopPageProps) {
|
|
229
|
+
const [discounts, setDiscounts] = useState<Discount[]>([])
|
|
230
|
+
const [loading, setLoading] = useState(true)
|
|
231
|
+
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
resolveShopProvider(provider).listDiscounts({ limit: 50 }).then(setDiscounts).finally(() => setLoading(false))
|
|
234
|
+
}, [provider])
|
|
235
|
+
|
|
236
|
+
const columns = useMemo<ColumnDef<Discount, any>[]>(() => [
|
|
237
|
+
{ accessorKey: 'title', header: 'Título', cell: ({ getValue }) => <span className="font-medium">{getValue() as string}</span> },
|
|
238
|
+
{ accessorKey: 'code', header: 'Código', cell: ({ getValue }) => <span className="font-mono text-sm">{(getValue() as string | undefined) ?? '—'}</span> },
|
|
239
|
+
{ accessorKey: 'type', header: 'Tipo', cell: ({ getValue }) => <span className="text-muted-foreground">{getValue() as string}</span> },
|
|
240
|
+
{
|
|
241
|
+
accessorKey: 'value', header: () => <span className="block text-right">Valor</span>,
|
|
242
|
+
cell: ({ row }) => <div className="text-right">{row.original.type === 'percentage' ? `${row.original.value}%` : currency(row.original.value)}</div>,
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: 'usos', accessorKey: 'timesUsed', header: () => <span className="block text-right">Usos</span>,
|
|
246
|
+
cell: ({ row }) => <div className="text-right">{row.original.timesUsed}{row.original.usageLimit ? ` / ${row.original.usageLimit}` : ''}</div>,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
accessorKey: 'status', header: 'Status',
|
|
250
|
+
cell: ({ getValue }) => {
|
|
251
|
+
const status = getValue() as string
|
|
252
|
+
return badge(status, statusColors[status] ?? 'bg-gray-100 text-gray-700')
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
], [])
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<div>
|
|
259
|
+
{loading ? (
|
|
260
|
+
<div className="py-12 text-center text-sm text-muted-foreground">Carregando...</div>
|
|
261
|
+
) : (
|
|
262
|
+
<DataTable
|
|
263
|
+
columns={columns}
|
|
264
|
+
data={discounts}
|
|
265
|
+
variant="card"
|
|
266
|
+
emptyMessage="Nenhum desconto cadastrado."
|
|
267
|
+
/>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// Main page
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
const TABS = [
|
|
278
|
+
{ id: 'products', label: 'Produtos' },
|
|
279
|
+
{ id: 'orders', label: 'Pedidos' },
|
|
280
|
+
{ id: 'customers', label: 'Clientes' },
|
|
281
|
+
{ id: 'discounts', label: 'Descontos' },
|
|
282
|
+
] as const
|
|
283
|
+
|
|
284
|
+
type TabId = typeof TABS[number]['id']
|
|
285
|
+
|
|
286
|
+
export const ShopPage: React.FC<ShopPageProps> = ({ provider }) => {
|
|
287
|
+
const [tab, setTab] = useState<TabId>('products')
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<div className="p-6 max-w-7xl mx-auto">
|
|
291
|
+
<div className="mb-6">
|
|
292
|
+
<h1 className="text-2xl font-semibold">Loja</h1>
|
|
293
|
+
<p className="text-muted-foreground mt-1">Gerencie produtos, pedidos, clientes e descontos.</p>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
{/* Tabs */}
|
|
297
|
+
<div className="border-b mb-6">
|
|
298
|
+
<nav className="flex gap-1">
|
|
299
|
+
{TABS.map(t => (
|
|
300
|
+
<button
|
|
301
|
+
key={t.id}
|
|
302
|
+
onClick={() => setTab(t.id)}
|
|
303
|
+
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
|
304
|
+
tab === t.id
|
|
305
|
+
? 'border-primary text-primary'
|
|
306
|
+
: 'border-transparent text-muted-foreground hover:text-foreground'
|
|
307
|
+
}`}
|
|
308
|
+
>
|
|
309
|
+
{t.label}
|
|
310
|
+
</button>
|
|
311
|
+
))}
|
|
312
|
+
</nav>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
{tab === 'products' && <ProductsTab provider={provider} />}
|
|
316
|
+
{tab === 'orders' && <OrdersTab provider={provider} />}
|
|
317
|
+
{tab === 'customers' && <CustomersTab provider={provider} />}
|
|
318
|
+
{tab === 'discounts' && <DiscountsTab provider={provider} />}
|
|
319
|
+
</div>
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
ShopPage.displayName = 'ShopPage'
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'
|
|
3
|
+
import { setShopTenantResolver } from '@fayz-ai/shop'
|
|
4
|
+
import { useOrganizationStore } from '@fayz-ai/saas'
|
|
5
|
+
import type { ShopProviderResolver } from './ShopPage'
|
|
6
|
+
|
|
7
|
+
const ShopPage = React.lazy(() => import('./ShopPage').then((m) => ({ default: m.ShopPage })))
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Options
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export interface ShopPluginOptions {
|
|
14
|
+
navPosition?: number
|
|
15
|
+
navSection?: 'main' | 'secondary' | 'settings'
|
|
16
|
+
navLabel?: string
|
|
17
|
+
scope?: PluginScope
|
|
18
|
+
verticalId?: VerticalId
|
|
19
|
+
currency?: { code?: string; locale?: string; symbol?: string }
|
|
20
|
+
provider?: ShopProviderResolver
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Factory
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export function createShopPlugin(options?: ShopPluginOptions): PluginManifest {
|
|
28
|
+
const curr = { code: 'BRL', locale: 'pt-BR', symbol: 'R$', ...options?.currency }
|
|
29
|
+
|
|
30
|
+
// Scope fallback shop queries to the active organization. Apps can inject a
|
|
31
|
+
// provider when they want SDK-owned data access instead of the global runtime.
|
|
32
|
+
setShopTenantResolver(() => useOrganizationStore.getState().currentOrg?.id)
|
|
33
|
+
|
|
34
|
+
const PageComponent: React.ComponentType<unknown> = () =>
|
|
35
|
+
React.createElement(React.Suspense, { fallback: null },
|
|
36
|
+
React.createElement(ShopPage as React.ComponentType<{ provider?: ShopProviderResolver }>, {
|
|
37
|
+
provider: options?.provider,
|
|
38
|
+
})
|
|
39
|
+
)
|
|
40
|
+
PageComponent.displayName = 'ShopPageWrapper'
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
id: 'shop',
|
|
44
|
+
name: options?.navLabel ?? 'Shop',
|
|
45
|
+
icon: 'ShoppingBag',
|
|
46
|
+
version: '1.0.0',
|
|
47
|
+
scope: options?.scope ?? 'vertical',
|
|
48
|
+
verticalId: options?.verticalId,
|
|
49
|
+
defaultEnabled: true,
|
|
50
|
+
dependencies: [],
|
|
51
|
+
navigation: [
|
|
52
|
+
{
|
|
53
|
+
section: options?.navSection ?? 'main',
|
|
54
|
+
position: options?.navPosition ?? 6,
|
|
55
|
+
label: options?.navLabel ?? 'Shop',
|
|
56
|
+
route: '/shop',
|
|
57
|
+
icon: 'ShoppingBag',
|
|
58
|
+
permission: { feature: 'shop', action: 'read' as const },
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
routes: [
|
|
62
|
+
{
|
|
63
|
+
path: '/shop',
|
|
64
|
+
component: PageComponent,
|
|
65
|
+
permission: { feature: 'shop', action: 'read' as const },
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
widgets: [],
|
|
69
|
+
aiTools: [
|
|
70
|
+
{
|
|
71
|
+
id: 'shop.list-products',
|
|
72
|
+
name: 'listProducts',
|
|
73
|
+
description: 'Lists products in the shop catalog.',
|
|
74
|
+
icon: 'Package',
|
|
75
|
+
mode: 'read' as const,
|
|
76
|
+
category: 'Shop',
|
|
77
|
+
parameters: {
|
|
78
|
+
type: 'object' as const,
|
|
79
|
+
properties: {
|
|
80
|
+
search: { type: 'string' as const, description: 'Filter by name' },
|
|
81
|
+
status: { type: 'string' as const, enum: ['draft', 'active', 'archived'] },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
suggestions: [
|
|
85
|
+
{ label: 'What products are available?' },
|
|
86
|
+
{ label: 'Which products are out of stock?' },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'shop.list-orders',
|
|
91
|
+
name: 'listOrders',
|
|
92
|
+
description: 'Lists recent shop orders.',
|
|
93
|
+
icon: 'ShoppingCart',
|
|
94
|
+
mode: 'read' as const,
|
|
95
|
+
category: 'Shop',
|
|
96
|
+
parameters: {
|
|
97
|
+
type: 'object' as const,
|
|
98
|
+
properties: {
|
|
99
|
+
financialStatus: { type: 'string' as const, enum: ['pending', 'paid', 'refunded', 'voided'] },
|
|
100
|
+
fulfillmentStatus: { type: 'string' as const, enum: ['unfulfilled', 'partially_fulfilled', 'fulfilled'] },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
suggestions: [
|
|
104
|
+
{ label: 'Show me pending orders' },
|
|
105
|
+
{ label: 'How many orders were paid today?' },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
settings: [
|
|
110
|
+
{
|
|
111
|
+
id: 'shop',
|
|
112
|
+
label: 'Shop',
|
|
113
|
+
icon: 'ShoppingBag',
|
|
114
|
+
component: (() => {
|
|
115
|
+
const ShopSettingsTab: React.ComponentType<unknown> = () =>
|
|
116
|
+
React.createElement('div', { className: 'p-4 space-y-2' },
|
|
117
|
+
React.createElement('h2', { className: 'text-lg font-semibold' }, 'Configurações da Loja'),
|
|
118
|
+
React.createElement('p', { className: 'text-sm text-muted-foreground' },
|
|
119
|
+
`Moeda: ${curr.symbol} (${curr.code})`
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
ShopSettingsTab.displayName = 'ShopSettingsTab'
|
|
123
|
+
return ShopSettingsTab
|
|
124
|
+
})(),
|
|
125
|
+
order: 30,
|
|
126
|
+
permission: { feature: 'shop', action: 'read' as const },
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Re-export shop types so consumer apps only need @fayz-ai/plugin-shop
|
|
133
|
+
export type {
|
|
134
|
+
Product, Order, ShopCustomer, Discount, Category,
|
|
135
|
+
CreateProductInput, UpdateProductInput, ListProductsOptions,
|
|
136
|
+
CreateOrderInput, UpdateOrderInput, ListOrdersOptions,
|
|
137
|
+
} from '@fayz-ai/shop'
|