@fayz-ai/plugin-shop 0.2.5 → 0.8.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 (55) hide show
  1. package/dist/ShopPage-HCLXMN7Q.js +743 -0
  2. package/dist/ShopPage-HCLXMN7Q.js.map +1 -0
  3. package/dist/ShopPage-JF2SQBKO.cjs +745 -0
  4. package/dist/ShopPage-JF2SQBKO.cjs.map +1 -0
  5. package/dist/ShopPage.d.ts +23 -1
  6. package/dist/ShopPage.d.ts.map +1 -1
  7. package/dist/data/customerDataProvider.d.ts +5 -0
  8. package/dist/data/customerDataProvider.d.ts.map +1 -0
  9. package/dist/data/discountDataProvider.d.ts +5 -0
  10. package/dist/data/discountDataProvider.d.ts.map +1 -0
  11. package/dist/data/productDataProvider.d.ts +6 -0
  12. package/dist/data/productDataProvider.d.ts.map +1 -0
  13. package/dist/data/shared.d.ts +12 -0
  14. package/dist/data/shared.d.ts.map +1 -0
  15. package/dist/data/shippingZoneDataProvider.d.ts +13 -0
  16. package/dist/data/shippingZoneDataProvider.d.ts.map +1 -0
  17. package/dist/index.cjs +986 -26
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +987 -27
  22. package/dist/index.js.map +1 -1
  23. package/dist/views/OrderDetailView.d.ts +10 -0
  24. package/dist/views/OrderDetailView.d.ts.map +1 -0
  25. package/dist/views/ProductImagesTab.d.ts +19 -0
  26. package/dist/views/ProductImagesTab.d.ts.map +1 -0
  27. package/dist/views/customerEntity.d.ts +4 -0
  28. package/dist/views/customerEntity.d.ts.map +1 -0
  29. package/dist/views/customerLookup.d.ts +22 -0
  30. package/dist/views/customerLookup.d.ts.map +1 -0
  31. package/dist/views/discountEntity.d.ts +4 -0
  32. package/dist/views/discountEntity.d.ts.map +1 -0
  33. package/dist/views/productEntity.d.ts +6 -0
  34. package/dist/views/productEntity.d.ts.map +1 -0
  35. package/dist/views/shippingZoneEntity.d.ts +4 -0
  36. package/dist/views/shippingZoneEntity.d.ts.map +1 -0
  37. package/package.json +8 -5
  38. package/src/ShopPage.tsx +156 -54
  39. package/src/data/customerDataProvider.ts +31 -0
  40. package/src/data/discountDataProvider.ts +42 -0
  41. package/src/data/productDataProvider.ts +54 -0
  42. package/src/data/shared.ts +42 -0
  43. package/src/data/shippingZoneDataProvider.ts +49 -0
  44. package/src/index.ts +104 -12
  45. package/src/views/OrderDetailView.tsx +575 -0
  46. package/src/views/ProductImagesTab.tsx +183 -0
  47. package/src/views/customerEntity.tsx +110 -0
  48. package/src/views/customerLookup.ts +31 -0
  49. package/src/views/discountEntity.tsx +137 -0
  50. package/src/views/productEntity.tsx +229 -0
  51. package/src/views/shippingZoneEntity.tsx +123 -0
  52. package/dist/ShopPage-65PPVKBD.js +0 -271
  53. package/dist/ShopPage-65PPVKBD.js.map +0 -1
  54. package/dist/ShopPage-PG5GKOZU.cjs +0 -273
  55. package/dist/ShopPage-PG5GKOZU.cjs.map +0 -1
