@fayz-ai/plugin-shop 0.9.0 → 0.9.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 (56) hide show
  1. package/README.md +4 -4
  2. package/dist/{ShopPage-JP27MZS2.js → ShopPage-PVHFKDF3.js} +7 -7
  3. package/dist/ShopPage-PVHFKDF3.js.map +1 -0
  4. package/dist/data/categoryDataProvider.d.ts +5 -0
  5. package/dist/data/categoryDataProvider.d.ts.map +1 -0
  6. package/dist/data/collectionDataProvider.d.ts +5 -0
  7. package/dist/data/collectionDataProvider.d.ts.map +1 -0
  8. package/dist/data/customerGroupDataProvider.d.ts +5 -0
  9. package/dist/data/customerGroupDataProvider.d.ts.map +1 -0
  10. package/dist/data/customerSegmentDataProvider.d.ts +16 -0
  11. package/dist/data/customerSegmentDataProvider.d.ts.map +1 -0
  12. package/dist/data/giftCardDataProvider.d.ts +5 -0
  13. package/dist/data/giftCardDataProvider.d.ts.map +1 -0
  14. package/dist/index.d.ts +3 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1088 -86
  17. package/dist/index.js.map +1 -1
  18. package/dist/views/ChannelsPage.d.ts +1 -1
  19. package/dist/views/ChannelsPage.d.ts.map +1 -1
  20. package/dist/views/ContentPage.d.ts +7 -0
  21. package/dist/views/ContentPage.d.ts.map +1 -0
  22. package/dist/views/EmailTemplatePage.d.ts +9 -0
  23. package/dist/views/EmailTemplatePage.d.ts.map +1 -0
  24. package/dist/views/EmailsPage.d.ts +7 -0
  25. package/dist/views/EmailsPage.d.ts.map +1 -0
  26. package/dist/views/ShopPage.d.ts.map +1 -0
  27. package/dist/views/categoryEntity.d.ts +4 -0
  28. package/dist/views/categoryEntity.d.ts.map +1 -0
  29. package/dist/views/collectionEntity.d.ts +4 -0
  30. package/dist/views/collectionEntity.d.ts.map +1 -0
  31. package/dist/views/customerGroupEntity.d.ts +4 -0
  32. package/dist/views/customerGroupEntity.d.ts.map +1 -0
  33. package/dist/views/customerSegmentEntity.d.ts +13 -0
  34. package/dist/views/customerSegmentEntity.d.ts.map +1 -0
  35. package/dist/views/giftCardEntity.d.ts +4 -0
  36. package/dist/views/giftCardEntity.d.ts.map +1 -0
  37. package/package.json +6 -8
  38. package/dist/ShopPage-JP27MZS2.js.map +0 -1
  39. package/dist/ShopPage.d.ts.map +0 -1
  40. package/src/ShopPage.tsx +0 -425
  41. package/src/components/ShopSettings.tsx +0 -108
  42. package/src/data/customerDataProvider.ts +0 -31
  43. package/src/data/discountDataProvider.ts +0 -42
  44. package/src/data/productDataProvider.ts +0 -54
  45. package/src/data/shared.ts +0 -42
  46. package/src/data/shippingZoneDataProvider.ts +0 -49
  47. package/src/index.ts +0 -270
  48. package/src/views/ChannelsPage.tsx +0 -274
  49. package/src/views/OrderDetailView.tsx +0 -319
  50. package/src/views/ProductImagesTab.tsx +0 -183
  51. package/src/views/customerEntity.tsx +0 -110
  52. package/src/views/customerLookup.ts +0 -31
  53. package/src/views/discountEntity.tsx +0 -144
  54. package/src/views/productEntity.tsx +0 -229
  55. package/src/views/shippingZoneEntity.tsx +0 -123
  56. /package/dist/{ShopPage.d.ts → views/ShopPage.d.ts} +0 -0
