@hed-hog/catalog 0.0.293 → 0.0.295

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 (66) hide show
  1. package/README.md +391 -361
  2. package/dist/catalog-resource.config.d.ts.map +1 -1
  3. package/dist/catalog-resource.config.js +51 -24
  4. package/dist/catalog-resource.config.js.map +1 -1
  5. package/dist/catalog.controller.d.ts +420 -0
  6. package/dist/catalog.controller.d.ts.map +1 -1
  7. package/dist/catalog.controller.js +98 -0
  8. package/dist/catalog.controller.js.map +1 -1
  9. package/dist/catalog.module.d.ts.map +1 -1
  10. package/dist/catalog.module.js +5 -1
  11. package/dist/catalog.module.js.map +1 -1
  12. package/dist/catalog.service.d.ts +216 -1
  13. package/dist/catalog.service.d.ts.map +1 -1
  14. package/dist/catalog.service.js +1121 -7
  15. package/dist/catalog.service.js.map +1 -1
  16. package/hedhog/data/catalog_attribute.yaml +202 -0
  17. package/hedhog/data/catalog_attribute_option.yaml +109 -0
  18. package/hedhog/data/catalog_category.yaml +47 -0
  19. package/hedhog/data/catalog_category_attribute.yaml +209 -0
  20. package/hedhog/data/menu.yaml +46 -12
  21. package/hedhog/data/role.yaml +7 -7
  22. package/hedhog/data/route.yaml +64 -0
  23. package/hedhog/frontend/app/[resource]/page.tsx.ejs +358 -0
  24. package/hedhog/frontend/app/_components/catalog-ai-form-assist-dialog.tsx.ejs +340 -0
  25. package/hedhog/frontend/app/_components/catalog-resource-form-sheet.tsx.ejs +815 -0
  26. package/hedhog/frontend/app/_lib/catalog-resources.tsx.ejs +504 -736
  27. package/hedhog/frontend/app/dashboard/page.tsx.ejs +14 -83
  28. package/hedhog/frontend/messages/en.json +150 -60
  29. package/hedhog/frontend/messages/pt.json +185 -95
  30. package/hedhog/table/catalog_affiliate_program.yaml +41 -41
  31. package/hedhog/table/catalog_attribute.yaml +22 -7
  32. package/hedhog/table/catalog_attribute_group.yaml +18 -18
  33. package/hedhog/table/catalog_attribute_option.yaml +40 -0
  34. package/hedhog/table/catalog_brand.yaml +34 -34
  35. package/hedhog/table/catalog_category.yaml +40 -0
  36. package/hedhog/table/catalog_category_attribute.yaml +13 -7
  37. package/hedhog/table/catalog_click_event.yaml +50 -50
  38. package/hedhog/table/catalog_comparison.yaml +3 -6
  39. package/hedhog/table/catalog_comparison_highlight.yaml +39 -39
  40. package/hedhog/table/catalog_comparison_item.yaml +30 -30
  41. package/hedhog/table/catalog_content_relation.yaml +42 -42
  42. package/hedhog/table/catalog_import_run.yaml +33 -33
  43. package/hedhog/table/catalog_import_source.yaml +24 -24
  44. package/hedhog/table/catalog_merchant.yaml +29 -29
  45. package/hedhog/table/catalog_offer.yaml +83 -83
  46. package/hedhog/table/catalog_price_history.yaml +34 -34
  47. package/hedhog/table/catalog_product.yaml +5 -3
  48. package/hedhog/table/catalog_product_attribute_value.yaml +15 -2
  49. package/hedhog/table/catalog_product_category.yaml +3 -3
  50. package/hedhog/table/catalog_product_image.yaml +34 -34
  51. package/hedhog/table/catalog_product_score.yaml +38 -38
  52. package/hedhog/table/catalog_product_site.yaml +47 -47
  53. package/hedhog/table/catalog_product_tag.yaml +19 -19
  54. package/hedhog/table/catalog_score_criterion.yaml +25 -8
  55. package/hedhog/table/catalog_seo_page_rule.yaml +2 -2
  56. package/hedhog/table/catalog_similarity_rule.yaml +19 -6
  57. package/hedhog/table/catalog_site.yaml +8 -0
  58. package/hedhog/table/catalog_site_category.yaml +3 -3
  59. package/package.json +7 -7
  60. package/src/catalog-resource.config.ts +51 -24
  61. package/src/catalog.controller.ts +67 -0
  62. package/src/catalog.module.ts +5 -1
  63. package/src/catalog.service.ts +1531 -6
  64. package/src/index.ts +1 -1
  65. package/src/language/en.json +4 -4
  66. package/src/language/pt.json +4 -4