@@ -0,0 +1,229 @@
1
+ import React from 'react'
2
+ import type { EntityDef, EntityImageConfig, FieldDef } from '@fayz-ai/core'
3
+ import type { Product, ShopProvider } from '@fayz-ai/shop'
4
+ import { ProductImagesTab } from './ProductImagesTab'
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Storefront Product — the single source of truth for the shop product CRUD.
8
+ // Drives the list columns AND the form sections via the generic CRUD scaffolding
9
+ // (createCrudPage → CrudListView + CrudFormPage), same pattern plugin-inventory
10
+ // uses for its ERP product.
11
+ //
12
+ // This is deliberately the STOREFRONT shape (what a shopper sees: name, price,
13
+ // compare-at, images, publication status), not the ERP shape plugin-inventory
14
+ // models (cost, margin, min/max levels). They are different products in
15
+ // different tables — see the shop/inventory bridge note in the roadmap.
16
+ //
17
+ // Not modelled here because the schema has nowhere to put them yet:
18
+ // • variants (options/SKU/stock per variant) — needs plg_shop_variants
19
+ // • SEO title/description — needs columns on plg_shop_products
20
+ // • multi-category — the column is a single category_id
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const STATUS_TONE: Record<string, string> = {
24
+ active: 'bg-emerald-100 text-emerald-800',
25
+ draft: 'bg-gray-100 text-gray-700',
26
+ archived: 'bg-orange-100 text-orange-700',
27
+ }
28
+
29
+ const STATUS_LABEL: Record<string, string> = {
30
+ active: 'Ativo',
31
+ draft: 'Rascunho',
32
+ archived: 'Arquivado',
33
+ }
34
+
35
+ function money(value: unknown, currency = 'BRL') {
36
+ const n = Number(value)
37
+ if (!Number.isFinite(n)) return '—'
38
+ return new Intl.NumberFormat('pt-BR', { style: 'currency', currency }).format(n)
39
+ }
40
+
41
+ /** `name` → `name-do-produto`, so the storefront URL is filled in for you. */
42
+ export function slugify(value: string): string {
43
+ return value
44
+ .normalize('NFD')
45
+ .replace(/[\u0300-\u036f]/g, '')
46
+ .toLowerCase()
47
+ .trim()
48
+ .replace(/[^a-z0-9]+/g, '-')
49
+ .replace(/^-+|-+$/g, '')
50
+ }
51
+
52
+ /**
53
+ * Wires the form's image slot to the product's own images. `uploadProductImage`
54
+ * has existed on the provider all along with nothing calling it — the slot was a
55
+ * decorative square, so a product with a photo still showed an empty placeholder.
56
+ */
57
+ function productImage(getProvider: () => ShopProvider): EntityImageConfig {
58
+ return {
59
+ get: (row) => {
60
+ const images = (row as Partial<Product>).images
61
+ return images?.find((i) => i.isPrimary)?.url ?? images?.[0]?.url
62
+ },
63
+ upload: async (file, row) => {
64
+ if (!row.id) throw new Error('Salve o produto antes de enviar a imagem.')
65
+ const image = await getProvider().uploadProductImage(row.id, file)
66
+ return image.url
67
+ },
68
+ accept: 'image/*',
69
+ }
70
+ }
71
+
72
+ export function buildShopProductEntity(
73
+ currencyCode = 'BRL',
74
+ getProvider?: () => ShopProvider,
75
+ ): EntityDef<Product> {
76
+ const fields: FieldDef[] = [
77
+ // — Geral —
78
+ {
79
+ key: 'name', label: 'Nome do produto', type: 'text', required: true,
80
+ group: 'general', placeholder: 'Costela Bovina Defumada 400g',
81
+ searchable: true, showInTable: true, sortable: true,
82
+ renderCell: (_v, row: any) => (
83
+ <div className="flex items-center gap-3">
84
+ {row.images?.[0]?.url && (
85
+ <img
86
+ src={row.images[0].url}
87
+ alt=""
88
+ className="h-9 w-9 rounded object-cover"
89
+ loading="lazy"
90
+ />
91
+ )}
92
+ <div className="min-w-0">
93
+ <p className="truncate font-medium">{row.name}</p>
94
+ {row.sku && <p className="text-xs text-muted-foreground">{row.sku}</p>}
95
+ </div>
96
+ </div>
97
+ ),
98
+ },
99
+ {
100
+ key: 'slug', label: 'URL', type: 'text', group: 'general',
101
+ placeholder: 'gerado a partir do nome',
102
+ hint: 'Endereço do produto na loja. Deixe em branco para gerar automaticamente.',
103
+ showInTable: false,
104
+ },
105
+ {
106
+ key: 'description', label: 'Descrição', type: 'textarea', group: 'general', span: 2,
107
+ placeholder: 'Como é feito, como preparar, o que acompanha…',
108
+ showInTable: false,
109
+ },
110
+
111
+ // — Preço —
112
+ {
113
+ key: 'price', label: 'Preço de venda', type: 'currency', required: true,
114
+ group: 'pricing', currency: currencyCode, currencySymbol: 'R$', currencyLocale: 'pt-BR',
115
+ showInTable: true, sortable: true,
116
+ renderCell: (v, row: any) => (
117
+ <div className="text-right">
118
+ <span className="font-medium">{money(v, row.currency)}</span>
119
+ {row.compareAtPrice > row.price && (
120
+ <span className="ml-2 text-xs text-muted-foreground line-through">
121
+ {money(row.compareAtPrice, row.currency)}
122
+ </span>
123
+ )}
124
+ </div>
125
+ ),
126
+ },
127
+ {
128
+ key: 'compareAtPrice', label: 'Preço comparativo', type: 'currency', group: 'pricing',
129
+ currency: currencyCode, currencySymbol: 'R$', currencyLocale: 'pt-BR',
130
+ hint: 'Preço "de" riscado. Deixe vazio se não estiver em promoção.',
131
+ showInTable: false,
132
+ },
133
+ {
134
+ key: 'discount', label: 'Desconto', type: 'computed', group: 'pricing', showInTable: false,
135
+ compute: (vals) => {
136
+ const price = Number(vals.price) || 0
137
+ const compare = Number(vals.compareAtPrice) || 0
138
+ if (compare <= 0 || price <= 0 || compare <= price) return null
139
+ const pct = ((compare - price) / compare) * 100
140
+ return { display: `-${pct.toFixed(0)}%`, tone: 'positive' }
141
+ },
142
+ },
143
+
144
+ // — Estoque —
145
+ {
146
+ key: 'sku', label: 'SKU', type: 'text', group: 'inventory',
147
+ placeholder: 'ART-001', searchable: true, showInTable: false,
148
+ },
149
+ {
150
+ key: 'inventoryCount', label: 'Estoque', type: 'number', group: 'inventory', min: 0,
151
+ placeholder: '0', hint: 'Chega a zero e o produto aparece como esgotado na loja.',
152
+ showInTable: true, sortable: true,
153
+ renderCell: (v) => {
154
+ const n = Number(v) || 0
155
+ return (
156
+ <span className={`block text-right ${n === 0 ? 'font-medium text-destructive' : n <= 3 ? 'text-orange-600' : ''}`}>
157
+ {n}
158
+ </span>
159
+ )
160
+ },
161
+ },
162
+
163
+ // — Publicação —
164
+ {
165
+ key: 'status', label: 'Status', type: 'segmented', required: true,
166
+ group: 'publishing', span: 2,
167
+ options: [
168
+ { label: 'Rascunho', value: 'draft', description: 'Só você vê. Não aparece na loja.' },
169
+ { label: 'Ativo', value: 'active', description: 'Publicado e à venda.' },
170
+ { label: 'Arquivado', value: 'archived', description: 'Fora da loja, histórico preservado.' },
171
+ ],
172
+ showInTable: true, sortable: false,
173
+ renderCell: (v) => {
174
+ const s = String(v ?? 'draft')
175
+ return (
176
+ <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${STATUS_TONE[s] ?? STATUS_TONE.draft}`}>
177
+ {STATUS_LABEL[s] ?? s}
178
+ </span>
179
+ )
180
+ },
181
+ },
182
+ {
183
+ key: 'sortOrder', label: 'Ordem na vitrine', type: 'number', group: 'publishing',
184
+ placeholder: '0', hint: 'Menor aparece primeiro.', showInTable: false,
185
+ },
186
+
187
+ // — Envio —
188
+ {
189
+ key: 'isPhysical', label: 'Produto físico', type: 'boolean', group: 'shipping',
190
+ hint: 'Desmarque para itens digitais, que não calculam frete.', showInTable: false,
191
+ },
192
+ { key: 'weight', label: 'Peso', type: 'number', group: 'shipping', min: 0, placeholder: '0.4', showInTable: false },
193
+ {
194
+ key: 'weightUnit', label: 'Unidade', type: 'select', group: 'shipping', showInTable: false,
195
+ options: [{ label: 'kg', value: 'kg' }, { label: 'g', value: 'g' }, { label: 'lb', value: 'lb' }],
196
+ },
197
+ ]
198
+
199
+ return {
200
+ name: 'Produto',
201
+ namePlural: 'Produtos',
202
+ icon: 'Package',
203
+ displayField: 'name',
204
+ fields,
205
+ fieldGroups: [
206
+ // imageSlot renders the scaffolding's image uploader alongside the group.
207
+ { id: 'general', label: 'Informações', columns: 2, imageSlot: true },
208
+ { id: 'pricing', label: 'Preço', description: 'O preço comparativo vira o "de" riscado na vitrine.', columns: 3 },
209
+ { id: 'inventory', label: 'Estoque', columns: 2 },
210
+ { id: 'publishing', label: 'Publicação', description: 'Controla se o produto aparece na loja.', columns: 2 },
211
+ { id: 'shipping', label: 'Envio', columns: 3 },
212
+ ],
213
+ facets: [{ field: 'status', allLabel: 'Todos' }],
214
+ image: getProvider ? productImage(getProvider) : undefined,
215
+ // The form's image slot sets the COVER; this tab manages the rest of the
216
+ // gallery. plg_shop_product_images has always held a collection, and until
217
+ // now nothing in the admin could add a second row to it — so the storefront
218
+ // carousel had exactly one slide for every product in the pool.
219
+ detailTabs: getProvider
220
+ ? [{
221
+ id: 'images',
222
+ label: 'Fotos',
223
+ icon: 'Image',
224
+ component: ProductImagesTab as never,
225
+ props: { getProvider },
226
+ }]
227
+ : undefined,
228
+ } as EntityDef<Product>
229
+ }
@@ -0,0 +1,123 @@
1
+ import React from 'react'
2
+ import type { EntityDef, FieldDef } from '@fayz-ai/core'
3
+ import type { ShippingZone } from '@fayz-ai/shop'
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Delivery zone — declarative CRUD, same contract as products and discounts.
7
+ //
8
+ // A zone is a range of postal codes with its own price and estimate. Ranges may
9
+ // overlap on purpose: the cheapest match wins, which is how a merchant layers a
10
+ // promotion over a broader zone without deleting the broader one.
11
+ //
12
+ // The two things worth being loud about in the UI:
13
+ // · a store with NO zones keeps charging its flat rate — zones are opt-in;
14
+ // · once the first zone exists, an address outside every zone can no longer
15
+ // order (shop_place_order refuses it), so the first save changes who can buy.
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const cep = (value: string | undefined): string => {
19
+ const digits = String(value ?? '').replace(/\D/g, '').padStart(8, '0')
20
+ return `${digits.slice(0, 5)}-${digits.slice(5)}`
21
+ }
22
+
23
+ export function buildShopShippingZoneEntity(currency = 'BRL'): EntityDef<ShippingZone> {
24
+ const money = (value: number | null | undefined) =>
25
+ value == null
26
+ ? '—'
27
+ : new Intl.NumberFormat('pt-BR', { style: 'currency', currency }).format(Number(value))
28
+
29
+ const fields: FieldDef[] = [
30
+ {
31
+ key: 'name', label: 'Nome da zona', type: 'text', required: true, group: 'zone',
32
+ placeholder: 'Rio de Janeiro — capital',
33
+ hint: 'Aparece para o cliente na hora de escolher a entrega.',
34
+ searchable: true, showInTable: true, sortable: true,
35
+ },
36
+ {
37
+ key: 'carrier', label: 'Transportadora', type: 'text', group: 'zone',
38
+ placeholder: 'Correios, motoboy próprio…',
39
+ hint: 'Opcional. Mostrado antes do nome da zona quando preenchido.',
40
+ showInTable: true,
41
+ },
42
+ {
43
+ key: 'postalFrom', label: 'CEP inicial', type: 'text', required: true, group: 'zone',
44
+ placeholder: '20000-000',
45
+ hint: 'Início da faixa. Pode digitar com ou sem traço.',
46
+ showInTable: true,
47
+ renderCell: (_v, row: any) => (
48
+ <span className="whitespace-nowrap font-mono text-xs">
49
+ {cep(row.postalFrom)} → {cep(row.postalTo)}
50
+ </span>
51
+ ),
52
+ },
53
+ {
54
+ key: 'postalTo', label: 'CEP final', type: 'text', required: true, group: 'zone',
55
+ placeholder: '23799-999',
56
+ hint: 'Fim da faixa, inclusive.',
57
+ showInTable: false,
58
+ },
59
+ {
60
+ key: 'rate', label: 'Valor do frete', type: 'number', required: true, group: 'pricing',
61
+ hint: 'Cobrado nos pedidos entregues nessa faixa.',
62
+ showInTable: true, sortable: true,
63
+ renderCell: (value: any) => <span className="tabular-nums">{money(value)}</span>,
64
+ },
65
+ {
66
+ key: 'freeAbove', label: 'Frete grátis acima de', type: 'number', group: 'pricing',
67
+ hint: 'Deixe vazio para nunca dar frete grátis nessa zona. Vale mais que a regra geral da loja.',
68
+ showInTable: true,
69
+ renderCell: (value: any) => <span className="tabular-nums">{money(value)}</span>,
70
+ },
71
+ {
72
+ key: 'etaMinDays', label: 'Prazo mínimo (dias)', type: 'number', group: 'delivery',
73
+ hint: '0 = no mesmo dia.',
74
+ showInTable: false,
75
+ },
76
+ {
77
+ key: 'etaMaxDays', label: 'Prazo máximo (dias)', type: 'number', group: 'delivery',
78
+ hint: 'O cliente vê a faixa "de X a Y dias úteis" na página do produto.',
79
+ showInTable: true,
80
+ renderCell: (_v, row: any) =>
81
+ row.etaMinDays == null && row.etaMaxDays == null
82
+ ? <span className="text-muted-foreground">—</span>
83
+ : <span>{row.etaMinDays ?? 0}–{row.etaMaxDays ?? row.etaMinDays} dias</span>,
84
+ },
85
+ {
86
+ key: 'active', label: 'Ativa', type: 'boolean', group: 'delivery',
87
+ hint: 'Desativar para de cotar essa faixa sem apagar a configuração.',
88
+ showInTable: true,
89
+ renderCell: (value: any) => (
90
+ <span className={`rounded-full px-2 py-0.5 text-xs font-medium ${
91
+ value ? 'bg-emerald-100 text-emerald-800' : 'bg-gray-100 text-gray-700'}`}>
92
+ {value ? 'Ativa' : 'Inativa'}
93
+ </span>
94
+ ),
95
+ },
96
+ {
97
+ key: 'sortOrder', label: 'Ordem', type: 'number', group: 'delivery',
98
+ hint: 'Desempate quando duas zonas custam o mesmo. Menor aparece primeiro.',
99
+ showInTable: false,
100
+ },
101
+ ]
102
+
103
+ return {
104
+ name: 'Zona de entrega',
105
+ namePlural: 'Entrega',
106
+ icon: 'Truck',
107
+ displayField: 'name',
108
+ fields,
109
+ fieldGroups: [
110
+ {
111
+ id: 'zone', label: 'Faixa de CEP', columns: 2,
112
+ description:
113
+ 'Faixas podem se sobrepor: quando duas cobrem o mesmo CEP, o cliente paga a mais barata.',
114
+ },
115
+ {
116
+ id: 'pricing', label: 'Preço', columns: 2,
117
+ description: 'O frete cobrado é recalculado no servidor a cada pedido, a partir daqui.',
118
+ },
119
+ { id: 'delivery', label: 'Prazo e status', columns: 2 },
120
+ ],
121
+ facets: [{ field: 'active', allLabel: 'Todas' }],
122
+ } as EntityDef<ShippingZone>
123
+ }
@@ -1,271 +0,0 @@
1
- import { useState, useCallback, useEffect, useMemo } from 'react';
2
- import { DataTable } from '@fayz-ai/ui';
3
- import { getShopProvider } from '@fayz-ai/shop';
4
- import { jsxs, jsx } from 'react/jsx-runtime';
5
-
6
- // src/ShopPage.tsx
7
- function currency(value, code = "BRL", locale = "pt-BR") {
8
- return new Intl.NumberFormat(locale, { style: "currency", currency: code }).format(value);
9
- }
10
- function badge(label, color) {
11
- return /* @__PURE__ */ jsx("span", { className: `inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${color}`, children: label });
12
- }
13
- var statusColors = {
14
- active: "bg-emerald-100 text-emerald-800",
15
- draft: "bg-gray-100 text-gray-700",
16
- archived: "bg-orange-100 text-orange-700",
17
- open: "bg-blue-100 text-blue-800",
18
- cancelled: "bg-red-100 text-red-700",
19
- paid: "bg-emerald-100 text-emerald-800",
20
- pending: "bg-yellow-100 text-yellow-800",
21
- fulfilled: "bg-emerald-100 text-emerald-800",
22
- unfulfilled: "bg-gray-100 text-gray-700",
23
- "partially_fulfilled": "bg-blue-100 text-blue-800"
24
- };
25
- function resolveShopProvider(provider) {
26
- return typeof provider === "function" ? provider() : provider ?? getShopProvider();
27
- }
28
- function ProductsTab({ provider }) {
29
- const [products, setProducts] = useState([]);
30
- const [loading, setLoading] = useState(true);
31
- const [search, setSearch] = useState("");
32
- const load = useCallback(async (opts) => {
33
- setLoading(true);
34
- try {
35
- const data = await resolveShopProvider(provider).listProducts(opts);
36
- setProducts(data);
37
- } finally {
38
- setLoading(false);
39
- }
40
- }, [provider]);
41
- useEffect(() => {
42
- load();
43
- }, [load]);
44
- const handleSearch = (e) => {
45
- const q = e.target.value;
46
- setSearch(q);
47
- load(q ? { search: q } : void 0);
48
- };
49
- const columns = useMemo(() => [
50
- { accessorKey: "name", header: "Produto", cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "font-medium", children: getValue() }) },
51
- { accessorKey: "sku", header: "SKU", cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: getValue() ?? "\u2014" }) },
52
- {
53
- accessorKey: "price",
54
- header: () => /* @__PURE__ */ jsx("span", { className: "block text-right", children: "Pre\xE7o" }),
55
- cell: ({ getValue, row }) => /* @__PURE__ */ jsx("div", { className: "text-right", children: currency(getValue(), row.original.currency) })
56
- },
57
- {
58
- accessorKey: "inventoryCount",
59
- header: () => /* @__PURE__ */ jsx("span", { className: "block text-right", children: "Estoque" }),
60
- cell: ({ getValue }) => {
61
- const count = getValue();
62
- return /* @__PURE__ */ jsx("div", { className: "text-right", children: /* @__PURE__ */ jsx("span", { className: count === 0 ? "text-red-600 font-medium" : "", children: count }) });
63
- }
64
- },
65
- {
66
- accessorKey: "status",
67
- header: "Status",
68
- cell: ({ getValue }) => {
69
- const status = getValue();
70
- return badge(status, statusColors[status] ?? "bg-gray-100 text-gray-700");
71
- }
72
- }
73
- ], []);
74
- return /* @__PURE__ */ jsxs("div", { children: [
75
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-4", children: [
76
- /* @__PURE__ */ jsx(
77
- "input",
78
- {
79
- type: "search",
80
- value: search,
81
- onChange: handleSearch,
82
- placeholder: "Buscar produtos...",
83
- className: "border rounded-lg px-3 py-2 text-sm w-64 focus:outline-none focus:ring-2 focus:ring-primary/50"
84
- }
85
- ),
86
- /* @__PURE__ */ jsxs("span", { className: "text-sm text-muted-foreground", children: [
87
- products.length,
88
- " produtos"
89
- ] })
90
- ] }),
91
- loading ? /* @__PURE__ */ jsx("div", { className: "py-12 text-center text-sm text-muted-foreground", children: "Carregando..." }) : /* @__PURE__ */ jsx(
92
- DataTable,
93
- {
94
- columns,
95
- data: products,
96
- variant: "card",
97
- emptyMessage: "Nenhum produto encontrado."
98
- }
99
- )
100
- ] });
101
- }
102
- function OrdersTab({ provider }) {
103
- const [orders, setOrders] = useState([]);
104
- const [loading, setLoading] = useState(true);
105
- useEffect(() => {
106
- resolveShopProvider(provider).listOrders({ limit: 50 }).then(setOrders).finally(() => setLoading(false));
107
- }, [provider]);
108
- const columns = useMemo(() => [
109
- { accessorKey: "orderNumber", header: "Pedido", cell: ({ getValue }) => /* @__PURE__ */ jsxs("span", { className: "font-medium", children: [
110
- "#",
111
- getValue()
112
- ] }) },
113
- {
114
- id: "customer",
115
- accessorFn: (o) => o.customerName ?? o.customerEmail ?? "\u2014",
116
- header: "Cliente",
117
- cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { children: getValue() })
118
- },
119
- {
120
- accessorKey: "total",
121
- header: () => /* @__PURE__ */ jsx("span", { className: "block text-right", children: "Total" }),
122
- cell: ({ getValue, row }) => /* @__PURE__ */ jsx("div", { className: "text-right font-medium", children: currency(getValue(), row.original.currency) })
123
- },
124
- {
125
- accessorKey: "financialStatus",
126
- header: "Pagamento",
127
- cell: ({ getValue }) => {
128
- const status = getValue();
129
- return badge(status, statusColors[status] ?? "bg-gray-100 text-gray-700");
130
- }
131
- },
132
- {
133
- accessorKey: "fulfillmentStatus",
134
- header: "Entrega",
135
- cell: ({ getValue }) => {
136
- const status = getValue();
137
- return badge(status, statusColors[status] ?? "bg-gray-100 text-gray-700");
138
- }
139
- },
140
- {
141
- accessorKey: "createdAt",
142
- header: "Data",
143
- cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: new Date(getValue()).toLocaleDateString("pt-BR") })
144
- }
145
- ], []);
146
- return /* @__PURE__ */ jsx("div", { children: loading ? /* @__PURE__ */ jsx("div", { className: "py-12 text-center text-sm text-muted-foreground", children: "Carregando..." }) : /* @__PURE__ */ jsx(
147
- DataTable,
148
- {
149
- columns,
150
- data: orders,
151
- variant: "card",
152
- emptyMessage: "Nenhum pedido ainda."
153
- }
154
- ) });
155
- }
156
- function CustomersTab({ provider }) {
157
- const [customers, setCustomers] = useState([]);
158
- const [loading, setLoading] = useState(true);
159
- useEffect(() => {
160
- resolveShopProvider(provider).listCustomers({ limit: 50 }).then(setCustomers).finally(() => setLoading(false));
161
- }, [provider]);
162
- const columns = useMemo(() => [
163
- {
164
- id: "name",
165
- accessorFn: (c) => `${c.firstName} ${c.lastName}`,
166
- header: "Nome",
167
- cell: ({ row }) => /* @__PURE__ */ jsxs("span", { className: "font-medium", children: [
168
- row.original.firstName,
169
- " ",
170
- row.original.lastName
171
- ] })
172
- },
173
- { accessorKey: "email", header: "Email", cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: getValue() ?? "\u2014" }) },
174
- { accessorKey: "phone", header: "Telefone", cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: getValue() ?? "\u2014" }) },
175
- {
176
- accessorKey: "ordersCount",
177
- header: () => /* @__PURE__ */ jsx("span", { className: "block text-right", children: "Pedidos" }),
178
- cell: ({ getValue }) => /* @__PURE__ */ jsx("div", { className: "text-right", children: getValue() })
179
- },
180
- {
181
- accessorKey: "totalSpent",
182
- header: () => /* @__PURE__ */ jsx("span", { className: "block text-right", children: "Total Gasto" }),
183
- cell: ({ getValue }) => /* @__PURE__ */ jsx("div", { className: "text-right", children: currency(getValue()) })
184
- }
185
- ], []);
186
- return /* @__PURE__ */ jsx("div", { children: loading ? /* @__PURE__ */ jsx("div", { className: "py-12 text-center text-sm text-muted-foreground", children: "Carregando..." }) : /* @__PURE__ */ jsx(
187
- DataTable,
188
- {
189
- columns,
190
- data: customers,
191
- variant: "card",
192
- emptyMessage: "Nenhum cliente ainda."
193
- }
194
- ) });
195
- }
196
- function DiscountsTab({ provider }) {
197
- const [discounts, setDiscounts] = useState([]);
198
- const [loading, setLoading] = useState(true);
199
- useEffect(() => {
200
- resolveShopProvider(provider).listDiscounts({ limit: 50 }).then(setDiscounts).finally(() => setLoading(false));
201
- }, [provider]);
202
- const columns = useMemo(() => [
203
- { accessorKey: "title", header: "T\xEDtulo", cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "font-medium", children: getValue() }) },
204
- { accessorKey: "code", header: "C\xF3digo", cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "font-mono text-sm", children: getValue() ?? "\u2014" }) },
205
- { accessorKey: "type", header: "Tipo", cell: ({ getValue }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: getValue() }) },
206
- {
207
- accessorKey: "value",
208
- header: () => /* @__PURE__ */ jsx("span", { className: "block text-right", children: "Valor" }),
209
- cell: ({ row }) => /* @__PURE__ */ jsx("div", { className: "text-right", children: row.original.type === "percentage" ? `${row.original.value}%` : currency(row.original.value) })
210
- },
211
- {
212
- id: "usos",
213
- accessorKey: "timesUsed",
214
- header: () => /* @__PURE__ */ jsx("span", { className: "block text-right", children: "Usos" }),
215
- cell: ({ row }) => /* @__PURE__ */ jsxs("div", { className: "text-right", children: [
216
- row.original.timesUsed,
217
- row.original.usageLimit ? ` / ${row.original.usageLimit}` : ""
218
- ] })
219
- },
220
- {
221
- accessorKey: "status",
222
- header: "Status",
223
- cell: ({ getValue }) => {
224
- const status = getValue();
225
- return badge(status, statusColors[status] ?? "bg-gray-100 text-gray-700");
226
- }
227
- }
228
- ], []);
229
- return /* @__PURE__ */ jsx("div", { children: loading ? /* @__PURE__ */ jsx("div", { className: "py-12 text-center text-sm text-muted-foreground", children: "Carregando..." }) : /* @__PURE__ */ jsx(
230
- DataTable,
231
- {
232
- columns,
233
- data: discounts,
234
- variant: "card",
235
- emptyMessage: "Nenhum desconto cadastrado."
236
- }
237
- ) });
238
- }
239
- var TABS = [
240
- { id: "products", label: "Produtos" },
241
- { id: "orders", label: "Pedidos" },
242
- { id: "customers", label: "Clientes" },
243
- { id: "discounts", label: "Descontos" }
244
- ];
245
- var ShopPage = ({ provider }) => {
246
- const [tab, setTab] = useState("products");
247
- return /* @__PURE__ */ jsxs("div", { className: "p-6 max-w-7xl mx-auto", children: [
248
- /* @__PURE__ */ jsxs("div", { className: "mb-6", children: [
249
- /* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: "Loja" }),
250
- /* @__PURE__ */ jsx("p", { className: "text-muted-foreground mt-1", children: "Gerencie produtos, pedidos, clientes e descontos." })
251
- ] }),
252
- /* @__PURE__ */ jsx("div", { className: "border-b mb-6", children: /* @__PURE__ */ jsx("nav", { className: "flex gap-1", children: TABS.map((t) => /* @__PURE__ */ jsx(
253
- "button",
254
- {
255
- onClick: () => setTab(t.id),
256
- className: `px-4 py-2 text-sm font-medium border-b-2 transition-colors ${tab === t.id ? "border-primary text-primary" : "border-transparent text-muted-foreground hover:text-foreground"}`,
257
- children: t.label
258
- },
259
- t.id
260
- )) }) }),
261
- tab === "products" && /* @__PURE__ */ jsx(ProductsTab, { provider }),
262
- tab === "orders" && /* @__PURE__ */ jsx(OrdersTab, { provider }),
263
- tab === "customers" && /* @__PURE__ */ jsx(CustomersTab, { provider }),
264
- tab === "discounts" && /* @__PURE__ */ jsx(DiscountsTab, { provider })
265
- ] });
266
- };
267
- ShopPage.displayName = "ShopPage";
268
-
269
- export { ShopPage };
270
- //# sourceMappingURL=ShopPage-65PPVKBD.js.map
271
- //# sourceMappingURL=ShopPage-65PPVKBD.js.map