package/src/ShopPage.tsx DELETED
@@ -1,425 +0,0 @@
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 { useAdminPath, adminNavigateTo } from '@fayz-ai/saas'
6
- import { PersonLink } from '@fayz-ai/saas'
7
- import { shopCustomerLookup } from './views/customerLookup'
8
- import { OrderDetailView } from './views/OrderDetailView'
9
- import type { Product, Order, ShopCustomer, Discount, ListProductsOptions, ShopProvider } from '@fayz-ai/shop'
10
-
11
- export type ShopProviderResolver = ShopProvider | (() => ShopProvider)
12
-
13
- export interface ShopPageProps {
14
- provider?: ShopProviderResolver
15
- }
16
-
17
- // ---------------------------------------------------------------------------
18
- // Shared helpers
19
- // ---------------------------------------------------------------------------
20
-
21
- function currency(value: number, code = 'BRL', locale = 'pt-BR') {
22
- return new Intl.NumberFormat(locale, { style: 'currency', currency: code }).format(value)
23
- }
24
-
25
- function badge(label: string, color: string) {
26
- return (
27
- <span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${color}`}>
28
- {label}
29
- </span>
30
- )
31
- }
32
-
33
- const statusColors: Record<string, string> = {
34
- active: 'bg-emerald-100 text-emerald-800',
35
- draft: 'bg-gray-100 text-gray-700',
36
- archived: 'bg-orange-100 text-orange-700',
37
- open: 'bg-blue-100 text-blue-800',
38
- cancelled: 'bg-red-100 text-red-700',
39
- paid: 'bg-emerald-100 text-emerald-800',
40
- pending: 'bg-yellow-100 text-yellow-800',
41
- fulfilled: 'bg-emerald-100 text-emerald-800',
42
- unfulfilled: 'bg-gray-100 text-gray-700',
43
- 'partially_fulfilled': 'bg-blue-100 text-blue-800',
44
- }
45
-
46
- function resolveShopProvider(provider?: ShopProviderResolver): ShopProvider {
47
- return typeof provider === 'function' ? provider() : provider ?? getShopProvider()
48
- }
49
-
50
- /**
51
- * Every tab used to load with `.then(setState).finally(stopLoading)` and no
52
- * catch. Against a pool where the shop provider reads as `anon`, listOrders and
53
- * listCustomers reject with "permission denied" — the rejection escaped React
54
- * and blanked the whole admin, so a permissions problem looked like a crash.
55
- * The tab now reports what actually failed and the rest of the shell survives.
56
- */
57
- function useShopList<T>(load: () => Promise<T[]>, deps: React.DependencyList) {
58
- const [items, setItems] = useState<T[]>([])
59
- const [loading, setLoading] = useState(true)
60
- const [error, setError] = useState<string | null>(null)
61
-
62
- useEffect(() => {
63
- let cancelled = false
64
- setLoading(true)
65
- setError(null)
66
- load()
67
- .then((data) => { if (!cancelled) setItems(data) })
68
- .catch((err: unknown) => {
69
- if (!cancelled) setError(err instanceof Error ? err.message : String(err))
70
- })
71
- .finally(() => { if (!cancelled) setLoading(false) })
72
- return () => { cancelled = true }
73
- // eslint-disable-next-line react-hooks/exhaustive-deps
74
- }, deps)
75
-
76
- return { items, loading, error }
77
- }
78
-
79
- function ErrorState({ message }: { message: string }) {
80
- return (
81
- <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
82
- <p className="font-medium">Não foi possível carregar estes dados.</p>
83
- <p className="mt-1 text-red-700">{message}</p>
84
- </div>
85
- )
86
- }
87
-
88
- // ---------------------------------------------------------------------------
89
- // Products tab
90
- // ---------------------------------------------------------------------------
91
-
92
- function ProductsTab({ provider }: ShopPageProps) {
93
- const [products, setProducts] = useState<Product[]>([])
94
- const [loading, setLoading] = useState(true)
95
- const [search, setSearch] = useState('')
96
-
97
- const [error, setError] = useState<string | null>(null)
98
-
99
- const load = useCallback(async (opts?: ListProductsOptions) => {
100
- setLoading(true)
101
- setError(null)
102
- try {
103
- const data = await resolveShopProvider(provider).listProducts(opts)
104
- setProducts(data)
105
- } catch (err: unknown) {
106
- setError(err instanceof Error ? err.message : String(err))
107
- } finally {
108
- setLoading(false)
109
- }
110
- }, [provider])
111
-
112
- useEffect(() => { load() }, [load])
113
-
114
- const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
115
- const q = e.target.value
116
- setSearch(q)
117
- load(q ? { search: q } : undefined)
118
- }
119
-
120
- const columns = useMemo<ColumnDef<Product, any>[]>(() => [
121
- { accessorKey: 'name', header: 'Produto', cell: ({ getValue }) => <span className="font-medium">{getValue() as string}</span> },
122
- { accessorKey: 'sku', header: 'SKU', cell: ({ getValue }) => <span className="text-muted-foreground">{(getValue() as string | undefined) ?? '—'}</span> },
123
- {
124
- accessorKey: 'price', header: () => <span className="block text-right">Preço</span>,
125
- cell: ({ getValue, row }) => <div className="text-right">{currency(getValue() as number, row.original.currency)}</div>,
126
- },
127
- {
128
- accessorKey: 'inventoryCount', header: () => <span className="block text-right">Estoque</span>,
129
- cell: ({ getValue }) => {
130
- const count = getValue() as number
131
- return <div className="text-right"><span className={count === 0 ? 'text-red-600 font-medium' : ''}>{count}</span></div>
132
- },
133
- },
134
- {
135
- accessorKey: 'status', header: 'Status',
136
- cell: ({ getValue }) => {
137
- const status = getValue() as string
138
- return badge(status, statusColors[status] ?? 'bg-gray-100 text-gray-700')
139
- },
140
- },
141
- ], [])
142
-
143
- return (
144
- <div>
145
- <div className="flex items-center justify-between mb-4">
146
- <input
147
- type="search" value={search} onChange={handleSearch}
148
- placeholder="Buscar produtos..."
149
- className="border rounded-lg px-3 py-2 text-sm w-64 focus:outline-none focus:ring-2 focus:ring-primary/50"
150
- />
151
- <span className="text-sm text-muted-foreground">{products.length} produtos</span>
152
- </div>
153
-
154
- {error ? (
155
- <ErrorState message={error} />
156
- ) : (
157
- <DataTable
158
- loading={loading}
159
- columns={columns}
160
- data={products}
161
- variant="card"
162
- emptyMessage="Nenhum produto encontrado."
163
- />
164
- )}
165
- </div>
166
- )
167
- }
168
-
169
- // ---------------------------------------------------------------------------
170
- // Orders tab
171
- // ---------------------------------------------------------------------------
172
-
173
- function OrdersTab({ provider }: ShopPageProps) {
174
- const { items: orders, loading, error } = useShopList<Order>(
175
- () => resolveShopProvider(provider).listOrders({ limit: 50 }),
176
- [provider],
177
- )
178
-
179
- const columns = useMemo<ColumnDef<Order, any>[]>(() => [
180
- { accessorKey: 'orderNumber', header: 'Pedido', cell: ({ getValue }) => <span className="font-medium">#{getValue() as string}</span> },
181
- {
182
- id: 'customer', accessorFn: (o) => o.customerName ?? o.customerEmail ?? '—', header: 'Cliente',
183
- // Links to the customer record. stopPropagation because the row itself
184
- // opens the order — without it the click would do both.
185
- cell: ({ getValue, row }) => {
186
- const label = getValue() as string
187
- const id = row.original.customerId
188
- if (!id) return <span>{label}</span>
189
- return (
190
- <PersonLink
191
- personId={id}
192
- name={label}
193
- lookup={shopCustomerLookup(() => resolveShopProvider(provider))}
194
- profileHref={`#/customers/${id}`}
195
- />
196
- )
197
- },
198
- },
199
- {
200
- accessorKey: 'total', header: () => <span className="block text-right">Total</span>,
201
- cell: ({ getValue, row }) => <div className="text-right font-medium">{currency(getValue() as number, row.original.currency)}</div>,
202
- },
203
- {
204
- accessorKey: 'financialStatus', header: 'Pagamento',
205
- cell: ({ getValue }) => {
206
- const status = getValue() as string
207
- return badge(status, statusColors[status] ?? 'bg-gray-100 text-gray-700')
208
- },
209
- },
210
- {
211
- accessorKey: 'fulfillmentStatus', header: 'Entrega',
212
- cell: ({ getValue }) => {
213
- const status = getValue() as string
214
- return badge(status, statusColors[status] ?? 'bg-gray-100 text-gray-700')
215
- },
216
- },
217
- {
218
- accessorKey: 'createdAt', header: 'Data',
219
- cell: ({ getValue }) => {
220
- // Time matters operationally: two orders on the same day are picked,
221
- // packed and settled in the order they arrived.
222
- const d = new Date(getValue() as string)
223
- return (
224
- <span className="whitespace-nowrap text-muted-foreground">
225
- {d.toLocaleDateString('pt-BR')}
226
- <span className="ml-1.5 text-xs opacity-70">{d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}</span>
227
- </span>
228
- )
229
- },
230
- },
231
- ], [])
232
-
233
- return (
234
- <div>
235
- {error ? (
236
- <ErrorState message={error} />
237
- ) : (
238
- <DataTable
239
- loading={loading}
240
- columns={columns}
241
- data={orders}
242
- variant="card"
243
- emptyMessage="Nenhum pedido ainda."
244
- onRowClick={(row: Order) => adminNavigateTo(`/shop/orders/${row.id}`)}
245
- />
246
- )}
247
- </div>
248
- )
249
- }
250
-
251
- // ---------------------------------------------------------------------------
252
- // Customers tab
253
- // ---------------------------------------------------------------------------
254
-
255
- function CustomersTab({ provider }: ShopPageProps) {
256
- const { items: customers, loading, error } = useShopList<ShopCustomer>(
257
- () => resolveShopProvider(provider).listCustomers({ limit: 50 }),
258
- [provider],
259
- )
260
-
261
- const columns = useMemo<ColumnDef<ShopCustomer, any>[]>(() => [
262
- {
263
- id: 'name', accessorFn: (c) => `${c.firstName} ${c.lastName}`, header: 'Nome',
264
- cell: ({ row }) => <span className="font-medium">{row.original.firstName} {row.original.lastName}</span>,
265
- },
266
- { accessorKey: 'email', header: 'Email', cell: ({ getValue }) => <span className="text-muted-foreground">{(getValue() as string | undefined) ?? '—'}</span> },
267
- { accessorKey: 'phone', header: 'Telefone', cell: ({ getValue }) => <span className="text-muted-foreground">{(getValue() as string | undefined) ?? '—'}</span> },
268
- {
269
- accessorKey: 'ordersCount', header: () => <span className="block text-right">Pedidos</span>,
270
- cell: ({ getValue }) => <div className="text-right">{getValue() as number}</div>,
271
- },
272
- {
273
- accessorKey: 'totalSpent', header: () => <span className="block text-right">Total Gasto</span>,
274
- cell: ({ getValue }) => <div className="text-right">{currency(getValue() as number)}</div>,
275
- },
276
- ], [])
277
-
278
- return (
279
- <div>
280
- {error ? (
281
- <ErrorState message={error} />
282
- ) : (
283
- <DataTable
284
- loading={loading}
285
- columns={columns}
286
- data={customers}
287
- variant="card"
288
- emptyMessage="Nenhum cliente ainda."
289
- />
290
- )}
291
- </div>
292
- )
293
- }
294
-
295
- // ---------------------------------------------------------------------------
296
- // Discounts tab
297
- // ---------------------------------------------------------------------------
298
-
299
- function DiscountsTab({ provider }: ShopPageProps) {
300
- const { items: discounts, loading, error } = useShopList<Discount>(
301
- () => resolveShopProvider(provider).listDiscounts({ limit: 50 }),
302
- [provider],
303
- )
304
-
305
- const columns = useMemo<ColumnDef<Discount, any>[]>(() => [
306
- { accessorKey: 'title', header: 'Título', cell: ({ getValue }) => <span className="font-medium">{getValue() as string}</span> },
307
- { accessorKey: 'code', header: 'Código', cell: ({ getValue }) => <span className="font-mono text-sm">{(getValue() as string | undefined) ?? '—'}</span> },
308
- { accessorKey: 'type', header: 'Tipo', cell: ({ getValue }) => <span className="text-muted-foreground">{getValue() as string}</span> },
309
- {
310
- accessorKey: 'value', header: () => <span className="block text-right">Valor</span>,
311
- cell: ({ row }) => <div className="text-right">{row.original.type === 'percentage' ? `${row.original.value}%` : currency(row.original.value)}</div>,
312
- },
313
- {
314
- id: 'usos', accessorKey: 'timesUsed', header: () => <span className="block text-right">Usos</span>,
315
- cell: ({ row }) => <div className="text-right">{row.original.timesUsed}{row.original.usageLimit ? ` / ${row.original.usageLimit}` : ''}</div>,
316
- },
317
- {
318
- accessorKey: 'status', header: 'Status',
319
- cell: ({ getValue }) => {
320
- const status = getValue() as string
321
- return badge(status, statusColors[status] ?? 'bg-gray-100 text-gray-700')
322
- },
323
- },
324
- ], [])
325
-
326
- return (
327
- <div>
328
- {error ? (
329
- <ErrorState message={error} />
330
- ) : (
331
- <DataTable
332
- loading={loading}
333
- columns={columns}
334
- data={discounts}
335
- variant="card"
336
- emptyMessage="Nenhum desconto cadastrado."
337
- />
338
- )}
339
- </div>
340
- )
341
- }
342
-
343
- // ---------------------------------------------------------------------------
344
- // Main page
345
- // ---------------------------------------------------------------------------
346
-
347
- const TABS = [
348
- { id: 'products', label: 'Produtos' },
349
- { id: 'orders', label: 'Pedidos' },
350
- { id: 'customers', label: 'Clientes' },
351
- { id: 'discounts', label: 'Descontos' },
352
- ] as const
353
-
354
- type TabId = typeof TABS[number]['id']
355
-
356
- const TAB_COPY: Record<TabId, { title: string; subtitle: string }> = {
357
- products: { title: 'Produtos', subtitle: 'Catálogo da loja: preço, estoque e status.' },
358
- orders: { title: 'Pedidos', subtitle: 'Pedidos recebidos, pagamento e entrega.' },
359
- customers: { title: 'Clientes', subtitle: 'Quem comprou, quanto e com que frequência.' },
360
- discounts: { title: 'Descontos', subtitle: 'Cupons, regras de uso e desempenho.' },
361
- }
362
-
363
- /**
364
- * `section` pins the page to one area, which is how the sidebar routes
365
- * /shop/products, /shop/orders, … as separate entries. Without it the page
366
- * keeps its own tab bar — the single-route behaviour, still used by /shop.
367
- */
368
- export const ShopPage: React.FC<ShopPageProps & { section?: TabId }> = ({ provider, section }) => {
369
- const [tab, setTab] = useState<TabId>(section ?? 'products')
370
- const active = section ?? tab
371
-
372
- // /shop/orders/<id> renders the detail. Orders deliberately do not go through
373
- // createCrudPage: an order is not an editable record, it is a document with
374
- // items, money and a shipment attached.
375
- const path = useAdminPath()
376
- const orderId = section === 'orders' ? path.match(/^\/shop\/orders\/([^/]+)$/)?.[1] : undefined
377
- if (orderId) {
378
- return (
379
- <div className="p-6 max-w-7xl mx-auto">
380
- <OrderDetailView
381
- orderId={orderId}
382
- provider={() => resolveShopProvider(provider)}
383
- onBack={() => adminNavigateTo('/shop/orders')}
384
- />
385
- </div>
386
- )
387
- }
388
- const copy = section ? TAB_COPY[section] : { title: 'Loja', subtitle: 'Gerencie produtos, pedidos, clientes e descontos.' }
389
-
390
- return (
391
- <div className="p-6 max-w-7xl mx-auto">
392
- <div className="mb-6">
393
- <h1 className="text-2xl font-semibold">{copy.title}</h1>
394
- <p className="text-muted-foreground mt-1">{copy.subtitle}</p>
395
- </div>
396
-
397
- {!section && (
398
- <div className="border-b mb-6">
399
- <nav className="flex gap-1">
400
- {TABS.map(t => (
401
- <button
402
- key={t.id}
403
- onClick={() => setTab(t.id)}
404
- className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
405
- tab === t.id
406
- ? 'border-primary text-primary'
407
- : 'border-transparent text-muted-foreground hover:text-foreground'
408
- }`}
409
- >
410
- {t.label}
411
- </button>
412
- ))}
413
- </nav>
414
- </div>
415
- )}
416
-
417
- {active === 'products' && <ProductsTab provider={provider} />}
418
- {active === 'orders' && <OrdersTab provider={provider} />}
419
- {active === 'customers' && <CustomersTab provider={provider} />}
420
- {active === 'discounts' && <DiscountsTab provider={provider} />}
421
- </div>
422
- )
423
- }
424
-
425
- ShopPage.displayName = 'ShopPage'
@@ -1,108 +0,0 @@
1
- import React from 'react'
2
- import { SettingsGroup, ToggleRow, SelectRow } from '@fayz-ai/saas'
3
-
4
- export interface ShopSettingsProps {
5
- currency?: { code?: string; locale?: string; symbol?: string }
6
- }
7
-
8
- const CURRENCIES = [
9
- { value: 'BRL', label: 'Real (R$)' },
10
- { value: 'USD', label: 'US Dollar ($)' },
11
- { value: 'EUR', label: 'Euro (€)' },
12
- { value: 'GBP', label: 'British Pound (£)' },
13
- ]
14
-
15
- const LOCALES = [
16
- { value: 'pt-BR', label: 'Português (Brasil)' },
17
- { value: 'en-US', label: 'English (US)' },
18
- { value: 'es-ES', label: 'Español' },
19
- ]
20
-
21
- /**
22
- * Shop plugin settings — store, catalog, checkout and notification config.
23
- * Seeded from the plugin's currency option; interactive (local state). Wire to
24
- * persisted plugin prefs when the shop settings store lands.
25
- */
26
- export function ShopSettings({ currency }: ShopSettingsProps) {
27
- const [code, setCode] = React.useState(currency?.code ?? 'BRL')
28
- const [locale, setLocale] = React.useState(currency?.locale ?? 'pt-BR')
29
- const [showOutOfStock, setShowOutOfStock] = React.useState(true)
30
- const [trackInventory, setTrackInventory] = React.useState(true)
31
- const [requireSku, setRequireSku] = React.useState(false)
32
- const [guestCheckout, setGuestCheckout] = React.useState(true)
33
- const [enableDiscounts, setEnableDiscounts] = React.useState(true)
34
- const [orderEmails, setOrderEmails] = React.useState(true)
35
- const [lowStockAlerts, setLowStockAlerts] = React.useState(false)
36
-
37
- return (
38
- <div className="space-y-4">
39
- <SettingsGroup title="Loja" description="Moeda e formatação da vitrine.">
40
- <SelectRow
41
- label="Moeda"
42
- description="Moeda padrão para preços e checkout."
43
- value={code}
44
- options={CURRENCIES}
45
- onChange={setCode}
46
- />
47
- <SelectRow
48
- label="Idioma / formato"
49
- description="Formatação de números, preços e datas."
50
- value={locale}
51
- options={LOCALES}
52
- onChange={setLocale}
53
- />
54
- </SettingsGroup>
55
-
56
- <SettingsGroup title="Catálogo" description="Como os produtos aparecem e são gerenciados.">
57
- <ToggleRow
58
- label="Mostrar produtos esgotados"
59
- description="Exibir itens sem estoque na vitrine."
60
- checked={showOutOfStock}
61
- onChange={setShowOutOfStock}
62
- />
63
- <ToggleRow
64
- label="Controlar estoque"
65
- description="Acompanhar quantidade e dar baixa no checkout."
66
- checked={trackInventory}
67
- onChange={setTrackInventory}
68
- />
69
- <ToggleRow
70
- label="Exigir SKU"
71
- description="Tornar o SKU obrigatório ao criar produtos."
72
- checked={requireSku}
73
- onChange={setRequireSku}
74
- />
75
- </SettingsGroup>
76
-
77
- <SettingsGroup title="Checkout" description="Opções de compra e pagamento.">
78
- <ToggleRow
79
- label="Checkout sem cadastro"
80
- description="Permitir compra como convidado."
81
- checked={guestCheckout}
82
- onChange={setGuestCheckout}
83
- />
84
- <ToggleRow
85
- label="Cupons de desconto"
86
- description="Habilitar códigos de desconto no carrinho."
87
- checked={enableDiscounts}
88
- onChange={setEnableDiscounts}
89
- />
90
- </SettingsGroup>
91
-
92
- <SettingsGroup title="Notificações" description="Avisos por e-mail da loja.">
93
- <ToggleRow
94
- label="E-mail de novo pedido"
95
- description="Notificar a equipe a cada novo pedido."
96
- checked={orderEmails}
97
- onChange={setOrderEmails}
98
- />
99
- <ToggleRow
100
- label="Alerta de estoque baixo"
101
- description="Avisar quando produtos atingirem o mínimo."
102
- checked={lowStockAlerts}
103
- onChange={setLowStockAlerts}
104
- />
105
- </SettingsGroup>
106
- </div>
107
- )
108
- }
@@ -1,31 +0,0 @@
1
- import type { CrudQuery, CrudResult, DataProvider } from '@fayz-ai/core'
2
- import type { CreateCustomerInput, ShopCustomer, UpdateCustomerInput } from '@fayz-ai/shop'
3
- import { applyQuery, resolve, type ShopProviderResolver } from './shared'
4
-
5
- export function createShopCustomerDataProvider(
6
- provider: ShopProviderResolver,
7
- ): DataProvider<ShopCustomer> {
8
- return {
9
- async list(query: CrudQuery): Promise<CrudResult<ShopCustomer>> {
10
- const rows = await resolve(provider).listCustomers(
11
- query.search ? { search: query.search } : undefined,
12
- )
13
- return applyQuery(rows, query)
14
- },
15
-
16
- async create(data): Promise<ShopCustomer> {
17
- return resolve(provider).createCustomer(data as unknown as CreateCustomerInput)
18
- },
19
-
20
- async update(id: string, data: Partial<ShopCustomer>): Promise<ShopCustomer> {
21
- // ordersCount / totalSpent are trigger-maintained (shop_refresh_customer_stats);
22
- // never send them back or a stale form value would fight the database.
23
- const { ordersCount: _o, totalSpent: _t, ...patch } = data
24
- return resolve(provider).updateCustomer(id, patch as UpdateCustomerInput)
25
- },
26
-
27
- async remove(id: string): Promise<void> {
28
- await resolve(provider).deleteCustomer(id)
29
- },
30
- }
31
- }
@@ -1,42 +0,0 @@
1
- import type { CrudQuery, CrudResult, DataProvider } from '@fayz-ai/core'
2
- import type { CreateDiscountInput, Discount, UpdateDiscountInput } from '@fayz-ai/shop'
3
- import { applyQuery, resolve, type ShopProviderResolver } from './shared'
4
-
5
- export function createShopDiscountDataProvider(
6
- provider: ShopProviderResolver,
7
- ): DataProvider<Discount> {
8
- return {
9
- async list(query: CrudQuery): Promise<CrudResult<Discount>> {
10
- const rows = await resolve(provider).listDiscounts()
11
- const term = query.search?.trim().toLowerCase()
12
- const filtered = term
13
- ? rows.filter((d) =>
14
- d.title.toLowerCase().includes(term) || (d.code ?? '').toLowerCase().includes(term))
15
- : rows
16
- return applyQuery(filtered, query, 'status')
17
- },
18
-
19
- async create(data): Promise<Discount> {
20
- const input = data as unknown as Partial<Discount>
21
- return resolve(provider).createDiscount({
22
- ...(input as CreateDiscountInput),
23
- // Codes are matched case-insensitively server-side; store them upper so
24
- // the admin list reads consistently.
25
- code: input.code ? String(input.code).trim().toUpperCase() : undefined,
26
- } as CreateDiscountInput)
27
- },
28
-
29
- async update(id: string, data: Partial<Discount>): Promise<Discount> {
30
- // timesUsed is incremented by shop_place_order inside the order transaction.
31
- const { timesUsed: _used, ...patch } = data
32
- if (patch.code !== undefined) {
33
- patch.code = patch.code ? String(patch.code).trim().toUpperCase() : null
34
- }
35
- return resolve(provider).updateDiscount(id, patch as UpdateDiscountInput)
36
- },
37
-
38
- async remove(id: string): Promise<void> {
39
- await resolve(provider).deleteDiscount(id)
40
- },
41
- }
42
- }
@@ -1,54 +0,0 @@
1
- import type { CrudQuery, CrudResult, DataProvider } from '@fayz-ai/core'
2
- import type { CreateProductInput, Product, UpdateProductInput } from '@fayz-ai/shop'
3
- import { slugify } from '../views/productEntity'
4
- import { applyQuery, resolve, type ShopProviderResolver } from './shared'
5
-
6
- export type { ShopProviderResolver }
7
-
8
- // ---------------------------------------------------------------------------
9
- // Bridges the generic CRUD scaffolding (DataProvider) to the shop provider, so
10
- // the product screens are the same createCrudPage machinery every other module
11
- // uses instead of a bespoke form.
12
- //
13
- // Which shop provider is injected decides whether writes work at all:
14
- // • createSupabaseShopProvider — authenticates as the merchant (their JWT),
15
- // full CRUD, subject to the tenant-member RLS policies.
16
- // • createFayzShopProvider — the storefront's anonymous REST client. Reads
17
- // work; every write throws FayzShopError 501 by design. The form surfaces
18
- // that message rather than pretending the save succeeded.
19
- // ---------------------------------------------------------------------------
20
-
21
- export function createShopProductDataProvider(provider: ShopProviderResolver): DataProvider<Product> {
22
- return {
23
- async list(query: CrudQuery): Promise<CrudResult<Product>> {
24
- const rows = await resolve(provider).listProducts(
25
- query.search ? { search: query.search } : undefined,
26
- )
27
- return applyQuery(rows, query, 'status')
28
- },
29
-
30
- async create(data): Promise<Product> {
31
- const input = data as unknown as Partial<Product>
32
- // The storefront URL is required by the table's UNIQUE(tenant_id, slug);
33
- // deriving it from the name keeps the merchant from having to know that.
34
- const slug = input.slug?.trim() || slugify(String(input.name ?? ''))
35
- return resolve(provider).createProduct({
36
- ...(input as CreateProductInput),
37
- slug,
38
- } as CreateProductInput)
39
- },
40
-
41
- async update(id: string, data: Partial<Product>): Promise<Product> {
42
- const patch = { ...data }
43
- if (patch.slug !== undefined && !String(patch.slug).trim()) {
44
- // Cleared on purpose → regenerate rather than write an empty slug.
45
- patch.slug = slugify(String(patch.name ?? ''))
46
- }
47
- return resolve(provider).updateProduct(id, patch as UpdateProductInput)
48
- },
49
-
50
- async remove(id: string): Promise<void> {
51
- await resolve(provider).deleteProduct(id)
52
- },
53
- }
54
- }