@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/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
+ }
@@ -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'