@@ -5,8 +5,10 @@ import {
5
5
  Building2,
6
6
  Eye,
7
7
  Factory,
8
+ FolderTree,
8
9
  Globe2,
9
10
  Layers3,
11
+ List,
10
12
  ListFilter,
11
13
  PackagePlus,
12
14
  PackageSearch,
@@ -34,33 +36,15 @@ type CatalogFormFieldType =
34
36
  | 'date'
35
37
  | 'datetime';
36
38
 
37
- export type CatalogLocalizedText = {
38
- pt: string;
39
- en: string;
40
- };
41
-
42
- export type CatalogFieldDefinition = {
43
- key: string;
44
- labelKey: string;
45
- type?: CatalogFieldDisplayType;
46
- };
47
-
48
- export type CatalogFilterOptionDefinition = {
49
- value: string;
50
- labelKey: string;
51
- };
52
-
39
+ export type CatalogLocalizedText = { pt: string; en: string };
40
+ export type CatalogFieldDefinition = { key: string; labelKey: string; type?: CatalogFieldDisplayType };
41
+ export type CatalogFilterOptionDefinition = { value: string; labelKey: string };
53
42
  export type CatalogContextualKpiDefinition = {
54
43
  translationKey: string;
55
44
  icon: LucideIcon;
56
45
  count: (records: Record<string, unknown>[]) => number;
57
46
  };
58
-
59
- export type CatalogFormOptionDefinition = {
60
- value: string;
61
- label: CatalogLocalizedText;
62
- };
63
-
47
+ export type CatalogFormOptionDefinition = { value: string; label: CatalogLocalizedText };
64
48
  export type CatalogRelationDefinition = {
65
49
  endpoint: string;
66
50
  resource?: string;
@@ -70,7 +54,6 @@ export type CatalogRelationDefinition = {
70
54
  labelKeys: string[];
71
55
  searchParam?: string;
72
56
  };
73
-
74
57
  export type CatalogFormFieldDefinition = {
75
58
  key: string;
76
59
  label: CatalogLocalizedText;
@@ -84,13 +67,11 @@ export type CatalogFormFieldDefinition = {
84
67
  accept?: string;
85
68
  uploadPreviewVariant?: 'default' | 'square';
86
69
  };
87
-
88
70
  export type CatalogFormSectionDefinition = {
89
71
  title: CatalogLocalizedText;
90
72
  description?: CatalogLocalizedText;
91
73
  fields: CatalogFormFieldDefinition[];
92
74
  };
93
-
94
75
  export type CatalogResourceDefinition = {
95
76
  resource: string;
96
77
  translationKey: string;
@@ -117,167 +98,197 @@ export type CatalogResourceDefinition = {
117
98
  };
118
99
 
119
100
  const ptEn = (pt: string, en: string): CatalogLocalizedText => ({ pt, en });
101
+ const relation = (endpoint: string, labelKeys: string[], options?: Partial<CatalogRelationDefinition>): CatalogRelationDefinition => ({
102
+ endpoint,
103
+ labelKeys,
104
+ searchParam: 'search',
105
+ ...options,
106
+ });
107
+ const fileField = (key: string, label: CatalogLocalizedText): CatalogFormFieldDefinition => ({
108
+ key,
109
+ label,
110
+ type: 'upload',
111
+ uploadDestination: `catalog/${key.replace(/_id$/, '')}`,
112
+ accept: 'image/*',
113
+ span: 2,
114
+ uploadPreviewVariant: 'square',
115
+ });
120
116
 
121
- const activeInactiveStatusFilterOptions: CatalogFilterOptionDefinition[] = [
117
+ const activeInactiveStatusFilterOptions = [
122
118
  { value: 'active', labelKey: 'active' },
123
119
  { value: 'inactive', labelKey: 'inactive' },
124
120
  ];
125
-
126
- const publicationStatusFilterOptions: CatalogFilterOptionDefinition[] = [
121
+ const publicationStatusFilterOptions = [
127
122
  { value: 'draft', labelKey: 'draft' },
128
123
  { value: 'published', labelKey: 'published' },
129
124
  { value: 'archived', labelKey: 'archived' },
130
125
  ];
131
-
132
- const availabilityFilterOptions: CatalogFilterOptionDefinition[] = [
126
+ const availabilityFilterOptions = [
133
127
  { value: 'in_stock', labelKey: 'inStock' },
134
128
  { value: 'out_of_stock', labelKey: 'outOfStock' },
135
- { value: 'preorder', labelKey: 'preorder' },
129
+ { value: 'pre_order', labelKey: 'preOrder' },
130
+ { value: 'unknown', labelKey: 'unknown' },
136
131
  ];
137
-
138
- const dataTypeFilterOptions: CatalogFilterOptionDefinition[] = [
132
+ const attributeTypeFilterOptions = [
139
133
  { value: 'text', labelKey: 'text' },
134
+ { value: 'long_text', labelKey: 'longText' },
140
135
  { value: 'number', labelKey: 'number' },
141
136
  { value: 'boolean', labelKey: 'boolean' },
142
- { value: 'json', labelKey: 'json' },
137
+ { value: 'option', labelKey: 'option' },
143
138
  ];
144
-
145
- const facetModeFilterOptions: CatalogFilterOptionDefinition[] = [
139
+ const facetModeFilterOptions = [
146
140
  { value: 'default', labelKey: 'default' },
147
141
  { value: 'range', labelKey: 'range' },
148
142
  { value: 'select', labelKey: 'select' },
143
+ { value: 'hidden', labelKey: 'hidden' },
149
144
  ];
150
145
 
151
146
  const activeInactiveStatusOptions = [
152
147
  { value: 'active', label: ptEn('Ativo', 'Active') },
153
148
  { value: 'inactive', label: ptEn('Inativo', 'Inactive') },
154
149
  ];
155
-
156
150
  const publicationStatusOptions = [
157
151
  { value: 'draft', label: ptEn('Rascunho', 'Draft') },
158
152
  { value: 'published', label: ptEn('Publicado', 'Published') },
159
153
  { value: 'archived', label: ptEn('Arquivado', 'Archived') },
160
154
  ];
161
-
162
155
  const comparisonStatusOptions = [
163
156
  { value: 'draft', label: ptEn('Rascunho', 'Draft') },
164
157
  { value: 'ready', label: ptEn('Pronto', 'Ready') },
165
158
  { value: 'disabled', label: ptEn('Desabilitado', 'Disabled') },
166
159
  ];
167
-
168
160
  const siteTypeOptions = [
169
161
  { value: 'portal', label: ptEn('Portal', 'Portal') },
170
- { value: 'storefront', label: ptEn('Loja', 'Storefront') },
171
- { value: 'landing_page', label: ptEn('Landing page', 'Landing page') },
162
+ { value: 'niche', label: ptEn('Nicho', 'Niche') },
163
+ { value: 'tenant', label: ptEn('Tenant', 'Tenant') },
172
164
  ];
173
-
174
- const dataTypeOptions = [
165
+ const attributeTypeOptions = [
175
166
  { value: 'text', label: ptEn('Texto', 'Text') },
167
+ { value: 'long_text', label: ptEn('Texto longo', 'Long text') },
176
168
  { value: 'number', label: ptEn('Número', 'Number') },
177
169
  { value: 'boolean', label: ptEn('Booleano', 'Boolean') },
178
- { value: 'json', label: ptEn('JSON', 'JSON') },
170
+ { value: 'option', label: ptEn('Opção', 'Option') },
179
171
  ];
180
-
181
172
  const comparisonModeOptions = [
182
173
  { value: 'neutral', label: ptEn('Neutro', 'Neutral') },
183
174
  { value: 'higher_better', label: ptEn('Maior é melhor', 'Higher is better') },
184
175
  { value: 'lower_better', label: ptEn('Menor é melhor', 'Lower is better') },
185
- { value: 'exact_match', label: ptEn('Comparação exata', 'Exact match') },
176
+ { value: 'boolean_true_better', label: ptEn('Verdadeiro é melhor', 'True is better') },
177
+ { value: 'exact_match', label: ptEn('Correspondência exata', 'Exact match') },
186
178
  ];
187
-
188
179
  const facetModeOptions = [
189
180
  { value: 'default', label: ptEn('Padrão', 'Default') },
190
181
  { value: 'range', label: ptEn('Faixa', 'Range') },
191
182
  { value: 'select', label: ptEn('Seleção', 'Select') },
183
+ { value: 'hidden', label: ptEn('Oculto', 'Hidden') },
192
184
  ];
193
-
194
185
  const comparisonTypeOptions = [
195
186
  { value: 'manual', label: ptEn('Manual', 'Manual') },
196
- { value: 'automated', label: ptEn('Automática', 'Automated') },
187
+ { value: 'automatic', label: ptEn('Automática', 'Automatic') },
188
+ { value: 'similarity', label: ptEn('Similaridade', 'Similarity') },
189
+ { value: 'compatibility', label: ptEn('Compatibilidade', 'Compatibility') },
197
190
  ];
198
-
199
191
  const generationModeOptions = [
200
192
  { value: 'manual', label: ptEn('Manual', 'Manual') },
201
193
  { value: 'automatic', label: ptEn('Automática', 'Automatic') },
202
- { value: 'ai_assisted', label: ptEn('Assistida por IA', 'AI assisted') },
203
194
  ];
204
-
205
195
  const availabilityOptions = [
206
196
  { value: 'in_stock', label: ptEn('Em estoque', 'In stock') },
207
197
  { value: 'out_of_stock', label: ptEn('Sem estoque', 'Out of stock') },
208
- { value: 'preorder', label: ptEn('Pré-venda', 'Pre-order') },
198
+ { value: 'pre_order', label: ptEn('Pré-venda', 'Pre-order') },
199
+ { value: 'unknown', label: ptEn('Desconhecido', 'Unknown') },
209
200
  ];
210
-
211
201
  const merchantTypeOptions = [
212
202
  { value: 'retailer', label: ptEn('Varejista', 'Retailer') },
213
203
  { value: 'marketplace', label: ptEn('Marketplace', 'Marketplace') },
214
- { value: 'brand_store', label: ptEn('Loja da marca', 'Brand store') },
204
+ { value: 'saas', label: ptEn('SaaS', 'SaaS') },
205
+ { value: 'direct', label: ptEn('Direto', 'Direct') },
215
206
  ];
216
-
217
207
  const networkTypeOptions = [
208
+ { value: 'amazon', label: ptEn('Amazon', 'Amazon') },
209
+ { value: 'mercado_livre', label: ptEn('Mercado Livre', 'Mercado Livre') },
210
+ { value: 'aliexpress', label: ptEn('AliExpress', 'AliExpress') },
211
+ { value: 'kabum', label: ptEn('KaBuM!', 'KaBuM!') },
218
212
  { value: 'direct', label: ptEn('Direto', 'Direct') },
219
213
  { value: 'network', label: ptEn('Rede', 'Network') },
220
- { value: 'api', label: ptEn('API', 'API') },
221
214
  ];
222
-
223
215
  const commissionTypeOptions = [
224
216
  { value: 'percentage', label: ptEn('Percentual', 'Percentage') },
225
- { value: 'fixed', label: ptEn('Valor fixo', 'Fixed amount') },
226
- { value: 'cpa', label: ptEn('CPA', 'CPA') },
227
- { value: 'cpl', label: ptEn('CPL', 'CPL') },
217
+ { value: 'fixed', label: ptEn('Valor fixo', 'Fixed') },
228
218
  ];
229
-
230
219
  const pageTypeOptions = [
231
220
  { value: 'comparison', label: ptEn('Comparação', 'Comparison') },
232
- { value: 'category', label: ptEn('Categoria', 'Category') },
233
- { value: 'ranking', label: ptEn('Ranking', 'Ranking') },
221
+ { value: 'best_of', label: ptEn('Melhores', 'Best of') },
222
+ { value: 'cost_benefit', label: ptEn('Custo-benefício', 'Cost benefit') },
223
+ { value: 'attribute', label: ptEn('Atributo', 'Attribute') },
224
+ { value: 'price_range', label: ptEn('Faixa de preço', 'Price range') },
225
+ { value: 'brand', label: ptEn('Marca', 'Brand') },
226
+ { value: 'use_case', label: ptEn('Caso de uso', 'Use case') },
234
227
  ];
235
-
236
228
  const canonicalStrategyOptions = [
237
- { value: 'self', label: ptEn('Self canonical', 'Self canonical') },
238
- { value: 'parent', label: ptEn('Canônica da categoria', 'Parent canonical') },
229
+ { value: 'self', label: ptEn('Canônica própria', 'Self canonical') },
230
+ { value: 'parent', label: ptEn('Canônica pai', 'Parent canonical') },
239
231
  { value: 'custom', label: ptEn('Canônica customizada', 'Custom canonical') },
240
232
  ];
241
-
242
233
  const sourceTypeOptions = [
243
234
  { value: 'api', label: ptEn('API', 'API') },
244
235
  { value: 'feed', label: ptEn('Feed', 'Feed') },
245
- { value: 'csv', label: ptEn('CSV', 'CSV') },
246
- { value: 'scraper', label: ptEn('Scraper', 'Scraper') },
236
+ { value: 'file', label: ptEn('Arquivo', 'File') },
237
+ { value: 'crawler', label: ptEn('Crawler', 'Crawler') },
247
238
  ];
248
-
249
239
  const currencyOptions = [
250
240
  { value: 'BRL', label: ptEn('Real brasileiro (BRL)', 'Brazilian real (BRL)') },
251
241
  { value: 'USD', label: ptEn('Dólar americano (USD)', 'US dollar (USD)') },
252
242
  { value: 'EUR', label: ptEn('Euro (EUR)', 'Euro (EUR)') },
253
243
  ];
254
244
 
255
- const relation = (
256
- endpoint: string,
257
- labelKeys: string[],
258
- options?: Partial<CatalogRelationDefinition>
259
- ): CatalogRelationDefinition => ({
260
- endpoint,
261
- labelKeys,
262
- searchParam: 'search',
263
- ...options,
264
- });
265
-
266
- const fileField = (
267
- key: string,
268
- label: CatalogLocalizedText
269
- ): CatalogFormFieldDefinition => ({
270
- key,
271
- label,
272
- type: 'upload',
273
- uploadDestination: `catalog/${key.replace(/_id$/, '')}`,
274
- accept: 'image/*',
275
- span: 2,
276
- uploadPreviewVariant: 'square',
277
- placeholder: ptEn('Envie uma imagem para representar este registro', 'Upload an image to represent this record'),
278
- });
245
+ export const catalogResources: CatalogResourceDefinition[] = [];
279
246
 
280
- export const catalogResources: CatalogResourceDefinition[] = [
247
+ catalogResources.push(
248
+ {
249
+ resource: 'categories',
250
+ translationKey: 'categories',
251
+ singularLabel: ptEn('Categoria', 'Category'),
252
+ createActionLabel: ptEn('Nova Categoria', 'New Category'),
253
+ editActionLabel: ptEn('Editar Categoria', 'Edit Category'),
254
+ icon: FolderTree,
255
+ href: '/catalog/categories',
256
+ colorClass: 'from-amber-500/20 via-orange-500/10 to-transparent',
257
+ glowClass: 'bg-amber-500/10 text-amber-700',
258
+ featured: true,
259
+ hasActiveStats: true,
260
+ listVariant: 'table',
261
+ primaryFilterField: 'status',
262
+ primaryFilterOptions: activeInactiveStatusFilterOptions,
263
+ titleFields: ['name', 'slug'],
264
+ descriptionFields: ['normalized_name', 'description'],
265
+ badgeFields: ['status'],
266
+ cardMetadata: [{ key: 'parent_category_id', labelKey: 'parentCategoryId' }, { key: 'comparison_enabled', labelKey: 'comparisonEnabled', type: 'boolean' }],
267
+ tableColumns: [
268
+ { key: 'id', labelKey: 'id' },
269
+ { key: 'name', labelKey: 'name' },
270
+ { key: 'slug', labelKey: 'slug' },
271
+ { key: 'parent_category_id', labelKey: 'parentCategoryId' },
272
+ { key: 'comparison_enabled', labelKey: 'comparisonEnabled', type: 'boolean' },
273
+ { key: 'sort_order', labelKey: 'sortOrder' },
274
+ { key: 'status', labelKey: 'status' },
275
+ ],
276
+ contextualKpi: { translationKey: 'visible', icon: Eye, count: (records) => records.length },
277
+ template: { parent_category_id: null, slug: '', name: '', normalized_name: '', description: '', comparison_enabled: true, status: 'active', sort_order: 0 },
278
+ formSections: [{
279
+ title: ptEn('Estrutura', 'Structure'),
280
+ fields: [
281
+ { key: 'name', label: ptEn('Nome', 'Name'), type: 'text', required: true },
282
+ { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true },
283
+ { key: 'normalized_name', label: ptEn('Nome normalizado', 'Normalized name'), type: 'text' },
284
+ { key: 'parent_category_id', label: ptEn('Categoria pai', 'Parent category'), type: 'relation', relation: relation('/catalog/categories', ['name', 'slug'], { resource: 'categories' }) },
285
+ { key: 'comparison_enabled', label: ptEn('Permite comparação', 'Comparison enabled'), type: 'switch' },
286
+ { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
287
+ { key: 'sort_order', label: ptEn('Ordem', 'Sort order'), type: 'number' },
288
+ { key: 'description', label: ptEn('Descrição', 'Description'), type: 'textarea', span: 2 },
289
+ ],
290
+ }],
291
+ },
281
292
  {
282
293
  resource: 'brands',
283
294
  translationKey: 'brands',
@@ -301,305 +312,19 @@ export const catalogResources: CatalogResourceDefinition[] = [
301
312
  { key: 'normalized_name', labelKey: 'normalizedName' },
302
313
  { key: 'website_url', labelKey: 'websiteUrl' },
303
314
  ],
304
- contextualKpi: {
305
- translationKey: 'visible',
306
- icon: Eye,
307
- count: (records) => records.length,
308
- },
309
- template: {
310
- slug: '',
311
- name: '',
312
- normalized_name: '',
313
- logo_file_id: null,
314
- status: 'active',
315
- website_url: '',
316
- },
317
- formSections: [
318
- {
319
- title: ptEn('Identificação', 'Identity'),
320
- fields: [
321
- { key: 'name', label: ptEn('Nome da marca', 'Brand name'), type: 'text', required: true, placeholder: ptEn('Ex.: Samsung', 'Ex.: Samsung') },
322
- { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true, placeholder: ptEn('Ex.: samsung', 'Ex.: samsung') },
323
- { key: 'normalized_name', label: ptEn('Nome normalizado', 'Normalized name'), type: 'text', placeholder: ptEn('Versão limpa para buscas e matching', 'Normalized value for search and matching') },
324
- { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
325
- { key: 'website_url', label: ptEn('Site oficial', 'Official website'), type: 'url', span: 2, placeholder: ptEn('https://www.sua-marca.com', 'https://www.your-brand.com') },
326
- fileField('logo_file_id', ptEn('Logo', 'Logo')),
327
- ],
328
- },
329
- ],
330
- },
331
- {
332
- resource: 'category-attributes',
333
- translationKey: 'categoryAttributes',
334
- singularLabel: ptEn('Atributo por categoria', 'Category attribute'),
335
- createActionLabel: ptEn('Novo Atributo por Categoria', 'New Category Attribute'),
336
- editActionLabel: ptEn('Editar Atributo por Categoria', 'Edit Category Attribute'),
337
- icon: BadgePercent,
338
- href: '/catalog/category-attributes',
339
- colorClass: 'from-pink-500/20 via-rose-500/10 to-transparent',
340
- glowClass: 'bg-pink-500/10 text-pink-700',
341
- listVariant: 'table',
342
- primaryFilterField: 'facet_mode',
343
- primaryFilterOptions: facetModeFilterOptions,
344
- titleFields: ['attribute_id', 'category_id'],
345
- descriptionFields: ['facet_mode'],
346
- badgeFields: ['facet_mode', 'is_required', 'is_highlighted'],
347
- cardMetadata: [
348
- { key: 'category_id', labelKey: 'categoryId' },
349
- { key: 'attribute_id', labelKey: 'attributeId' },
350
- { key: 'weight', labelKey: 'weight' },
351
- ],
352
- tableColumns: [
353
- { key: 'id', labelKey: 'id' },
354
- { key: 'category_id', labelKey: 'categoryId' },
355
- { key: 'attribute_id', labelKey: 'attributeId' },
356
- { key: 'facet_mode', labelKey: 'facetMode' },
357
- { key: 'is_required', labelKey: 'isRequired', type: 'boolean' },
358
- { key: 'is_highlighted', labelKey: 'isHighlighted', type: 'boolean' },
359
- ],
360
- contextualKpi: {
361
- translationKey: 'requiredInSlice',
362
- icon: BadgePercent,
363
- count: (records) => records.filter((record) => record.is_required === true).length,
364
- },
365
- template: {
366
- category_id: null,
367
- attribute_id: null,
368
- is_required: false,
369
- is_highlighted: true,
370
- display_order: 0,
371
- weight: 1,
372
- facet_mode: 'default',
373
- },
374
- formSections: [
375
- {
376
- title: ptEn('Relacionamentos', 'Relationships'),
377
- fields: [
378
- {
379
- key: 'category_id',
380
- label: ptEn('Categoria', 'Category'),
381
- type: 'relation',
382
- required: true,
383
- relation: relation('/category', ['name', 'slug'], { valueKey: 'category_id' }),
384
- },
385
- {
386
- key: 'attribute_id',
387
- label: ptEn('Atributo', 'Attribute'),
388
- type: 'relation',
389
- required: true,
390
- relation: relation('/catalog/attributes', ['label', 'slug'], {
391
- resource: 'attributes',
392
- createResource: 'attributes',
393
- allowCreate: true,
394
- }),
395
- },
396
- { key: 'facet_mode', label: ptEn('Modo de faceta', 'Facet mode'), type: 'select', required: true, options: facetModeOptions },
397
- { key: 'display_order', label: ptEn('Ordem de exibição', 'Display order'), type: 'number' },
398
- { key: 'weight', label: ptEn('Peso', 'Weight'), type: 'number' },
399
- { key: 'is_required', label: ptEn('Obrigatório', 'Required'), type: 'switch' },
400
- { key: 'is_highlighted', label: ptEn('Destacado', 'Highlighted'), type: 'switch' },
401
- ],
402
- },
403
- ],
404
- },
405
- {
406
- resource: 'comparisons',
407
- translationKey: 'comparisons',
408
- singularLabel: ptEn('Comparação', 'Comparison'),
409
- createActionLabel: ptEn('Nova Comparação', 'New Comparison'),
410
- editActionLabel: ptEn('Editar Comparação', 'Edit Comparison'),
411
- icon: Scale,
412
- href: '/catalog/comparisons',
413
- colorClass: 'from-amber-500/20 via-yellow-500/10 to-transparent',
414
- glowClass: 'bg-amber-500/10 text-amber-700',
415
- hasActiveStats: true,
416
- listVariant: 'cards',
417
- primaryFilterField: 'status',
418
- primaryFilterOptions: publicationStatusFilterOptions,
419
- titleFields: ['title', 'slug'],
420
- descriptionFields: ['summary', 'slug'],
421
- badgeFields: ['status', 'comparison_type', 'generation_mode'],
422
- cardMetadata: [
423
- { key: 'slug', labelKey: 'slug' },
424
- { key: 'site_id', labelKey: 'siteId' },
425
- { key: 'category_id', labelKey: 'categoryId' },
426
- ],
427
- contextualKpi: {
428
- translationKey: 'publishedInSlice',
429
- icon: Scale,
430
- count: (records) =>
431
- records.filter((record) =>
432
- String(record.status ?? '') === 'published'
433
- ).length,
434
- },
435
- template: {
436
- category_id: null,
437
- site_id: null,
438
- primary_content_id: null,
439
- slug: '',
440
- comparison_type: 'manual',
441
- generation_mode: 'manual',
442
- status: 'draft',
443
- title: '',
444
- summary: '',
445
- intro: '',
446
- verdict: '',
447
- spec_snapshot_json: {},
448
- eligibility_hash: '',
449
- published_at: '',
450
- },
451
- formSections: [
452
- {
453
- title: ptEn('Base da comparação', 'Comparison basics'),
454
- fields: [
455
- { key: 'title', label: ptEn('Título', 'Title'), type: 'text', required: true, placeholder: ptEn('Ex.: Melhor celular premium de 2026', 'Ex.: Best premium smartphone of 2026') },
456
- { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true, placeholder: ptEn('Ex.: melhor-celular-premium-2026', 'Ex.: best-premium-smartphone-2026') },
457
- {
458
- key: 'site_id',
459
- label: ptEn('Site', 'Site'),
460
- type: 'relation',
461
- required: true,
462
- relation: relation('/catalog/sites', ['name', 'domain', 'slug'], {
463
- resource: 'sites',
464
- createResource: 'sites',
465
- allowCreate: true,
466
- }),
467
- },
468
- { key: 'category_id', label: ptEn('Categoria', 'Category'), type: 'relation', relation: relation('/category', ['name', 'slug'], { valueKey: 'category_id' }) },
469
- { key: 'primary_content_id', label: ptEn('Conteúdo principal', 'Primary content'), type: 'relation', relation: relation('/content', ['title', 'slug'], { valueKey: 'content_id' }) },
470
- { key: 'comparison_type', label: ptEn('Tipo de comparação', 'Comparison type'), type: 'select', required: true, options: comparisonTypeOptions },
471
- { key: 'generation_mode', label: ptEn('Modo de geração', 'Generation mode'), type: 'select', required: true, options: generationModeOptions },
472
- { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: publicationStatusOptions },
473
- { key: 'published_at', label: ptEn('Publicado em', 'Published at'), type: 'datetime' },
474
- ],
475
- },
476
- {
477
- title: ptEn('Conteúdo editorial', 'Editorial content'),
478
- fields: [
479
- { key: 'summary', label: ptEn('Resumo', 'Summary'), type: 'richtext', span: 2, placeholder: ptEn('Resumo editorial da comparação', 'Editorial summary of the comparison') },
480
- { key: 'intro', label: ptEn('Introdução', 'Introduction'), type: 'richtext', span: 2, placeholder: ptEn('Contextualize o comparativo para o leitor', 'Add reader-facing context for the comparison') },
481
- { key: 'verdict', label: ptEn('Veredito', 'Verdict'), type: 'richtext', span: 2, placeholder: ptEn('Conclusão e recomendação final', 'Final conclusion and recommendation') },
482
- { key: 'eligibility_hash', label: ptEn('Hash de elegibilidade', 'Eligibility hash'), type: 'text', span: 2, placeholder: ptEn('Chave técnica para rastrear o conjunto elegível', 'Technical key to track the eligible set') },
483
- { key: 'spec_snapshot_json', label: ptEn('Snapshot de regras', 'Rule snapshot'), type: 'json', span: 2 },
484
- ],
485
- },
486
- ],
487
- },
488
- {
489
- resource: 'offers',
490
- translationKey: 'offers',
491
- singularLabel: ptEn('Oferta', 'Offer'),
492
- createActionLabel: ptEn('Nova Oferta', 'New Offer'),
493
- editActionLabel: ptEn('Editar Oferta', 'Edit Offer'),
494
- icon: ShoppingCart,
495
- href: '/catalog/offers',
496
- colorClass: 'from-red-500/20 via-orange-500/10 to-transparent',
497
- glowClass: 'bg-red-500/10 text-red-700',
498
- featured: true,
499
- listVariant: 'cards',
500
- primaryFilterField: 'availability_status',
501
- primaryFilterOptions: availabilityFilterOptions,
502
- titleFields: ['title', 'external_offer_id'],
503
- descriptionFields: ['external_offer_id', 'affiliate_url'],
504
- badgeFields: ['availability_status', 'is_featured'],
505
- cardMetadata: [
506
- { key: 'price_amount', labelKey: 'priceAmount', type: 'currency' },
507
- { key: 'merchant_id', labelKey: 'merchantId' },
508
- { key: 'product_id', labelKey: 'productId' },
509
- ],
510
- contextualKpi: {
511
- translationKey: 'inStockInSlice',
512
- icon: ShoppingCart,
513
- count: (records) => records.filter((record) => record.availability_status === 'in_stock').length,
514
- },
515
- template: {
516
- product_id: null,
517
- merchant_id: null,
518
- affiliate_program_id: null,
519
- site_id: null,
520
- external_offer_id: '',
521
- title: '',
522
- price_amount: 0,
523
- price_currency: 'BRL',
524
- original_price_amount: 0,
525
- installment_json: {},
526
- availability_status: 'in_stock',
527
- affiliate_url: '',
528
- deep_link_url: '',
529
- priority_score: 0,
530
- is_featured: false,
531
- valid_from: '',
532
- valid_until: '',
533
- last_seen_at: '',
534
- },
535
- formSections: [
536
- {
537
- title: ptEn('Oferta comercial', 'Commercial offer'),
538
- fields: [
539
- { key: 'title', label: ptEn('Título da oferta', 'Offer title'), type: 'text', required: true, span: 2, placeholder: ptEn('Ex.: Galaxy S25 Ultra 256GB por R$ 6.999', 'Ex.: Galaxy S25 Ultra 256GB for $1,299') },
540
- {
541
- key: 'product_id',
542
- label: ptEn('Produto', 'Product'),
543
- type: 'relation',
544
- required: true,
545
- relation: relation('/catalog/products', ['name', 'model_name', 'slug'], {
546
- resource: 'products',
547
- createResource: 'products',
548
- allowCreate: true,
549
- }),
550
- },
551
- {
552
- key: 'merchant_id',
553
- label: ptEn('Lojista', 'Merchant'),
554
- type: 'relation',
555
- required: true,
556
- relation: relation('/catalog/merchants', ['name', 'slug'], {
557
- resource: 'merchants',
558
- createResource: 'merchants',
559
- allowCreate: true,
560
- }),
561
- },
562
- {
563
- key: 'affiliate_program_id',
564
- label: ptEn('Programa de afiliação', 'Affiliate program'),
565
- type: 'relation',
566
- relation: relation('/catalog/affiliate-programs', ['name', 'slug'], {
567
- resource: 'affiliate-programs',
568
- createResource: 'affiliate-programs',
569
- allowCreate: true,
570
- }),
571
- },
572
- {
573
- key: 'site_id',
574
- label: ptEn('Site', 'Site'),
575
- type: 'relation',
576
- relation: relation('/catalog/sites', ['name', 'domain', 'slug'], {
577
- resource: 'sites',
578
- createResource: 'sites',
579
- allowCreate: true,
580
- }),
581
- },
582
- { key: 'external_offer_id', label: ptEn('ID externo', 'External ID'), type: 'text', placeholder: ptEn('ID da oferta no feed/API do parceiro', 'Offer id from partner feed/API') },
583
- { key: 'availability_status', label: ptEn('Disponibilidade', 'Availability'), type: 'select', required: true, options: availabilityOptions },
584
- { key: 'price_amount', label: ptEn('Preço', 'Price'), type: 'currency', required: true },
585
- { key: 'original_price_amount', label: ptEn('Preço original', 'Original price'), type: 'currency' },
586
- { key: 'price_currency', label: ptEn('Moeda', 'Currency'), type: 'select', required: true, options: currencyOptions },
587
- { key: 'priority_score', label: ptEn('Prioridade', 'Priority'), type: 'number' },
588
- { key: 'is_featured', label: ptEn('Oferta destacada', 'Featured offer'), type: 'switch' },
589
- ],
590
- },
591
- {
592
- title: ptEn('Links e vigência', 'Links and schedule'),
593
- fields: [
594
- { key: 'affiliate_url', label: ptEn('URL de afiliação', 'Affiliate URL'), type: 'url', span: 2, placeholder: ptEn('https://parceiro.com/r/abc123', 'https://partner.com/r/abc123') },
595
- { key: 'deep_link_url', label: ptEn('Deep link', 'Deep link'), type: 'url', span: 2, placeholder: ptEn('https://loja.com/produto/oferta', 'https://store.com/product/offer') },
596
- { key: 'valid_from', label: ptEn('Válida a partir de', 'Valid from'), type: 'datetime' },
597
- { key: 'valid_until', label: ptEn('Válida até', 'Valid until'), type: 'datetime' },
598
- { key: 'last_seen_at', label: ptEn('Última captura', 'Last seen at'), type: 'datetime' },
599
- { key: 'installment_json', label: ptEn('Parcelamento', 'Installments'), type: 'json', span: 2 },
600
- ],
601
- },
602
- ],
315
+ contextualKpi: { translationKey: 'visible', icon: Eye, count: (records) => records.length },
316
+ template: { slug: '', name: '', normalized_name: '', logo_file_id: null, status: 'active', website_url: '' },
317
+ formSections: [{
318
+ title: ptEn('Identificação', 'Identity'),
319
+ fields: [
320
+ { key: 'name', label: ptEn('Nome da marca', 'Brand name'), type: 'text', required: true },
321
+ { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true },
322
+ { key: 'normalized_name', label: ptEn('Nome normalizado', 'Normalized name'), type: 'text' },
323
+ { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
324
+ { key: 'website_url', label: ptEn('Site oficial', 'Official website'), type: 'url', span: 2 },
325
+ fileField('logo_file_id', ptEn('Logo', 'Logo')),
326
+ ],
327
+ }],
603
328
  },
604
329
  {
605
330
  resource: 'sites',
@@ -611,6 +336,7 @@ export const catalogResources: CatalogResourceDefinition[] = [
611
336
  href: '/catalog/sites',
612
337
  colorClass: 'from-sky-500/20 via-cyan-500/10 to-transparent',
613
338
  glowClass: 'bg-sky-500/10 text-sky-700',
339
+ featured: true,
614
340
  hasActiveStats: true,
615
341
  listVariant: 'cards',
616
342
  primaryFilterField: 'status',
@@ -623,36 +349,19 @@ export const catalogResources: CatalogResourceDefinition[] = [
623
349
  { key: 'domain', labelKey: 'domain' },
624
350
  { key: 'default_locale_id', labelKey: 'defaultLocaleId' },
625
351
  ],
626
- contextualKpi: {
627
- translationKey: 'visible',
628
- icon: Eye,
629
- count: (records) => records.length,
630
- },
631
- template: {
632
- slug: '',
633
- name: '',
634
- domain: '',
635
- status: 'active',
636
- site_type: 'portal',
637
- default_locale_id: null,
638
- theme_settings_json: {},
639
- seo_defaults_json: {},
640
- },
352
+ contextualKpi: { translationKey: 'visible', icon: Eye, count: (records) => records.length },
353
+ template: { slug: '', name: '', domain: '', status: 'active', site_type: 'portal', default_locale_id: null, logo_file_id: null, theme_settings_json: {}, seo_defaults_json: {} },
641
354
  formSections: [
642
355
  {
643
356
  title: ptEn('Dados principais', 'Main details'),
644
357
  fields: [
645
- { key: 'name', label: ptEn('Nome do site', 'Site name'), type: 'text', required: true, placeholder: ptEn('Ex.: Tech Radar BR', 'Ex.: Tech Radar BR') },
646
- { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true, placeholder: ptEn('Ex.: tech-radar-br', 'Ex.: tech-radar-br') },
647
- { key: 'domain', label: ptEn('Domínio', 'Domain'), type: 'url', required: true, placeholder: ptEn('https://www.exemplo.com', 'https://www.example.com') },
358
+ { key: 'name', label: ptEn('Nome do site', 'Site name'), type: 'text', required: true },
359
+ { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true },
360
+ { key: 'domain', label: ptEn('Domínio', 'Domain'), type: 'url', required: true },
648
361
  { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
649
362
  { key: 'site_type', label: ptEn('Tipo de site', 'Site type'), type: 'select', required: true, options: siteTypeOptions },
650
- {
651
- key: 'default_locale_id',
652
- label: ptEn('Locale padrão', 'Default locale'),
653
- type: 'relation',
654
- relation: relation('/locale', ['name', 'code'], { valueKey: 'id' }),
655
- },
363
+ { key: 'default_locale_id', label: ptEn('Locale padrão', 'Default locale'), type: 'relation', required: true, relation: relation('/locale', ['name', 'code']) },
364
+ fileField('logo_file_id', ptEn('Logo', 'Logo')),
656
365
  ],
657
366
  },
658
367
  {
@@ -676,7 +385,7 @@ export const catalogResources: CatalogResourceDefinition[] = [
676
385
  glowClass: 'bg-emerald-500/10 text-emerald-700',
677
386
  featured: true,
678
387
  hasActiveStats: true,
679
- listVariant: 'cards',
388
+ listVariant: 'table',
680
389
  primaryFilterField: 'status',
681
390
  primaryFilterOptions: publicationStatusFilterOptions,
682
391
  titleFields: ['name', 'model_name', 'slug'],
@@ -685,69 +394,30 @@ export const catalogResources: CatalogResourceDefinition[] = [
685
394
  cardMetadata: [
686
395
  { key: 'slug', labelKey: 'slug' },
687
396
  { key: 'sku', labelKey: 'sku' },
688
- { key: 'brand_id', labelKey: 'brandId' },
689
- { key: 'category_id', labelKey: 'categoryId' },
397
+ { key: 'brand_name', labelKey: 'brandId' },
398
+ { key: 'category_name', labelKey: 'catalogCategoryId' },
690
399
  ],
691
- contextualKpi: {
692
- translationKey: 'activeInSlice',
693
- icon: PackageSearch,
694
- count: (records) => records.filter((record) => record.is_active === true).length,
695
- },
696
- template: {
697
- brand_id: null,
698
- category_id: null,
699
- primary_content_id: null,
700
- slug: '',
701
- name: '',
702
- short_description: '',
703
- description: '',
704
- model_name: '',
705
- sku: '',
706
- gtin: '',
707
- status: 'draft',
708
- comparison_status: 'draft',
709
- release_date: '',
710
- spec_snapshot_json: {},
711
- comparison_snapshot_json: {},
712
- is_active: true,
713
- },
400
+ tableColumns: [
401
+ { key: 'name', labelKey: 'name' },
402
+ { key: 'brand_name', labelKey: 'brandId' },
403
+ { key: 'category_name', labelKey: 'catalogCategoryId' },
404
+ { key: 'status', labelKey: 'status' },
405
+ { key: 'is_active', labelKey: 'isActive', type: 'boolean' },
406
+ ],
407
+ contextualKpi: { translationKey: 'activeInSlice', icon: PackageSearch, count: (records) => records.filter((record) => record.is_active === true).length },
408
+ template: { brand_id: null, catalog_category_id: null, primary_content_id: null, slug: '', name: '', short_description: '', description: '', model_name: '', sku: '', gtin: '', status: 'draft', comparison_status: 'draft', release_date: '', spec_snapshot_json: {}, comparison_snapshot_json: {}, is_active: true },
714
409
  formSections: [
715
410
  {
716
411
  title: ptEn('Base do produto', 'Product basics'),
717
412
  fields: [
718
- { key: 'name', label: ptEn('Nome do produto', 'Product name'), type: 'text', required: true, placeholder: ptEn('Ex.: Galaxy S25 Ultra', 'Ex.: Galaxy S25 Ultra') },
719
- { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true, placeholder: ptEn('Ex.: galaxy-s25-ultra', 'Ex.: galaxy-s25-ultra') },
720
- {
721
- key: 'brand_id',
722
- label: ptEn('Marca', 'Brand'),
723
- type: 'relation',
724
- required: true,
725
- relation: relation('/catalog/brands', ['name', 'slug'], {
726
- resource: 'brands',
727
- createResource: 'brands',
728
- allowCreate: true,
729
- }),
730
- },
731
- {
732
- key: 'category_id',
733
- label: ptEn('Categoria', 'Category'),
734
- type: 'relation',
735
- required: true,
736
- relation: relation('/category', ['name', 'slug'], {
737
- valueKey: 'category_id',
738
- }),
739
- },
740
- {
741
- key: 'primary_content_id',
742
- label: ptEn('Conteúdo principal', 'Primary content'),
743
- type: 'relation',
744
- relation: relation('/content', ['title', 'slug'], {
745
- valueKey: 'content_id',
746
- }),
747
- },
748
- { key: 'model_name', label: ptEn('Modelo', 'Model'), type: 'text', placeholder: ptEn('Nome técnico ou comercial do modelo', 'Technical or commercial model name') },
749
- { key: 'sku', label: ptEn('SKU', 'SKU'), type: 'text', placeholder: ptEn('Código interno de estoque', 'Internal stock code') },
750
- { key: 'gtin', label: ptEn('GTIN', 'GTIN'), type: 'text', placeholder: ptEn('Código de barras/GTIN', 'Barcode / GTIN') },
413
+ { key: 'name', label: ptEn('Nome do produto', 'Product name'), type: 'text', required: true },
414
+ { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true },
415
+ { key: 'brand_id', label: ptEn('Marca', 'Brand'), type: 'relation', required: true, relation: relation('/catalog/brands', ['name', 'slug'], { resource: 'brands', createResource: 'brands', allowCreate: true }) },
416
+ { key: 'catalog_category_id', label: ptEn('Categoria', 'Category'), type: 'relation', required: true, relation: relation('/catalog/categories', ['name', 'slug'], { resource: 'categories', createResource: 'categories', allowCreate: true }) },
417
+ { key: 'primary_content_id', label: ptEn('Conteúdo principal', 'Primary content'), type: 'relation', relation: relation('/content', ['title', 'slug']) },
418
+ { key: 'model_name', label: ptEn('Modelo', 'Model'), type: 'text' },
419
+ { key: 'sku', label: ptEn('SKU', 'SKU'), type: 'text' },
420
+ { key: 'gtin', label: ptEn('GTIN', 'GTIN'), type: 'text' },
751
421
  { key: 'release_date', label: ptEn('Data de lançamento', 'Release date'), type: 'date' },
752
422
  { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: publicationStatusOptions },
753
423
  { key: 'comparison_status', label: ptEn('Status de comparação', 'Comparison status'), type: 'select', required: true, options: comparisonStatusOptions },
@@ -757,136 +427,116 @@ export const catalogResources: CatalogResourceDefinition[] = [
757
427
  {
758
428
  title: ptEn('Conteúdo', 'Content'),
759
429
  fields: [
760
- { key: 'short_description', label: ptEn('Resumo curto', 'Short description'), type: 'richtext', span: 2, placeholder: ptEn('Resumo rápido para cards e snippets', 'Short summary for cards and snippets') },
761
- { key: 'description', label: ptEn('Descrição completa', 'Full description'), type: 'richtext', span: 2, placeholder: ptEn('Detalhes completos, diferenciais e contexto do produto', 'Full product details, highlights and context') },
430
+ { key: 'short_description', label: ptEn('Resumo curto', 'Short description'), type: 'richtext', span: 2 },
431
+ { key: 'description', label: ptEn('Descrição completa', 'Full description'), type: 'richtext', span: 2 },
432
+ ],
433
+ },
434
+ {
435
+ title: ptEn('Snapshots', 'Snapshots'),
436
+ description: ptEn('Campos secundários para cache, leitura legada e payloads desnormalizados.', 'Secondary fields for cache, legacy reads, and denormalized payloads.'),
437
+ fields: [
762
438
  { key: 'spec_snapshot_json', label: ptEn('Snapshot de especificações', 'Specification snapshot'), type: 'json', span: 2 },
763
439
  { key: 'comparison_snapshot_json', label: ptEn('Snapshot de comparação', 'Comparison snapshot'), type: 'json', span: 2 },
764
440
  ],
765
441
  },
766
442
  ],
767
- },
443
+ }
444
+ );
445
+
446
+ catalogResources.push(
768
447
  {
769
- resource: 'attribute-groups',
770
- translationKey: 'attributeGroups',
771
- singularLabel: ptEn('Grupo de atributos', 'Attribute group'),
772
- createActionLabel: ptEn('Novo Grupo de Atributos', 'New Attribute Group'),
773
- editActionLabel: ptEn('Editar Grupo de Atributos', 'Edit Attribute Group'),
774
- icon: Layers3,
775
- href: '/catalog/attribute-groups',
776
- colorClass: 'from-violet-500/20 via-fuchsia-500/10 to-transparent',
777
- glowClass: 'bg-violet-500/10 text-violet-700',
448
+ resource: 'comparisons',
449
+ translationKey: 'comparisons',
450
+ singularLabel: ptEn('Comparação', 'Comparison'),
451
+ createActionLabel: ptEn('Nova Comparação', 'New Comparison'),
452
+ editActionLabel: ptEn('Editar Comparação', 'Edit Comparison'),
453
+ icon: Scale,
454
+ href: '/catalog/comparisons',
455
+ colorClass: 'from-amber-500/20 via-yellow-500/10 to-transparent',
456
+ glowClass: 'bg-amber-500/10 text-amber-700',
778
457
  hasActiveStats: true,
779
- listVariant: 'table',
458
+ listVariant: 'cards',
780
459
  primaryFilterField: 'status',
781
- primaryFilterOptions: activeInactiveStatusFilterOptions,
782
- titleFields: ['name', 'slug'],
783
- descriptionFields: ['slug'],
784
- badgeFields: ['status'],
785
- cardMetadata: [
786
- { key: 'slug', labelKey: 'slug' },
787
- { key: 'status', labelKey: 'status' },
788
- ],
789
- tableColumns: [
790
- { key: 'id', labelKey: 'id' },
791
- { key: 'name', labelKey: 'name' },
792
- { key: 'slug', labelKey: 'slug' },
793
- { key: 'status', labelKey: 'status' },
794
- ],
795
- contextualKpi: {
796
- translationKey: 'visible',
797
- icon: Eye,
798
- count: (records) => records.length,
799
- },
800
- template: { slug: '', name: '', status: 'active' },
460
+ primaryFilterOptions: publicationStatusFilterOptions,
461
+ titleFields: ['title', 'slug'],
462
+ descriptionFields: ['summary', 'slug'],
463
+ badgeFields: ['status', 'comparison_type', 'generation_mode'],
464
+ cardMetadata: [{ key: 'slug', labelKey: 'slug' }, { key: 'site_id', labelKey: 'siteId' }, { key: 'catalog_category_id', labelKey: 'catalogCategoryId' }],
465
+ contextualKpi: { translationKey: 'publishedInSlice', icon: Scale, count: (records) => records.filter((record) => String(record.status ?? '') === 'published').length },
466
+ template: { catalog_category_id: null, site_id: null, primary_content_id: null, slug: '', comparison_type: 'manual', generation_mode: 'manual', status: 'draft', title: '', summary: '', intro: '', verdict: '', eligibility_hash: '', published_at: '' },
801
467
  formSections: [
802
468
  {
803
- title: ptEn('Grupo', 'Group'),
469
+ title: ptEn('Base da comparação', 'Comparison basics'),
804
470
  fields: [
805
- { key: 'name', label: ptEn('Nome do grupo', 'Group name'), type: 'text', required: true, placeholder: ptEn('Ex.: Tela e Display', 'Ex.: Display and Screen') },
806
- { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true, placeholder: ptEn('Ex.: tela-e-display', 'Ex.: display-and-screen') },
807
- { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
471
+ { key: 'title', label: ptEn('Título', 'Title'), type: 'text', required: true },
472
+ { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true },
473
+ { key: 'site_id', label: ptEn('Site', 'Site'), type: 'relation', required: true, relation: relation('/catalog/sites', ['name', 'domain', 'slug'], { resource: 'sites', createResource: 'sites', allowCreate: true }) },
474
+ { key: 'catalog_category_id', label: ptEn('Categoria', 'Category'), type: 'relation', required: true, relation: relation('/catalog/categories', ['name', 'slug'], { resource: 'categories' }) },
475
+ { key: 'primary_content_id', label: ptEn('Conteúdo principal', 'Primary content'), type: 'relation', relation: relation('/content', ['title', 'slug']) },
476
+ { key: 'comparison_type', label: ptEn('Tipo de comparação', 'Comparison type'), type: 'select', required: true, options: comparisonTypeOptions },
477
+ { key: 'generation_mode', label: ptEn('Modo de geração', 'Generation mode'), type: 'select', required: true, options: generationModeOptions },
478
+ { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: publicationStatusOptions },
479
+ { key: 'published_at', label: ptEn('Publicado em', 'Published at'), type: 'datetime' },
480
+ ],
481
+ },
482
+ {
483
+ title: ptEn('Conteúdo editorial', 'Editorial content'),
484
+ fields: [
485
+ { key: 'summary', label: ptEn('Resumo', 'Summary'), type: 'richtext', span: 2 },
486
+ { key: 'intro', label: ptEn('Introdução', 'Introduction'), type: 'richtext', span: 2 },
487
+ { key: 'verdict', label: ptEn('Veredito', 'Verdict'), type: 'richtext', span: 2 },
488
+ { key: 'eligibility_hash', label: ptEn('Hash de elegibilidade', 'Eligibility hash'), type: 'text', span: 2 },
808
489
  ],
809
490
  },
810
491
  ],
811
492
  },
812
493
  {
813
- resource: 'attributes',
814
- translationKey: 'attributes',
815
- singularLabel: ptEn('Atributo', 'Attribute'),
816
- createActionLabel: ptEn('Novo Atributo', 'New Attribute'),
817
- editActionLabel: ptEn('Editar Atributo', 'Edit Attribute'),
818
- icon: Tags,
819
- href: '/catalog/attributes',
820
- colorClass: 'from-indigo-500/20 via-blue-500/10 to-transparent',
821
- glowClass: 'bg-indigo-500/10 text-indigo-700',
822
- listVariant: 'table',
823
- primaryFilterField: 'data_type',
824
- primaryFilterOptions: dataTypeFilterOptions,
825
- titleFields: ['label', 'slug'],
826
- descriptionFields: ['description', 'slug'],
827
- badgeFields: ['data_type', 'comparison_mode'],
828
- cardMetadata: [
829
- { key: 'group_id', labelKey: 'groupId' },
830
- { key: 'is_filterable', labelKey: 'isFilterable', type: 'boolean' },
831
- { key: 'is_required_for_comparison', labelKey: 'requiredForComparison', type: 'boolean' },
832
- ],
833
- tableColumns: [
834
- { key: 'id', labelKey: 'id' },
835
- { key: 'label', labelKey: 'label' },
836
- { key: 'slug', labelKey: 'slug' },
837
- { key: 'data_type', labelKey: 'dataType' },
838
- { key: 'comparison_mode', labelKey: 'comparisonMode' },
839
- ],
840
- contextualKpi: {
841
- translationKey: 'filterableInSlice',
842
- icon: ListFilter,
843
- count: (records) => records.filter((record) => record.is_filterable === true).length,
844
- },
845
- template: {
846
- group_id: null,
847
- slug: '',
848
- label: '',
849
- description: '',
850
- data_type: 'text',
851
- unit: '',
852
- comparison_mode: 'neutral',
853
- is_filterable: true,
854
- is_sortable: false,
855
- is_required_for_comparison: false,
856
- normalization_rule_json: {},
857
- display_order: 0,
858
- },
494
+ resource: 'offers',
495
+ translationKey: 'offers',
496
+ singularLabel: ptEn('Oferta', 'Offer'),
497
+ createActionLabel: ptEn('Nova Oferta', 'New Offer'),
498
+ editActionLabel: ptEn('Editar Oferta', 'Edit Offer'),
499
+ icon: ShoppingCart,
500
+ href: '/catalog/offers',
501
+ colorClass: 'from-red-500/20 via-orange-500/10 to-transparent',
502
+ glowClass: 'bg-red-500/10 text-red-700',
503
+ featured: true,
504
+ listVariant: 'cards',
505
+ primaryFilterField: 'availability_status',
506
+ primaryFilterOptions: availabilityFilterOptions,
507
+ titleFields: ['title', 'external_offer_id'],
508
+ descriptionFields: ['external_offer_id', 'affiliate_url'],
509
+ badgeFields: ['availability_status', 'is_featured'],
510
+ cardMetadata: [{ key: 'price_amount', labelKey: 'priceAmount', type: 'currency' }, { key: 'merchant_id', labelKey: 'merchantId' }, { key: 'product_id', labelKey: 'productId' }],
511
+ contextualKpi: { translationKey: 'inStockInSlice', icon: ShoppingCart, count: (records) => records.filter((record) => record.availability_status === 'in_stock').length },
512
+ template: { product_id: null, merchant_id: null, affiliate_program_id: null, site_id: null, external_offer_id: '', title: '', price_amount: 0, price_currency: 'BRL', original_price_amount: null, installment_json: {}, availability_status: 'in_stock', affiliate_url: '', deep_link_url: '', priority_score: 0, is_featured: false, valid_from: '', valid_until: '', last_seen_at: '' },
859
513
  formSections: [
860
514
  {
861
- title: ptEn('Configuração do atributo', 'Attribute setup'),
515
+ title: ptEn('Oferta comercial', 'Commercial offer'),
862
516
  fields: [
863
- { key: 'label', label: ptEn('Label', 'Label'), type: 'text', required: true, placeholder: ptEn('Ex.: Tamanho da tela', 'Ex.: Screen size') },
864
- { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true, placeholder: ptEn('Ex.: tamanho-da-tela', 'Ex.: screen-size') },
865
- {
866
- key: 'group_id',
867
- label: ptEn('Grupo', 'Group'),
868
- type: 'relation',
869
- required: true,
870
- relation: relation('/catalog/attribute-groups', ['name', 'slug'], {
871
- resource: 'attribute-groups',
872
- createResource: 'attribute-groups',
873
- allowCreate: true,
874
- }),
875
- },
876
- { key: 'data_type', label: ptEn('Tipo de dado', 'Data type'), type: 'select', required: true, options: dataTypeOptions },
877
- { key: 'unit', label: ptEn('Unidade', 'Unit'), type: 'text', placeholder: ptEn('Ex.: polegadas, GB, kg', 'Ex.: inches, GB, kg') },
878
- { key: 'comparison_mode', label: ptEn('Modo de comparação', 'Comparison mode'), type: 'select', required: true, options: comparisonModeOptions },
879
- { key: 'display_order', label: ptEn('Ordem de exibição', 'Display order'), type: 'number' },
880
- { key: 'description', label: ptEn('Descrição', 'Description'), type: 'richtext', span: 2, placeholder: ptEn('Explique como o atributo deve ser usado e exibido', 'Explain how the attribute should be used and displayed') },
517
+ { key: 'title', label: ptEn('Título da oferta', 'Offer title'), type: 'text', required: true, span: 2 },
518
+ { key: 'product_id', label: ptEn('Produto', 'Product'), type: 'relation', required: true, relation: relation('/catalog/products', ['name', 'model_name', 'slug'], { resource: 'products' }) },
519
+ { key: 'merchant_id', label: ptEn('Lojista', 'Merchant'), type: 'relation', required: true, relation: relation('/catalog/merchants', ['name', 'slug'], { resource: 'merchants', createResource: 'merchants', allowCreate: true }) },
520
+ { key: 'affiliate_program_id', label: ptEn('Programa de afiliação', 'Affiliate program'), type: 'relation', relation: relation('/catalog/affiliate-programs', ['name', 'slug'], { resource: 'affiliate-programs', createResource: 'affiliate-programs', allowCreate: true }) },
521
+ { key: 'site_id', label: ptEn('Site', 'Site'), type: 'relation', relation: relation('/catalog/sites', ['name', 'domain', 'slug'], { resource: 'sites' }) },
522
+ { key: 'external_offer_id', label: ptEn('ID externo', 'External ID'), type: 'text' },
523
+ { key: 'availability_status', label: ptEn('Disponibilidade', 'Availability'), type: 'select', required: true, options: availabilityOptions },
524
+ { key: 'price_amount', label: ptEn('Preço', 'Price'), type: 'currency', required: true },
525
+ { key: 'original_price_amount', label: ptEn('Preço original', 'Original price'), type: 'currency' },
526
+ { key: 'price_currency', label: ptEn('Moeda', 'Currency'), type: 'select', required: true, options: currencyOptions },
527
+ { key: 'priority_score', label: ptEn('Prioridade', 'Priority'), type: 'number' },
528
+ { key: 'is_featured', label: ptEn('Oferta destacada', 'Featured offer'), type: 'switch' },
881
529
  ],
882
530
  },
883
531
  {
884
- title: ptEn('Regras', 'Rules'),
532
+ title: ptEn('Links e vigência', 'Links and schedule'),
885
533
  fields: [
886
- { key: 'is_filterable', label: ptEn('Filtrável', 'Filterable'), type: 'switch' },
887
- { key: 'is_sortable', label: ptEn('Ordenável', 'Sortable'), type: 'switch' },
888
- { key: 'is_required_for_comparison', label: ptEn('Obrigatório para comparação', 'Required for comparison'), type: 'switch' },
889
- { key: 'normalization_rule_json', label: ptEn('Regra de normalização', 'Normalization rule'), type: 'json', span: 2 },
534
+ { key: 'affiliate_url', label: ptEn('URL de afiliação', 'Affiliate URL'), type: 'url', span: 2 },
535
+ { key: 'deep_link_url', label: ptEn('Deep link', 'Deep link'), type: 'url', span: 2 },
536
+ { key: 'valid_from', label: ptEn('Válida a partir de', 'Valid from'), type: 'datetime' },
537
+ { key: 'valid_until', label: ptEn('Válida até', 'Valid until'), type: 'datetime' },
538
+ { key: 'last_seen_at', label: ptEn('Última captura', 'Last seen at'), type: 'datetime' },
539
+ { key: 'installment_json', label: ptEn('Parcelamento', 'Installments'), type: 'json', span: 2 },
890
540
  ],
891
541
  },
892
542
  ],
@@ -909,35 +559,19 @@ export const catalogResources: CatalogResourceDefinition[] = [
909
559
  titleFields: ['name', 'slug'],
910
560
  descriptionFields: ['slug'],
911
561
  badgeFields: ['status', 'merchant_type'],
912
- cardMetadata: [
913
- { key: 'slug', labelKey: 'slug' },
914
- { key: 'merchant_type', labelKey: 'merchantType' },
915
- { key: 'logo_file_id', labelKey: 'logoFileId' },
916
- ],
917
- contextualKpi: {
918
- translationKey: 'visible',
919
- icon: Eye,
920
- count: (records) => records.length,
921
- },
922
- template: {
923
- slug: '',
924
- name: '',
925
- status: 'active',
926
- merchant_type: 'retailer',
927
- logo_file_id: null,
928
- },
929
- formSections: [
930
- {
931
- title: ptEn('Perfil do lojista', 'Merchant profile'),
932
- fields: [
933
- { key: 'name', label: ptEn('Nome do lojista', 'Merchant name'), type: 'text', required: true, placeholder: ptEn('Ex.: Magazine Luiza', 'Ex.: Best Buy') },
934
- { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true, placeholder: ptEn('Ex.: magazine-luiza', 'Ex.: best-buy') },
935
- { key: 'merchant_type', label: ptEn('Tipo de lojista', 'Merchant type'), type: 'select', required: true, options: merchantTypeOptions },
936
- { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
937
- fileField('logo_file_id', ptEn('Logo', 'Logo')),
938
- ],
939
- },
940
- ],
562
+ cardMetadata: [{ key: 'slug', labelKey: 'slug' }, { key: 'merchant_type', labelKey: 'merchantType' }, { key: 'logo_file_id', labelKey: 'logoFileId' }],
563
+ contextualKpi: { translationKey: 'visible', icon: Eye, count: (records) => records.length },
564
+ template: { slug: '', name: '', status: 'active', merchant_type: 'retailer', logo_file_id: null },
565
+ formSections: [{
566
+ title: ptEn('Perfil do lojista', 'Merchant profile'),
567
+ fields: [
568
+ { key: 'name', label: ptEn('Nome do lojista', 'Merchant name'), type: 'text', required: true },
569
+ { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true },
570
+ { key: 'merchant_type', label: ptEn('Tipo de lojista', 'Merchant type'), type: 'select', required: true, options: merchantTypeOptions },
571
+ { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
572
+ fileField('logo_file_id', ptEn('Logo', 'Logo')),
573
+ ],
574
+ }],
941
575
  },
942
576
  {
943
577
  resource: 'affiliate-programs',
@@ -956,51 +590,22 @@ export const catalogResources: CatalogResourceDefinition[] = [
956
590
  titleFields: ['name', 'slug'],
957
591
  descriptionFields: ['slug', 'network_type'],
958
592
  badgeFields: ['status', 'network_type', 'commission_type'],
959
- cardMetadata: [
960
- { key: 'merchant_id', labelKey: 'merchantId' },
961
- { key: 'commission_type', labelKey: 'commissionType' },
962
- { key: 'default_commission_value', labelKey: 'defaultCommissionValue', type: 'currency' },
963
- ],
964
- contextualKpi: {
965
- translationKey: 'visible',
966
- icon: Eye,
967
- count: (records) => records.length,
968
- },
969
- template: {
970
- merchant_id: null,
971
- slug: '',
972
- name: '',
973
- network_type: 'direct',
974
- tracking_template: '',
975
- commission_type: 'percentage',
976
- default_commission_value: 0,
977
- status: 'active',
978
- },
979
- formSections: [
980
- {
981
- title: ptEn('Programa', 'Program'),
982
- fields: [
983
- { key: 'name', label: ptEn('Nome do programa', 'Program name'), type: 'text', required: true, placeholder: ptEn('Ex.: Programa Magalu Ads', 'Ex.: Best Buy Affiliate Program') },
984
- { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true, placeholder: ptEn('Ex.: magalu-ads', 'Ex.: best-buy-affiliate') },
985
- {
986
- key: 'merchant_id',
987
- label: ptEn('Lojista', 'Merchant'),
988
- type: 'relation',
989
- required: true,
990
- relation: relation('/catalog/merchants', ['name', 'slug'], {
991
- resource: 'merchants',
992
- createResource: 'merchants',
993
- allowCreate: true,
994
- }),
995
- },
996
- { key: 'network_type', label: ptEn('Rede', 'Network'), type: 'select', required: true, options: networkTypeOptions },
997
- { key: 'commission_type', label: ptEn('Tipo de comissão', 'Commission type'), type: 'select', required: true, options: commissionTypeOptions },
998
- { key: 'default_commission_value', label: ptEn('Comissão padrão', 'Default commission'), type: 'currency' },
999
- { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
1000
- { key: 'tracking_template', label: ptEn('Template de tracking', 'Tracking template'), type: 'textarea', span: 2, placeholder: ptEn('Template com macros e parâmetros de tracking', 'Template with tracking macros and parameters') },
1001
- ],
1002
- },
1003
- ],
593
+ cardMetadata: [{ key: 'merchant_id', labelKey: 'merchantId' }, { key: 'commission_type', labelKey: 'commissionType' }, { key: 'default_commission_value', labelKey: 'defaultCommissionValue', type: 'currency' }],
594
+ contextualKpi: { translationKey: 'visible', icon: Eye, count: (records) => records.length },
595
+ template: { merchant_id: null, slug: '', name: '', network_type: 'direct', tracking_template: '', commission_type: 'percentage', default_commission_value: 0, status: 'active' },
596
+ formSections: [{
597
+ title: ptEn('Programa', 'Program'),
598
+ fields: [
599
+ { key: 'name', label: ptEn('Nome do programa', 'Program name'), type: 'text', required: true },
600
+ { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true },
601
+ { key: 'merchant_id', label: ptEn('Lojista', 'Merchant'), type: 'relation', required: true, relation: relation('/catalog/merchants', ['name', 'slug'], { resource: 'merchants', createResource: 'merchants', allowCreate: true }) },
602
+ { key: 'network_type', label: ptEn('Rede', 'Network'), type: 'select', required: true, options: networkTypeOptions },
603
+ { key: 'commission_type', label: ptEn('Tipo de comissão', 'Commission type'), type: 'select', required: true, options: commissionTypeOptions },
604
+ { key: 'default_commission_value', label: ptEn('Comissão padrão', 'Default commission'), type: 'currency' },
605
+ { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
606
+ { key: 'tracking_template', label: ptEn('Template de tracking', 'Tracking template'), type: 'textarea', span: 2 },
607
+ ],
608
+ }],
1004
609
  },
1005
610
  {
1006
611
  resource: 'seo-rules',
@@ -1020,47 +625,17 @@ export const catalogResources: CatalogResourceDefinition[] = [
1020
625
  titleFields: ['rule_slug', 'page_type'],
1021
626
  descriptionFields: ['page_type', 'canonical_strategy'],
1022
627
  badgeFields: ['status', 'page_type', 'canonical_strategy'],
1023
- cardMetadata: [
1024
- { key: 'site_id', labelKey: 'siteId' },
1025
- { key: 'category_id', labelKey: 'categoryId' },
1026
- { key: 'priority', labelKey: 'priority' },
1027
- ],
1028
- contextualKpi: {
1029
- translationKey: 'visible',
1030
- icon: Eye,
1031
- count: (records) => records.length,
1032
- },
1033
- template: {
1034
- site_id: null,
1035
- category_id: null,
1036
- page_type: 'comparison',
1037
- rule_slug: '',
1038
- status: 'active',
1039
- generation_query_json: {},
1040
- min_product_count: 2,
1041
- min_attribute_coverage: 1,
1042
- canonical_strategy: 'self',
1043
- priority: 0,
1044
- template_json: {},
1045
- },
628
+ cardMetadata: [{ key: 'site_id', labelKey: 'siteId' }, { key: 'catalog_category_id', labelKey: 'catalogCategoryId' }, { key: 'priority', labelKey: 'priority' }],
629
+ contextualKpi: { translationKey: 'visible', icon: Eye, count: (records) => records.length },
630
+ template: { site_id: null, catalog_category_id: null, page_type: 'comparison', rule_slug: '', status: 'active', generation_query_json: {}, min_product_count: 2, min_attribute_coverage: 1, canonical_strategy: 'self', priority: 0, template_json: {} },
1046
631
  formSections: [
1047
632
  {
1048
633
  title: ptEn('Escopo da regra', 'Rule scope'),
1049
634
  fields: [
1050
- {
1051
- key: 'site_id',
1052
- label: ptEn('Site', 'Site'),
1053
- type: 'relation',
1054
- required: true,
1055
- relation: relation('/catalog/sites', ['name', 'domain', 'slug'], {
1056
- resource: 'sites',
1057
- createResource: 'sites',
1058
- allowCreate: true,
1059
- }),
1060
- },
1061
- { key: 'category_id', label: ptEn('Categoria', 'Category'), type: 'relation', relation: relation('/category', ['name', 'slug'], { valueKey: 'category_id' }) },
635
+ { key: 'site_id', label: ptEn('Site', 'Site'), type: 'relation', required: true, relation: relation('/catalog/sites', ['name', 'domain', 'slug'], { resource: 'sites' }) },
636
+ { key: 'catalog_category_id', label: ptEn('Categoria', 'Category'), type: 'relation', relation: relation('/catalog/categories', ['name', 'slug'], { resource: 'categories' }) },
1062
637
  { key: 'page_type', label: ptEn('Tipo de página', 'Page type'), type: 'select', required: true, options: pageTypeOptions },
1063
- { key: 'rule_slug', label: ptEn('Slug da regra', 'Rule slug'), type: 'text', required: true, placeholder: ptEn('Ex.: comparativo-celulares-premium', 'Ex.: premium-smartphone-comparison') },
638
+ { key: 'rule_slug', label: ptEn('Slug da regra', 'Rule slug'), type: 'text', required: true },
1064
639
  { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
1065
640
  { key: 'canonical_strategy', label: ptEn('Estratégia canônica', 'Canonical strategy'), type: 'select', required: true, options: canonicalStrategyOptions },
1066
641
  { key: 'priority', label: ptEn('Prioridade', 'Priority'), type: 'number' },
@@ -1095,66 +670,259 @@ export const catalogResources: CatalogResourceDefinition[] = [
1095
670
  titleFields: ['name', 'slug'],
1096
671
  descriptionFields: ['slug', 'source_type'],
1097
672
  badgeFields: ['status', 'source_type'],
673
+ cardMetadata: [{ key: 'slug', labelKey: 'slug' }, { key: 'source_type', labelKey: 'sourceType' }, { key: 'status', labelKey: 'status' }],
674
+ contextualKpi: { translationKey: 'visible', icon: Eye, count: (records) => records.length },
675
+ template: { slug: '', name: '', source_type: 'api', config_json: {}, status: 'active' },
676
+ formSections: [{
677
+ title: ptEn('Origem dos dados', 'Data source'),
678
+ fields: [
679
+ { key: 'name', label: ptEn('Nome da fonte', 'Source name'), type: 'text', required: true },
680
+ { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true },
681
+ { key: 'source_type', label: ptEn('Tipo de fonte', 'Source type'), type: 'select', required: true, options: sourceTypeOptions },
682
+ { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
683
+ { key: 'config_json', label: ptEn('Configuração', 'Configuration'), type: 'json', span: 2 },
684
+ ],
685
+ }],
686
+ }
687
+ );
688
+
689
+ catalogResources.push(
690
+ {
691
+ resource: 'attribute-groups',
692
+ translationKey: 'attributeGroups',
693
+ singularLabel: ptEn('Grupo de atributos', 'Attribute group'),
694
+ createActionLabel: ptEn('Novo Grupo de Atributos', 'New Attribute Group'),
695
+ editActionLabel: ptEn('Editar Grupo de Atributos', 'Edit Attribute Group'),
696
+ icon: Layers3,
697
+ href: '/catalog/attribute-groups',
698
+ colorClass: 'from-violet-500/20 via-fuchsia-500/10 to-transparent',
699
+ glowClass: 'bg-violet-500/10 text-violet-700',
700
+ hasActiveStats: true,
701
+ listVariant: 'table',
702
+ primaryFilterField: 'status',
703
+ primaryFilterOptions: activeInactiveStatusFilterOptions,
704
+ titleFields: ['name', 'slug'],
705
+ descriptionFields: ['slug'],
706
+ badgeFields: ['status'],
707
+ cardMetadata: [{ key: 'slug', labelKey: 'slug' }, { key: 'status', labelKey: 'status' }],
708
+ tableColumns: [
709
+ { key: 'id', labelKey: 'id' },
710
+ { key: 'name', labelKey: 'name' },
711
+ { key: 'slug', labelKey: 'slug' },
712
+ { key: 'status', labelKey: 'status' },
713
+ ],
714
+ contextualKpi: { translationKey: 'visible', icon: Eye, count: (records) => records.length },
715
+ template: { slug: '', name: '', status: 'active' },
716
+ formSections: [{
717
+ title: ptEn('Grupo', 'Group'),
718
+ fields: [
719
+ { key: 'name', label: ptEn('Nome do grupo', 'Group name'), type: 'text', required: true },
720
+ { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true },
721
+ { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
722
+ ],
723
+ }],
724
+ },
725
+ {
726
+ resource: 'attributes',
727
+ translationKey: 'attributes',
728
+ singularLabel: ptEn('Atributo', 'Attribute'),
729
+ createActionLabel: ptEn('Novo Atributo', 'New Attribute'),
730
+ editActionLabel: ptEn('Editar Atributo', 'Edit Attribute'),
731
+ icon: Tags,
732
+ href: '/catalog/attributes',
733
+ colorClass: 'from-indigo-500/20 via-blue-500/10 to-transparent',
734
+ glowClass: 'bg-indigo-500/10 text-indigo-700',
735
+ listVariant: 'table',
736
+ primaryFilterField: 'data_type',
737
+ primaryFilterOptions: attributeTypeFilterOptions,
738
+ titleFields: ['name', 'code', 'slug'],
739
+ descriptionFields: ['description', 'slug'],
740
+ badgeFields: ['data_type', 'comparison_mode', 'status'],
1098
741
  cardMetadata: [
742
+ { key: 'group_id', labelKey: 'groupId' },
743
+ { key: 'group_name', labelKey: 'groupName' },
744
+ { key: 'is_filterable', labelKey: 'isFilterable', type: 'boolean' },
745
+ { key: 'is_comparable', labelKey: 'isComparable', type: 'boolean' },
746
+ ],
747
+ tableColumns: [
748
+ { key: 'id', labelKey: 'id' },
749
+ { key: 'name', labelKey: 'name' },
750
+ { key: 'code', labelKey: 'code' },
1099
751
  { key: 'slug', labelKey: 'slug' },
1100
- { key: 'source_type', labelKey: 'sourceType' },
752
+ { key: 'data_type', labelKey: 'dataType' },
1101
753
  { key: 'status', labelKey: 'status' },
1102
754
  ],
1103
- contextualKpi: {
1104
- translationKey: 'visible',
1105
- icon: Eye,
1106
- count: (records) => records.length,
1107
- },
1108
- template: {
1109
- slug: '',
1110
- name: '',
1111
- source_type: 'api',
1112
- config_json: {},
1113
- status: 'active',
1114
- },
755
+ contextualKpi: { translationKey: 'filterableInSlice', icon: ListFilter, count: (records) => records.filter((record) => record.is_filterable === true).length },
756
+ template: { code: '', group_id: null, group_name: '', slug: '', name: '', description: '', data_type: 'text', unit: '', comparison_mode: 'neutral', is_filterable: true, is_sortable: false, is_comparable: true, is_required_default: false, status: 'active', display_order: 0 },
1115
757
  formSections: [
1116
758
  {
1117
- title: ptEn('Origem dos dados', 'Data source'),
759
+ title: ptEn('Configuração do atributo', 'Attribute setup'),
1118
760
  fields: [
1119
- { key: 'name', label: ptEn('Nome da fonte', 'Source name'), type: 'text', required: true, placeholder: ptEn('Ex.: API principal do parceiro', 'Ex.: Primary partner API') },
1120
- { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true, placeholder: ptEn('Ex.: api-parceiro-principal', 'Ex.: primary-partner-api') },
1121
- { key: 'source_type', label: ptEn('Tipo de fonte', 'Source type'), type: 'select', required: true, options: sourceTypeOptions },
761
+ { key: 'name', label: ptEn('Nome', 'Name'), type: 'text', required: true },
762
+ { key: 'code', label: ptEn('Código', 'Code'), type: 'text', required: true },
763
+ { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true },
764
+ { key: 'group_id', label: ptEn('Grupo', 'Group'), type: 'relation', relation: relation('/catalog/attribute-groups', ['name', 'slug'], { resource: 'attribute-groups', createResource: 'attribute-groups', allowCreate: true }) },
765
+ { key: 'group_name', label: ptEn('Grupo textual', 'Group name'), type: 'text' },
766
+ { key: 'data_type', label: ptEn('Tipo de dado', 'Data type'), type: 'select', required: true, options: attributeTypeOptions },
767
+ { key: 'unit', label: ptEn('Unidade', 'Unit'), type: 'text' },
1122
768
  { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
1123
- { key: 'config_json', label: ptEn('Configuração', 'Configuration'), type: 'json', span: 2 },
769
+ { key: 'comparison_mode', label: ptEn('Modo de comparação', 'Comparison mode'), type: 'select', required: true, options: comparisonModeOptions },
770
+ { key: 'display_order', label: ptEn('Ordem de exibição', 'Display order'), type: 'number' },
771
+ { key: 'description', label: ptEn('Descrição', 'Description'), type: 'richtext', span: 2 },
772
+ ],
773
+ },
774
+ {
775
+ title: ptEn('Regras', 'Rules'),
776
+ fields: [
777
+ { key: 'is_filterable', label: ptEn('Filtrável', 'Filterable'), type: 'switch' },
778
+ { key: 'is_sortable', label: ptEn('Ordenável', 'Sortable'), type: 'switch' },
779
+ { key: 'is_comparable', label: ptEn('Comparável', 'Comparable'), type: 'switch' },
780
+ { key: 'is_required_default', label: ptEn('Obrigatório por padrão', 'Required by default'), type: 'switch' },
1124
781
  ],
1125
782
  },
1126
783
  ],
1127
784
  },
1128
- ];
1129
-
1130
- export const catalogResourceMap = new Map(
1131
- catalogResources.map((resource) => [resource.resource, resource])
785
+ {
786
+ resource: 'attribute-options',
787
+ translationKey: 'attributeOptions',
788
+ singularLabel: ptEn('Opção de atributo', 'Attribute option'),
789
+ createActionLabel: ptEn('Nova Opção', 'New Option'),
790
+ editActionLabel: ptEn('Editar Opção', 'Edit Option'),
791
+ icon: List,
792
+ href: '/catalog/attribute-options',
793
+ colorClass: 'from-slate-500/20 via-zinc-500/10 to-transparent',
794
+ glowClass: 'bg-slate-500/10 text-slate-700',
795
+ hasActiveStats: true,
796
+ listVariant: 'table',
797
+ primaryFilterField: 'status',
798
+ primaryFilterOptions: activeInactiveStatusFilterOptions,
799
+ titleFields: ['label', 'option_value', 'slug'],
800
+ descriptionFields: ['option_value', 'normalized_value'],
801
+ badgeFields: ['status', 'is_default'],
802
+ cardMetadata: [{ key: 'attribute_id', labelKey: 'attributeId' }, { key: 'sort_order', labelKey: 'sortOrder' }],
803
+ tableColumns: [
804
+ { key: 'id', labelKey: 'id' },
805
+ { key: 'label', labelKey: 'label' },
806
+ { key: 'slug', labelKey: 'slug' },
807
+ { key: 'option_value', labelKey: 'optionValue' },
808
+ { key: 'status', labelKey: 'status' },
809
+ { key: 'is_default', labelKey: 'isDefault', type: 'boolean' },
810
+ ],
811
+ contextualKpi: { translationKey: 'visible', icon: Eye, count: (records) => records.length },
812
+ template: { attribute_id: null, slug: '', label: '', option_value: '', normalized_value: '', sort_order: 0, status: 'active', is_default: false },
813
+ formSections: [{
814
+ title: ptEn('Opção', 'Option'),
815
+ fields: [
816
+ { key: 'attribute_id', label: ptEn('Atributo', 'Attribute'), type: 'relation', required: true, relation: relation('/catalog/attributes', ['name', 'code', 'slug'], { resource: 'attributes', createResource: 'attributes', allowCreate: true }) },
817
+ { key: 'label', label: ptEn('Rótulo', 'Label'), type: 'text', required: true },
818
+ { key: 'slug', label: ptEn('Slug', 'Slug'), type: 'text', required: true },
819
+ { key: 'option_value', label: ptEn('Valor', 'Value'), type: 'text', required: true },
820
+ { key: 'normalized_value', label: ptEn('Valor normalizado', 'Normalized value'), type: 'text' },
821
+ { key: 'sort_order', label: ptEn('Ordem', 'Sort order'), type: 'number' },
822
+ { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
823
+ { key: 'is_default', label: ptEn('Padrão', 'Default'), type: 'switch' },
824
+ ],
825
+ }],
826
+ },
827
+ {
828
+ resource: 'category-attributes',
829
+ translationKey: 'categoryAttributes',
830
+ singularLabel: ptEn('Atributo por categoria', 'Category attribute'),
831
+ createActionLabel: ptEn('Novo Atributo por Categoria', 'New Category Attribute'),
832
+ editActionLabel: ptEn('Editar Atributo por Categoria', 'Edit Category Attribute'),
833
+ icon: BadgePercent,
834
+ href: '/catalog/category-attributes',
835
+ colorClass: 'from-pink-500/20 via-rose-500/10 to-transparent',
836
+ glowClass: 'bg-pink-500/10 text-pink-700',
837
+ listVariant: 'table',
838
+ primaryFilterField: 'facet_mode',
839
+ primaryFilterOptions: facetModeFilterOptions,
840
+ titleFields: ['attribute_id', 'catalog_category_id'],
841
+ descriptionFields: ['facet_mode'],
842
+ badgeFields: ['facet_mode', 'is_required', 'is_highlight'],
843
+ cardMetadata: [{ key: 'catalog_category_id', labelKey: 'catalogCategoryId' }, { key: 'attribute_id', labelKey: 'attributeId' }, { key: 'weight', labelKey: 'weight' }],
844
+ tableColumns: [
845
+ { key: 'id', labelKey: 'id' },
846
+ { key: 'catalog_category_id', labelKey: 'catalogCategoryId' },
847
+ { key: 'attribute_id', labelKey: 'attributeId' },
848
+ { key: 'facet_mode', labelKey: 'facetMode' },
849
+ { key: 'is_required', labelKey: 'isRequired', type: 'boolean' },
850
+ { key: 'is_highlight', labelKey: 'isHighlight', type: 'boolean' },
851
+ ],
852
+ contextualKpi: { translationKey: 'requiredInSlice', icon: BadgePercent, count: (records) => records.filter((record) => record.is_required === true).length },
853
+ template: { catalog_category_id: null, attribute_id: null, is_required: false, is_highlight: true, is_filter_visible: true, is_comparison_visible: true, sort_order: 0, weight: 1, facet_mode: 'default' },
854
+ formSections: [{
855
+ title: ptEn('Relacionamentos', 'Relationships'),
856
+ fields: [
857
+ { key: 'catalog_category_id', label: ptEn('Categoria', 'Category'), type: 'relation', required: true, relation: relation('/catalog/categories', ['name', 'slug'], { resource: 'categories', createResource: 'categories', allowCreate: true }) },
858
+ { key: 'attribute_id', label: ptEn('Atributo', 'Attribute'), type: 'relation', required: true, relation: relation('/catalog/attributes', ['name', 'code', 'slug'], { resource: 'attributes', createResource: 'attributes', allowCreate: true }) },
859
+ { key: 'facet_mode', label: ptEn('Modo de faceta', 'Facet mode'), type: 'select', required: true, options: facetModeOptions },
860
+ { key: 'sort_order', label: ptEn('Ordem de exibição', 'Display order'), type: 'number' },
861
+ { key: 'weight', label: ptEn('Peso', 'Weight'), type: 'number' },
862
+ { key: 'is_required', label: ptEn('Obrigatório', 'Required'), type: 'switch' },
863
+ { key: 'is_highlight', label: ptEn('Destacado', 'Highlighted'), type: 'switch' },
864
+ { key: 'is_filter_visible', label: ptEn('Visível no filtro', 'Visible in filters'), type: 'switch' },
865
+ { key: 'is_comparison_visible', label: ptEn('Visível na comparação', 'Visible in comparison'), type: 'switch' },
866
+ ],
867
+ }],
868
+ },
869
+ {
870
+ resource: 'product-attributes',
871
+ translationKey: 'productAttributes',
872
+ singularLabel: ptEn('Atributo de produto', 'Product attribute'),
873
+ createActionLabel: ptEn('Novo Valor de Atributo', 'New Attribute Value'),
874
+ editActionLabel: ptEn('Editar Valor de Atributo', 'Edit Attribute Value'),
875
+ icon: ListFilter,
876
+ href: '/catalog/product-attributes',
877
+ colorClass: 'from-teal-500/20 via-emerald-500/10 to-transparent',
878
+ glowClass: 'bg-teal-500/10 text-teal-700',
879
+ listVariant: 'table',
880
+ primaryFilterField: 'is_verified',
881
+ primaryFilterOptions: [{ value: 'true', labelKey: 'verified' }, { value: 'false', labelKey: 'unverified' }],
882
+ titleFields: ['raw_value', 'value_text', 'attribute_id'],
883
+ descriptionFields: ['normalized_value', 'source_type'],
884
+ badgeFields: ['is_verified', 'source_type'],
885
+ cardMetadata: [{ key: 'product_id', labelKey: 'productId' }, { key: 'attribute_id', labelKey: 'attributeId' }, { key: 'attribute_option_id', labelKey: 'attributeOptionId' }],
886
+ tableColumns: [
887
+ { key: 'id', labelKey: 'id' },
888
+ { key: 'product_id', labelKey: 'productId' },
889
+ { key: 'attribute_id', labelKey: 'attributeId' },
890
+ { key: 'attribute_option_id', labelKey: 'attributeOptionId' },
891
+ { key: 'raw_value', labelKey: 'rawValue' },
892
+ { key: 'value_text', labelKey: 'valueText' },
893
+ { key: 'is_verified', labelKey: 'isVerified', type: 'boolean' },
894
+ ],
895
+ contextualKpi: { translationKey: 'verifiedInSlice', icon: BadgePercent, count: (records) => records.filter((record) => record.is_verified === true).length },
896
+ template: { product_id: null, attribute_id: null, attribute_option_id: null, value_text: '', value_number: null, value_boolean: null, raw_value: '', value_unit: '', normalized_value: '', normalized_text: '', normalized_number: null, source_type: 'manual', confidence_score: null, is_verified: false },
897
+ formSections: [{
898
+ title: ptEn('Valor', 'Value'),
899
+ fields: [
900
+ { key: 'product_id', label: ptEn('Produto', 'Product'), type: 'relation', required: true, relation: relation('/catalog/products', ['name', 'model_name', 'slug'], { resource: 'products' }) },
901
+ { key: 'attribute_id', label: ptEn('Atributo', 'Attribute'), type: 'relation', required: true, relation: relation('/catalog/attributes', ['name', 'code', 'slug'], { resource: 'attributes' }) },
902
+ { key: 'attribute_option_id', label: ptEn('Opção', 'Option'), type: 'relation', relation: relation('/catalog/attribute-options', ['label', 'option_value'], { resource: 'attribute-options' }) },
903
+ { key: 'raw_value', label: ptEn('Valor bruto', 'Raw value'), type: 'textarea', span: 2 },
904
+ { key: 'value_text', label: ptEn('Texto', 'Text'), type: 'text' },
905
+ { key: 'value_number', label: ptEn('Número', 'Number'), type: 'number' },
906
+ { key: 'value_unit', label: ptEn('Unidade', 'Unit'), type: 'text' },
907
+ { key: 'normalized_value', label: ptEn('Valor normalizado', 'Normalized value'), type: 'text' },
908
+ { key: 'source_type', label: ptEn('Origem', 'Source'), type: 'select', options: [{ value: 'manual', label: ptEn('Manual', 'Manual') }, { value: 'import', label: ptEn('Importação', 'Import') }, { value: 'computed', label: ptEn('Calculado', 'Computed') }] },
909
+ { key: 'confidence_score', label: ptEn('Confiança', 'Confidence'), type: 'number' },
910
+ { key: 'is_verified', label: ptEn('Verificado', 'Verified'), type: 'switch' },
911
+ ],
912
+ }],
913
+ }
1132
914
  );
1133
915
 
916
+ export const catalogResourceMap = new Map(catalogResources.map((resource) => [resource.resource, resource]));
1134
917
  export const catalogDashboardHref = '/catalog/dashboard';
1135
-
1136
- export const catalogQuickActionResources = catalogResources.filter(
1137
- (resource) => resource.featured
1138
- );
1139
-
1140
- export const catalogKpiResources = ['products', 'brands', 'offers', 'merchants'];
918
+ export const catalogQuickActionResources = catalogResources.filter((resource) => resource.featured);
919
+ export const catalogKpiResources = ['categories', 'products', 'brands', 'offers'];
1141
920
 
1142
921
  export function getCatalogRecordLabel(record: Record<string, unknown>) {
1143
- return (
1144
- record.name ||
1145
- record.title ||
1146
- record.label ||
1147
- record.slug ||
1148
- record.rule_slug ||
1149
- record.external_offer_id ||
1150
- `#${record.id}`
1151
- );
922
+ return record.name || record.title || record.label || record.code || record.slug || record.rule_slug || record.option_value || record.external_offer_id || `#${record.id}`;
1152
923
  }
1153
924
 
1154
- export function getCatalogLocalizedText(
1155
- value: CatalogLocalizedText,
1156
- localeCode?: string | null
1157
- ) {
925
+ export function getCatalogLocalizedText(value: CatalogLocalizedText, localeCode?: string | null) {
1158
926
  return localeCode?.toLowerCase().startsWith('pt') ? value.pt : value.en;
1159
927
  }
1160
928