@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
@@ -1,144 +0,0 @@
1
- import React from 'react'
2
- import type { EntityDef, FieldDef } from '@fayz-ai/core'
3
- import type { Discount } from '@fayz-ai/shop'
4
-
5
- // ---------------------------------------------------------------------------
6
- // Discount — declarative CRUD, same contract as products and customers.
7
- //
8
- // `timesUsed` is read-only: shop_place_order increments it inside the same
9
- // transaction that validates the coupon, so the admin must never write it.
10
- //
11
- // `buy_x_get_y` is deliberately absent from the type options: the enum value
12
- // exists in the database but shop_place_order records the code and applies no
13
- // monetary effect (0003:116), so offering it would create coupons that silently
14
- // do nothing.
15
- // ---------------------------------------------------------------------------
16
-
17
- const STATUS_TONE: Record<string, string> = {
18
- active: 'bg-emerald-100 text-emerald-800',
19
- draft: 'bg-gray-100 text-gray-700',
20
- expired: 'bg-orange-100 text-orange-700',
21
- disabled: 'bg-red-100 text-red-700',
22
- }
23
-
24
- export function buildShopDiscountEntity(): EntityDef<Discount> {
25
- const fields: FieldDef[] = [
26
- // — Cupom —
27
- {
28
- key: 'title', label: 'Nome interno', type: 'text', required: true,
29
- group: 'coupon', placeholder: 'Bem-vindo 10%',
30
- hint: 'Só o admin vê. Serve para você identificar a campanha.',
31
- searchable: true, showInTable: true, sortable: true,
32
- renderCell: (_v, row: any) => (
33
- <div className="min-w-0">
34
- <p className="truncate font-medium">{row.title}</p>
35
- {row.code && <p className="font-mono text-xs text-muted-foreground">{row.code}</p>}
36
- </div>
37
- ),
38
- },
39
- {
40
- key: 'code', label: 'Código', type: 'text', group: 'coupon',
41
- placeholder: 'ARTORIUS10',
42
- hint: 'O que o cliente digita no carrinho. Maiúsculas e minúsculas não importam.',
43
- searchable: true, showInTable: false,
44
- },
45
-
46
- // — Regra —
47
- {
48
- key: 'type', label: 'Tipo', type: 'segmented', required: true, group: 'rule', span: 2,
49
- options: [
50
- { label: 'Percentual', value: 'percentage', description: 'Desconta uma % do subtotal.' },
51
- { label: 'Valor fixo', value: 'fixed_amount', description: 'Desconta um valor em reais.' },
52
- { label: 'Frete grátis', value: 'free_shipping', description: 'Zera o frete do pedido.' },
53
- ],
54
- showInTable: true, sortable: false,
55
- renderCell: (v) => {
56
- const labels: Record<string, string> = {
57
- percentage: 'Percentual', fixed_amount: 'Valor fixo', free_shipping: 'Frete grátis', buy_x_get_y: 'Leve X pague Y',
58
- }
59
- return <span className="text-muted-foreground">{labels[String(v)] ?? String(v)}</span>
60
- },
61
- },
62
- {
63
- key: 'value', label: 'Valor', type: 'number', group: 'rule', min: 0,
64
- placeholder: '10',
65
- hint: 'Percentual: 10 = 10%. Valor fixo: 10 = R$ 10,00. Frete grátis ignora este campo.',
66
- showInTable: true, sortable: true,
67
- renderCell: (_v, row: any) => (
68
- <span className="block text-right font-medium">
69
- {row.type === 'percentage'
70
- ? `${row.value}%`
71
- : row.type === 'free_shipping'
72
- ? '—'
73
- : new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(Number(row.value) || 0)}
74
- </span>
75
- ),
76
- },
77
-
78
- // — Limites —
79
- {
80
- key: 'minSubtotal', label: 'Pedido mínimo', type: 'currency', group: 'limits', min: 0,
81
- currency: 'BRL', currencySymbol: 'R$', currencyLocale: 'pt-BR',
82
- placeholder: 'sem mínimo',
83
- hint: 'Valor mínimo do pedido para o cupom valer. Deixe vazio para não exigir mínimo.',
84
- showInTable: false,
85
- },
86
- {
87
- key: 'usageLimit', label: 'Limite de usos', type: 'number', group: 'limits', min: 0,
88
- placeholder: 'sem limite', hint: 'Deixe vazio para uso ilimitado.', showInTable: false,
89
- },
90
- {
91
- key: 'oncePerCustomer', label: 'Uma vez por cliente', type: 'boolean', group: 'limits',
92
- hint: 'Só vale para quem se identifica no checkout — compra como visitante não é rastreada.',
93
- showInTable: false,
94
- },
95
- {
96
- key: 'timesUsed', label: 'Usos', type: 'number',
97
- showInForm: false, showInTable: true, sortable: true,
98
- renderCell: (_v, row: any) => (
99
- <span className="block text-right text-muted-foreground">
100
- {row.timesUsed}{row.usageLimit ? ` / ${row.usageLimit}` : ''}
101
- </span>
102
- ),
103
- },
104
-
105
- // — Vigência —
106
- { key: 'startsAt', label: 'Início', type: 'date', group: 'schedule', showInTable: false },
107
- {
108
- key: 'endsAt', label: 'Fim', type: 'date', group: 'schedule',
109
- hint: 'Vazio = sem data de expiração.', showInTable: false,
110
- },
111
- {
112
- key: 'status', label: 'Status', type: 'segmented', required: true, group: 'schedule', span: 2,
113
- options: [
114
- { label: 'Rascunho', value: 'draft', description: 'Não aceito no carrinho.' },
115
- { label: 'Ativo', value: 'active', description: 'Valendo agora.' },
116
- { label: 'Desativado', value: 'disabled', description: 'Suspenso sem apagar o histórico.' },
117
- ],
118
- showInTable: true, sortable: false,
119
- renderCell: (v) => {
120
- const s = String(v ?? 'draft')
121
- return (
122
- <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${STATUS_TONE[s] ?? STATUS_TONE.draft}`}>
123
- {s}
124
- </span>
125
- )
126
- },
127
- },
128
- ]
129
-
130
- return {
131
- name: 'Desconto',
132
- namePlural: 'Descontos',
133
- icon: 'Tag',
134
- displayField: 'title',
135
- fields,
136
- fieldGroups: [
137
- { id: 'coupon', label: 'Cupom', columns: 2 },
138
- { id: 'rule', label: 'Regra', description: 'O desconto é recalculado no servidor a cada pedido.', columns: 2 },
139
- { id: 'limits', label: 'Limites de uso', columns: 2 },
140
- { id: 'schedule', label: 'Vigência', columns: 2 },
141
- ],
142
- facets: [{ field: 'status', allLabel: 'Todos' }],
143
- } as EntityDef<Discount>
144
- }
@@ -1,229 +0,0 @@
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
- }
@@ -1,123 +0,0 @@
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
- }
File without changes