@hed-hog/catalog 0.0.293 → 0.0.294

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 (45) hide show
  1. package/README.md +409 -379
  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 +1111 -5
  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 +133 -99
  21. package/hedhog/data/route.yaml +72 -8
  22. package/hedhog/frontend/app/[resource]/page.tsx.ejs +391 -33
  23. package/hedhog/frontend/app/_components/catalog-ai-form-assist-dialog.tsx.ejs +340 -0
  24. package/hedhog/frontend/app/_components/catalog-resource-form-sheet.tsx.ejs +907 -92
  25. package/hedhog/frontend/app/_lib/catalog-resources.tsx.ejs +929 -1161
  26. package/hedhog/frontend/messages/en.json +389 -299
  27. package/hedhog/frontend/messages/pt.json +389 -299
  28. package/hedhog/table/catalog_attribute.yaml +67 -52
  29. package/hedhog/table/catalog_attribute_option.yaml +40 -0
  30. package/hedhog/table/catalog_category.yaml +40 -0
  31. package/hedhog/table/catalog_category_attribute.yaml +37 -31
  32. package/hedhog/table/catalog_comparison.yaml +19 -22
  33. package/hedhog/table/catalog_product.yaml +30 -28
  34. package/hedhog/table/catalog_product_attribute_value.yaml +44 -31
  35. package/hedhog/table/catalog_product_category.yaml +13 -13
  36. package/hedhog/table/catalog_score_criterion.yaml +42 -25
  37. package/hedhog/table/catalog_seo_page_rule.yaml +10 -10
  38. package/hedhog/table/catalog_similarity_rule.yaml +33 -20
  39. package/hedhog/table/catalog_site.yaml +21 -13
  40. package/hedhog/table/catalog_site_category.yaml +12 -12
  41. package/package.json +6 -6
  42. package/src/catalog-resource.config.ts +132 -105
  43. package/src/catalog.controller.ts +91 -24
  44. package/src/catalog.module.ts +16 -12
  45. package/src/catalog.service.ts +1569 -56
@@ -1,1161 +1,929 @@
1
- import type { LucideIcon } from 'lucide-react';
2
- import {
3
- BadgePercent,
4
- Boxes,
5
- Building2,
6
- Eye,
7
- Factory,
8
- Globe2,
9
- Layers3,
10
- ListFilter,
11
- PackagePlus,
12
- PackageSearch,
13
- Scale,
14
- SearchCode,
15
- ShoppingCart,
16
- Store,
17
- Tags,
18
- } from 'lucide-react';
19
-
20
- type CatalogListVariant = 'cards' | 'table';
21
- type CatalogFieldDisplayType = 'text' | 'boolean' | 'currency';
22
- type CatalogFormFieldType =
23
- | 'text'
24
- | 'url'
25
- | 'textarea'
26
- | 'richtext'
27
- | 'number'
28
- | 'currency'
29
- | 'switch'
30
- | 'select'
31
- | 'relation'
32
- | 'upload'
33
- | 'json'
34
- | 'date'
35
- | 'datetime';
36
-
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
-
53
- export type CatalogContextualKpiDefinition = {
54
- translationKey: string;
55
- icon: LucideIcon;
56
- count: (records: Record<string, unknown>[]) => number;
57
- };
58
-
59
- export type CatalogFormOptionDefinition = {
60
- value: string;
61
- label: CatalogLocalizedText;
62
- };
63
-
64
- export type CatalogRelationDefinition = {
65
- endpoint: string;
66
- resource?: string;
67
- createResource?: string;
68
- allowCreate?: boolean;
69
- valueKey?: string;
70
- labelKeys: string[];
71
- searchParam?: string;
72
- };
73
-
74
- export type CatalogFormFieldDefinition = {
75
- key: string;
76
- label: CatalogLocalizedText;
77
- type: CatalogFormFieldType;
78
- required?: boolean;
79
- span?: 1 | 2;
80
- placeholder?: CatalogLocalizedText;
81
- options?: CatalogFormOptionDefinition[];
82
- relation?: CatalogRelationDefinition;
83
- uploadDestination?: string;
84
- accept?: string;
85
- uploadPreviewVariant?: 'default' | 'square';
86
- };
87
-
88
- export type CatalogFormSectionDefinition = {
89
- title: CatalogLocalizedText;
90
- description?: CatalogLocalizedText;
91
- fields: CatalogFormFieldDefinition[];
92
- };
93
-
94
- export type CatalogResourceDefinition = {
95
- resource: string;
96
- translationKey: string;
97
- singularLabel: CatalogLocalizedText;
98
- createActionLabel: CatalogLocalizedText;
99
- editActionLabel: CatalogLocalizedText;
100
- icon: LucideIcon;
101
- href: string;
102
- colorClass: string;
103
- glowClass: string;
104
- featured?: boolean;
105
- hasActiveStats?: boolean;
106
- template: Record<string, unknown>;
107
- listVariant: CatalogListVariant;
108
- primaryFilterField: string;
109
- primaryFilterOptions: CatalogFilterOptionDefinition[];
110
- titleFields: string[];
111
- descriptionFields: string[];
112
- badgeFields: string[];
113
- cardMetadata: CatalogFieldDefinition[];
114
- tableColumns?: CatalogFieldDefinition[];
115
- contextualKpi: CatalogContextualKpiDefinition;
116
- formSections: CatalogFormSectionDefinition[];
117
- };
118
-
119
- const ptEn = (pt: string, en: string): CatalogLocalizedText => ({ pt, en });
120
-
121
- const activeInactiveStatusFilterOptions: CatalogFilterOptionDefinition[] = [
122
- { value: 'active', labelKey: 'active' },
123
- { value: 'inactive', labelKey: 'inactive' },
124
- ];
125
-
126
- const publicationStatusFilterOptions: CatalogFilterOptionDefinition[] = [
127
- { value: 'draft', labelKey: 'draft' },
128
- { value: 'published', labelKey: 'published' },
129
- { value: 'archived', labelKey: 'archived' },
130
- ];
131
-
132
- const availabilityFilterOptions: CatalogFilterOptionDefinition[] = [
133
- { value: 'in_stock', labelKey: 'inStock' },
134
- { value: 'out_of_stock', labelKey: 'outOfStock' },
135
- { value: 'preorder', labelKey: 'preorder' },
136
- ];
137
-
138
- const dataTypeFilterOptions: CatalogFilterOptionDefinition[] = [
139
- { value: 'text', labelKey: 'text' },
140
- { value: 'number', labelKey: 'number' },
141
- { value: 'boolean', labelKey: 'boolean' },
142
- { value: 'json', labelKey: 'json' },
143
- ];
144
-
145
- const facetModeFilterOptions: CatalogFilterOptionDefinition[] = [
146
- { value: 'default', labelKey: 'default' },
147
- { value: 'range', labelKey: 'range' },
148
- { value: 'select', labelKey: 'select' },
149
- ];
150
-
151
- const activeInactiveStatusOptions = [
152
- { value: 'active', label: ptEn('Ativo', 'Active') },
153
- { value: 'inactive', label: ptEn('Inativo', 'Inactive') },
154
- ];
155
-
156
- const publicationStatusOptions = [
157
- { value: 'draft', label: ptEn('Rascunho', 'Draft') },
158
- { value: 'published', label: ptEn('Publicado', 'Published') },
159
- { value: 'archived', label: ptEn('Arquivado', 'Archived') },
160
- ];
161
-
162
- const comparisonStatusOptions = [
163
- { value: 'draft', label: ptEn('Rascunho', 'Draft') },
164
- { value: 'ready', label: ptEn('Pronto', 'Ready') },
165
- { value: 'disabled', label: ptEn('Desabilitado', 'Disabled') },
166
- ];
167
-
168
- const siteTypeOptions = [
169
- { value: 'portal', label: ptEn('Portal', 'Portal') },
170
- { value: 'storefront', label: ptEn('Loja', 'Storefront') },
171
- { value: 'landing_page', label: ptEn('Landing page', 'Landing page') },
172
- ];
173
-
174
- const dataTypeOptions = [
175
- { value: 'text', label: ptEn('Texto', 'Text') },
176
- { value: 'number', label: ptEn('Número', 'Number') },
177
- { value: 'boolean', label: ptEn('Booleano', 'Boolean') },
178
- { value: 'json', label: ptEn('JSON', 'JSON') },
179
- ];
180
-
181
- const comparisonModeOptions = [
182
- { value: 'neutral', label: ptEn('Neutro', 'Neutral') },
183
- { value: 'higher_better', label: ptEn('Maior é melhor', 'Higher is better') },
184
- { value: 'lower_better', label: ptEn('Menor é melhor', 'Lower is better') },
185
- { value: 'exact_match', label: ptEn('Comparação exata', 'Exact match') },
186
- ];
187
-
188
- const facetModeOptions = [
189
- { value: 'default', label: ptEn('Padrão', 'Default') },
190
- { value: 'range', label: ptEn('Faixa', 'Range') },
191
- { value: 'select', label: ptEn('Seleção', 'Select') },
192
- ];
193
-
194
- const comparisonTypeOptions = [
195
- { value: 'manual', label: ptEn('Manual', 'Manual') },
196
- { value: 'automated', label: ptEn('Automática', 'Automated') },
197
- ];
198
-
199
- const generationModeOptions = [
200
- { value: 'manual', label: ptEn('Manual', 'Manual') },
201
- { value: 'automatic', label: ptEn('Automática', 'Automatic') },
202
- { value: 'ai_assisted', label: ptEn('Assistida por IA', 'AI assisted') },
203
- ];
204
-
205
- const availabilityOptions = [
206
- { value: 'in_stock', label: ptEn('Em estoque', 'In stock') },
207
- { value: 'out_of_stock', label: ptEn('Sem estoque', 'Out of stock') },
208
- { value: 'preorder', label: ptEn('Pré-venda', 'Pre-order') },
209
- ];
210
-
211
- const merchantTypeOptions = [
212
- { value: 'retailer', label: ptEn('Varejista', 'Retailer') },
213
- { value: 'marketplace', label: ptEn('Marketplace', 'Marketplace') },
214
- { value: 'brand_store', label: ptEn('Loja da marca', 'Brand store') },
215
- ];
216
-
217
- const networkTypeOptions = [
218
- { value: 'direct', label: ptEn('Direto', 'Direct') },
219
- { value: 'network', label: ptEn('Rede', 'Network') },
220
- { value: 'api', label: ptEn('API', 'API') },
221
- ];
222
-
223
- const commissionTypeOptions = [
224
- { 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') },
228
- ];
229
-
230
- const pageTypeOptions = [
231
- { value: 'comparison', label: ptEn('Comparação', 'Comparison') },
232
- { value: 'category', label: ptEn('Categoria', 'Category') },
233
- { value: 'ranking', label: ptEn('Ranking', 'Ranking') },
234
- ];
235
-
236
- const canonicalStrategyOptions = [
237
- { value: 'self', label: ptEn('Self canonical', 'Self canonical') },
238
- { value: 'parent', label: ptEn('Canônica da categoria', 'Parent canonical') },
239
- { value: 'custom', label: ptEn('Canônica customizada', 'Custom canonical') },
240
- ];
241
-
242
- const sourceTypeOptions = [
243
- { value: 'api', label: ptEn('API', 'API') },
244
- { value: 'feed', label: ptEn('Feed', 'Feed') },
245
- { value: 'csv', label: ptEn('CSV', 'CSV') },
246
- { value: 'scraper', label: ptEn('Scraper', 'Scraper') },
247
- ];
248
-
249
- const currencyOptions = [
250
- { value: 'BRL', label: ptEn('Real brasileiro (BRL)', 'Brazilian real (BRL)') },
251
- { value: 'USD', label: ptEn('Dólar americano (USD)', 'US dollar (USD)') },
252
- { value: 'EUR', label: ptEn('Euro (EUR)', 'Euro (EUR)') },
253
- ];
254
-
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
- });
279
-
280
- export const catalogResources: CatalogResourceDefinition[] = [
281
- {
282
- resource: 'brands',
283
- translationKey: 'brands',
284
- singularLabel: ptEn('Marca', 'Brand'),
285
- createActionLabel: ptEn('Nova Marca', 'New Brand'),
286
- editActionLabel: ptEn('Editar Marca', 'Edit Brand'),
287
- icon: Factory,
288
- href: '/catalog/brands',
289
- colorClass: 'from-orange-500/20 via-amber-500/10 to-transparent',
290
- glowClass: 'bg-orange-500/10 text-orange-700',
291
- featured: true,
292
- hasActiveStats: true,
293
- listVariant: 'cards',
294
- primaryFilterField: 'status',
295
- primaryFilterOptions: activeInactiveStatusFilterOptions,
296
- titleFields: ['name', 'slug'],
297
- descriptionFields: ['normalized_name', 'website_url'],
298
- badgeFields: ['status'],
299
- cardMetadata: [
300
- { key: 'slug', labelKey: 'slug' },
301
- { key: 'normalized_name', labelKey: 'normalizedName' },
302
- { key: 'website_url', labelKey: 'websiteUrl' },
303
- ],
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
- ],
603
- },
604
- {
605
- resource: 'sites',
606
- translationKey: 'sites',
607
- singularLabel: ptEn('Site', 'Site'),
608
- createActionLabel: ptEn('Novo Site', 'New Site'),
609
- editActionLabel: ptEn('Editar Site', 'Edit Site'),
610
- icon: Globe2,
611
- href: '/catalog/sites',
612
- colorClass: 'from-sky-500/20 via-cyan-500/10 to-transparent',
613
- glowClass: 'bg-sky-500/10 text-sky-700',
614
- hasActiveStats: true,
615
- listVariant: 'cards',
616
- primaryFilterField: 'status',
617
- primaryFilterOptions: activeInactiveStatusFilterOptions,
618
- titleFields: ['name', 'domain', 'slug'],
619
- descriptionFields: ['domain', 'slug'],
620
- badgeFields: ['status', 'site_type'],
621
- cardMetadata: [
622
- { key: 'slug', labelKey: 'slug' },
623
- { key: 'domain', labelKey: 'domain' },
624
- { key: 'default_locale_id', labelKey: 'defaultLocaleId' },
625
- ],
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
- },
641
- formSections: [
642
- {
643
- title: ptEn('Dados principais', 'Main details'),
644
- 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') },
648
- { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
649
- { 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
- },
656
- ],
657
- },
658
- {
659
- title: ptEn('Configurações', 'Settings'),
660
- fields: [
661
- { key: 'theme_settings_json', label: ptEn('Tema e identidade', 'Theme and identity'), type: 'json', span: 2 },
662
- { key: 'seo_defaults_json', label: ptEn('Padrões de SEO', 'SEO defaults'), type: 'json', span: 2 },
663
- ],
664
- },
665
- ],
666
- },
667
- {
668
- resource: 'products',
669
- translationKey: 'products',
670
- singularLabel: ptEn('Produto', 'Product'),
671
- createActionLabel: ptEn('Novo Produto', 'New Product'),
672
- editActionLabel: ptEn('Editar Produto', 'Edit Product'),
673
- icon: PackageSearch,
674
- href: '/catalog/products',
675
- colorClass: 'from-emerald-500/20 via-green-500/10 to-transparent',
676
- glowClass: 'bg-emerald-500/10 text-emerald-700',
677
- featured: true,
678
- hasActiveStats: true,
679
- listVariant: 'cards',
680
- primaryFilterField: 'status',
681
- primaryFilterOptions: publicationStatusFilterOptions,
682
- titleFields: ['name', 'model_name', 'slug'],
683
- descriptionFields: ['model_name', 'sku', 'slug'],
684
- badgeFields: ['status', 'comparison_status', 'is_active'],
685
- cardMetadata: [
686
- { key: 'slug', labelKey: 'slug' },
687
- { key: 'sku', labelKey: 'sku' },
688
- { key: 'brand_id', labelKey: 'brandId' },
689
- { key: 'category_id', labelKey: 'categoryId' },
690
- ],
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
- },
714
- formSections: [
715
- {
716
- title: ptEn('Base do produto', 'Product basics'),
717
- 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') },
751
- { key: 'release_date', label: ptEn('Data de lançamento', 'Release date'), type: 'date' },
752
- { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: publicationStatusOptions },
753
- { key: 'comparison_status', label: ptEn('Status de comparação', 'Comparison status'), type: 'select', required: true, options: comparisonStatusOptions },
754
- { key: 'is_active', label: ptEn('Ativo para comparação', 'Active for comparison'), type: 'switch' },
755
- ],
756
- },
757
- {
758
- title: ptEn('Conteúdo', 'Content'),
759
- 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') },
762
- { key: 'spec_snapshot_json', label: ptEn('Snapshot de especificações', 'Specification snapshot'), type: 'json', span: 2 },
763
- { key: 'comparison_snapshot_json', label: ptEn('Snapshot de comparação', 'Comparison snapshot'), type: 'json', span: 2 },
764
- ],
765
- },
766
- ],
767
- },
768
- {
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',
778
- hasActiveStats: true,
779
- listVariant: 'table',
780
- 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' },
801
- formSections: [
802
- {
803
- title: ptEn('Grupo', 'Group'),
804
- 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 },
808
- ],
809
- },
810
- ],
811
- },
812
- {
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
- },
859
- formSections: [
860
- {
861
- title: ptEn('Configuração do atributo', 'Attribute setup'),
862
- 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') },
881
- ],
882
- },
883
- {
884
- title: ptEn('Regras', 'Rules'),
885
- 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 },
890
- ],
891
- },
892
- ],
893
- },
894
- {
895
- resource: 'merchants',
896
- translationKey: 'merchants',
897
- singularLabel: ptEn('Lojista', 'Merchant'),
898
- createActionLabel: ptEn('Novo Lojista', 'New Merchant'),
899
- editActionLabel: ptEn('Editar Lojista', 'Edit Merchant'),
900
- icon: Store,
901
- href: '/catalog/merchants',
902
- colorClass: 'from-lime-500/20 via-green-500/10 to-transparent',
903
- glowClass: 'bg-lime-500/10 text-lime-700',
904
- featured: true,
905
- hasActiveStats: true,
906
- listVariant: 'cards',
907
- primaryFilterField: 'status',
908
- primaryFilterOptions: activeInactiveStatusFilterOptions,
909
- titleFields: ['name', 'slug'],
910
- descriptionFields: ['slug'],
911
- 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
- ],
941
- },
942
- {
943
- resource: 'affiliate-programs',
944
- translationKey: 'affiliatePrograms',
945
- singularLabel: ptEn('Programa de afiliação', 'Affiliate program'),
946
- createActionLabel: ptEn('Novo Programa de Afiliação', 'New Affiliate Program'),
947
- editActionLabel: ptEn('Editar Programa de Afiliação', 'Edit Affiliate Program'),
948
- icon: Building2,
949
- href: '/catalog/affiliate-programs',
950
- colorClass: 'from-cyan-500/20 via-teal-500/10 to-transparent',
951
- glowClass: 'bg-cyan-500/10 text-cyan-700',
952
- hasActiveStats: true,
953
- listVariant: 'cards',
954
- primaryFilterField: 'status',
955
- primaryFilterOptions: activeInactiveStatusFilterOptions,
956
- titleFields: ['name', 'slug'],
957
- descriptionFields: ['slug', 'network_type'],
958
- 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
- ],
1004
- },
1005
- {
1006
- resource: 'seo-rules',
1007
- translationKey: 'seoRules',
1008
- singularLabel: ptEn('Regra de SEO', 'SEO rule'),
1009
- createActionLabel: ptEn('Nova Regra de SEO', 'New SEO Rule'),
1010
- editActionLabel: ptEn('Editar Regra de SEO', 'Edit SEO Rule'),
1011
- icon: SearchCode,
1012
- href: '/catalog/seo-rules',
1013
- colorClass: 'from-slate-500/20 via-zinc-500/10 to-transparent',
1014
- glowClass: 'bg-slate-500/10 text-slate-700',
1015
- featured: true,
1016
- hasActiveStats: true,
1017
- listVariant: 'cards',
1018
- primaryFilterField: 'status',
1019
- primaryFilterOptions: activeInactiveStatusFilterOptions,
1020
- titleFields: ['rule_slug', 'page_type'],
1021
- descriptionFields: ['page_type', 'canonical_strategy'],
1022
- 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
- },
1046
- formSections: [
1047
- {
1048
- title: ptEn('Escopo da regra', 'Rule scope'),
1049
- 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' }) },
1062
- { 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') },
1064
- { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
1065
- { key: 'canonical_strategy', label: ptEn('Estratégia canônica', 'Canonical strategy'), type: 'select', required: true, options: canonicalStrategyOptions },
1066
- { key: 'priority', label: ptEn('Prioridade', 'Priority'), type: 'number' },
1067
- { key: 'min_product_count', label: ptEn('Mínimo de produtos', 'Minimum products'), type: 'number' },
1068
- { key: 'min_attribute_coverage', label: ptEn('Cobertura mínima de atributos', 'Minimum attribute coverage'), type: 'number' },
1069
- ],
1070
- },
1071
- {
1072
- title: ptEn('Regras dinâmicas', 'Dynamic rules'),
1073
- fields: [
1074
- { key: 'generation_query_json', label: ptEn('Consulta geradora', 'Generation query'), type: 'json', span: 2 },
1075
- { key: 'template_json', label: ptEn('Template de SEO', 'SEO template'), type: 'json', span: 2 },
1076
- ],
1077
- },
1078
- ],
1079
- },
1080
- {
1081
- resource: 'import-sources',
1082
- translationKey: 'importSources',
1083
- singularLabel: ptEn('Fonte de importação', 'Import source'),
1084
- createActionLabel: ptEn('Nova Fonte de Importação', 'New Import Source'),
1085
- editActionLabel: ptEn('Editar Fonte de Importação', 'Edit Import Source'),
1086
- icon: PackagePlus,
1087
- href: '/catalog/import-sources',
1088
- colorClass: 'from-stone-500/20 via-neutral-500/10 to-transparent',
1089
- glowClass: 'bg-stone-500/10 text-stone-700',
1090
- featured: true,
1091
- hasActiveStats: true,
1092
- listVariant: 'cards',
1093
- primaryFilterField: 'status',
1094
- primaryFilterOptions: activeInactiveStatusFilterOptions,
1095
- titleFields: ['name', 'slug'],
1096
- descriptionFields: ['slug', 'source_type'],
1097
- badgeFields: ['status', 'source_type'],
1098
- cardMetadata: [
1099
- { key: 'slug', labelKey: 'slug' },
1100
- { key: 'source_type', labelKey: 'sourceType' },
1101
- { key: 'status', labelKey: 'status' },
1102
- ],
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
- },
1115
- formSections: [
1116
- {
1117
- title: ptEn('Origem dos dados', 'Data source'),
1118
- 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 },
1122
- { 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 },
1124
- ],
1125
- },
1126
- ],
1127
- },
1128
- ];
1129
-
1130
- export const catalogResourceMap = new Map(
1131
- catalogResources.map((resource) => [resource.resource, resource])
1132
- );
1133
-
1134
- 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'];
1141
-
1142
- 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
- );
1152
- }
1153
-
1154
- export function getCatalogLocalizedText(
1155
- value: CatalogLocalizedText,
1156
- localeCode?: string | null
1157
- ) {
1158
- return localeCode?.toLowerCase().startsWith('pt') ? value.pt : value.en;
1159
- }
1160
-
1161
- export const catalogModuleIcon = Boxes;
1
+ import type { LucideIcon } from 'lucide-react';
2
+ import {
3
+ BadgePercent,
4
+ Boxes,
5
+ Building2,
6
+ Eye,
7
+ Factory,
8
+ FolderTree,
9
+ Globe2,
10
+ Layers3,
11
+ List,
12
+ ListFilter,
13
+ PackagePlus,
14
+ PackageSearch,
15
+ Scale,
16
+ SearchCode,
17
+ ShoppingCart,
18
+ Store,
19
+ Tags,
20
+ } from 'lucide-react';
21
+
22
+ type CatalogListVariant = 'cards' | 'table';
23
+ type CatalogFieldDisplayType = 'text' | 'boolean' | 'currency';
24
+ type CatalogFormFieldType =
25
+ | 'text'
26
+ | 'url'
27
+ | 'textarea'
28
+ | 'richtext'
29
+ | 'number'
30
+ | 'currency'
31
+ | 'switch'
32
+ | 'select'
33
+ | 'relation'
34
+ | 'upload'
35
+ | 'json'
36
+ | 'date'
37
+ | 'datetime';
38
+
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 };
42
+ export type CatalogContextualKpiDefinition = {
43
+ translationKey: string;
44
+ icon: LucideIcon;
45
+ count: (records: Record<string, unknown>[]) => number;
46
+ };
47
+ export type CatalogFormOptionDefinition = { value: string; label: CatalogLocalizedText };
48
+ export type CatalogRelationDefinition = {
49
+ endpoint: string;
50
+ resource?: string;
51
+ createResource?: string;
52
+ allowCreate?: boolean;
53
+ valueKey?: string;
54
+ labelKeys: string[];
55
+ searchParam?: string;
56
+ };
57
+ export type CatalogFormFieldDefinition = {
58
+ key: string;
59
+ label: CatalogLocalizedText;
60
+ type: CatalogFormFieldType;
61
+ required?: boolean;
62
+ span?: 1 | 2;
63
+ placeholder?: CatalogLocalizedText;
64
+ options?: CatalogFormOptionDefinition[];
65
+ relation?: CatalogRelationDefinition;
66
+ uploadDestination?: string;
67
+ accept?: string;
68
+ uploadPreviewVariant?: 'default' | 'square';
69
+ };
70
+ export type CatalogFormSectionDefinition = {
71
+ title: CatalogLocalizedText;
72
+ description?: CatalogLocalizedText;
73
+ fields: CatalogFormFieldDefinition[];
74
+ };
75
+ export type CatalogResourceDefinition = {
76
+ resource: string;
77
+ translationKey: string;
78
+ singularLabel: CatalogLocalizedText;
79
+ createActionLabel: CatalogLocalizedText;
80
+ editActionLabel: CatalogLocalizedText;
81
+ icon: LucideIcon;
82
+ href: string;
83
+ colorClass: string;
84
+ glowClass: string;
85
+ featured?: boolean;
86
+ hasActiveStats?: boolean;
87
+ template: Record<string, unknown>;
88
+ listVariant: CatalogListVariant;
89
+ primaryFilterField: string;
90
+ primaryFilterOptions: CatalogFilterOptionDefinition[];
91
+ titleFields: string[];
92
+ descriptionFields: string[];
93
+ badgeFields: string[];
94
+ cardMetadata: CatalogFieldDefinition[];
95
+ tableColumns?: CatalogFieldDefinition[];
96
+ contextualKpi: CatalogContextualKpiDefinition;
97
+ formSections: CatalogFormSectionDefinition[];
98
+ };
99
+
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
+ });
116
+
117
+ const activeInactiveStatusFilterOptions = [
118
+ { value: 'active', labelKey: 'active' },
119
+ { value: 'inactive', labelKey: 'inactive' },
120
+ ];
121
+ const publicationStatusFilterOptions = [
122
+ { value: 'draft', labelKey: 'draft' },
123
+ { value: 'published', labelKey: 'published' },
124
+ { value: 'archived', labelKey: 'archived' },
125
+ ];
126
+ const availabilityFilterOptions = [
127
+ { value: 'in_stock', labelKey: 'inStock' },
128
+ { value: 'out_of_stock', labelKey: 'outOfStock' },
129
+ { value: 'pre_order', labelKey: 'preOrder' },
130
+ { value: 'unknown', labelKey: 'unknown' },
131
+ ];
132
+ const attributeTypeFilterOptions = [
133
+ { value: 'text', labelKey: 'text' },
134
+ { value: 'long_text', labelKey: 'longText' },
135
+ { value: 'number', labelKey: 'number' },
136
+ { value: 'boolean', labelKey: 'boolean' },
137
+ { value: 'option', labelKey: 'option' },
138
+ ];
139
+ const facetModeFilterOptions = [
140
+ { value: 'default', labelKey: 'default' },
141
+ { value: 'range', labelKey: 'range' },
142
+ { value: 'select', labelKey: 'select' },
143
+ { value: 'hidden', labelKey: 'hidden' },
144
+ ];
145
+
146
+ const activeInactiveStatusOptions = [
147
+ { value: 'active', label: ptEn('Ativo', 'Active') },
148
+ { value: 'inactive', label: ptEn('Inativo', 'Inactive') },
149
+ ];
150
+ const publicationStatusOptions = [
151
+ { value: 'draft', label: ptEn('Rascunho', 'Draft') },
152
+ { value: 'published', label: ptEn('Publicado', 'Published') },
153
+ { value: 'archived', label: ptEn('Arquivado', 'Archived') },
154
+ ];
155
+ const comparisonStatusOptions = [
156
+ { value: 'draft', label: ptEn('Rascunho', 'Draft') },
157
+ { value: 'ready', label: ptEn('Pronto', 'Ready') },
158
+ { value: 'disabled', label: ptEn('Desabilitado', 'Disabled') },
159
+ ];
160
+ const siteTypeOptions = [
161
+ { value: 'portal', label: ptEn('Portal', 'Portal') },
162
+ { value: 'niche', label: ptEn('Nicho', 'Niche') },
163
+ { value: 'tenant', label: ptEn('Tenant', 'Tenant') },
164
+ ];
165
+ const attributeTypeOptions = [
166
+ { value: 'text', label: ptEn('Texto', 'Text') },
167
+ { value: 'long_text', label: ptEn('Texto longo', 'Long text') },
168
+ { value: 'number', label: ptEn('Número', 'Number') },
169
+ { value: 'boolean', label: ptEn('Booleano', 'Boolean') },
170
+ { value: 'option', label: ptEn('Opção', 'Option') },
171
+ ];
172
+ const comparisonModeOptions = [
173
+ { value: 'neutral', label: ptEn('Neutro', 'Neutral') },
174
+ { value: 'higher_better', label: ptEn('Maior é melhor', 'Higher is better') },
175
+ { value: 'lower_better', label: ptEn('Menor é melhor', 'Lower is better') },
176
+ { value: 'boolean_true_better', label: ptEn('Verdadeiro é melhor', 'True is better') },
177
+ { value: 'exact_match', label: ptEn('Correspondência exata', 'Exact match') },
178
+ ];
179
+ const facetModeOptions = [
180
+ { value: 'default', label: ptEn('Padrão', 'Default') },
181
+ { value: 'range', label: ptEn('Faixa', 'Range') },
182
+ { value: 'select', label: ptEn('Seleção', 'Select') },
183
+ { value: 'hidden', label: ptEn('Oculto', 'Hidden') },
184
+ ];
185
+ const comparisonTypeOptions = [
186
+ { value: 'manual', label: ptEn('Manual', 'Manual') },
187
+ { value: 'automatic', label: ptEn('Automática', 'Automatic') },
188
+ { value: 'similarity', label: ptEn('Similaridade', 'Similarity') },
189
+ { value: 'compatibility', label: ptEn('Compatibilidade', 'Compatibility') },
190
+ ];
191
+ const generationModeOptions = [
192
+ { value: 'manual', label: ptEn('Manual', 'Manual') },
193
+ { value: 'automatic', label: ptEn('Automática', 'Automatic') },
194
+ ];
195
+ const availabilityOptions = [
196
+ { value: 'in_stock', label: ptEn('Em estoque', 'In stock') },
197
+ { value: 'out_of_stock', label: ptEn('Sem estoque', 'Out of stock') },
198
+ { value: 'pre_order', label: ptEn('Pré-venda', 'Pre-order') },
199
+ { value: 'unknown', label: ptEn('Desconhecido', 'Unknown') },
200
+ ];
201
+ const merchantTypeOptions = [
202
+ { value: 'retailer', label: ptEn('Varejista', 'Retailer') },
203
+ { value: 'marketplace', label: ptEn('Marketplace', 'Marketplace') },
204
+ { value: 'saas', label: ptEn('SaaS', 'SaaS') },
205
+ { value: 'direct', label: ptEn('Direto', 'Direct') },
206
+ ];
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!') },
212
+ { value: 'direct', label: ptEn('Direto', 'Direct') },
213
+ { value: 'network', label: ptEn('Rede', 'Network') },
214
+ ];
215
+ const commissionTypeOptions = [
216
+ { value: 'percentage', label: ptEn('Percentual', 'Percentage') },
217
+ { value: 'fixed', label: ptEn('Valor fixo', 'Fixed') },
218
+ ];
219
+ const pageTypeOptions = [
220
+ { value: 'comparison', label: ptEn('Comparação', 'Comparison') },
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') },
227
+ ];
228
+ const canonicalStrategyOptions = [
229
+ { value: 'self', label: ptEn('Canônica própria', 'Self canonical') },
230
+ { value: 'parent', label: ptEn('Canônica pai', 'Parent canonical') },
231
+ { value: 'custom', label: ptEn('Canônica customizada', 'Custom canonical') },
232
+ ];
233
+ const sourceTypeOptions = [
234
+ { value: 'api', label: ptEn('API', 'API') },
235
+ { value: 'feed', label: ptEn('Feed', 'Feed') },
236
+ { value: 'file', label: ptEn('Arquivo', 'File') },
237
+ { value: 'crawler', label: ptEn('Crawler', 'Crawler') },
238
+ ];
239
+ const currencyOptions = [
240
+ { value: 'BRL', label: ptEn('Real brasileiro (BRL)', 'Brazilian real (BRL)') },
241
+ { value: 'USD', label: ptEn('Dólar americano (USD)', 'US dollar (USD)') },
242
+ { value: 'EUR', label: ptEn('Euro (EUR)', 'Euro (EUR)') },
243
+ ];
244
+
245
+ export const catalogResources: CatalogResourceDefinition[] = [];
246
+
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
+ },
292
+ {
293
+ resource: 'brands',
294
+ translationKey: 'brands',
295
+ singularLabel: ptEn('Marca', 'Brand'),
296
+ createActionLabel: ptEn('Nova Marca', 'New Brand'),
297
+ editActionLabel: ptEn('Editar Marca', 'Edit Brand'),
298
+ icon: Factory,
299
+ href: '/catalog/brands',
300
+ colorClass: 'from-orange-500/20 via-amber-500/10 to-transparent',
301
+ glowClass: 'bg-orange-500/10 text-orange-700',
302
+ featured: true,
303
+ hasActiveStats: true,
304
+ listVariant: 'cards',
305
+ primaryFilterField: 'status',
306
+ primaryFilterOptions: activeInactiveStatusFilterOptions,
307
+ titleFields: ['name', 'slug'],
308
+ descriptionFields: ['normalized_name', 'website_url'],
309
+ badgeFields: ['status'],
310
+ cardMetadata: [
311
+ { key: 'slug', labelKey: 'slug' },
312
+ { key: 'normalized_name', labelKey: 'normalizedName' },
313
+ { key: 'website_url', labelKey: 'websiteUrl' },
314
+ ],
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
+ }],
328
+ },
329
+ {
330
+ resource: 'sites',
331
+ translationKey: 'sites',
332
+ singularLabel: ptEn('Site', 'Site'),
333
+ createActionLabel: ptEn('Novo Site', 'New Site'),
334
+ editActionLabel: ptEn('Editar Site', 'Edit Site'),
335
+ icon: Globe2,
336
+ href: '/catalog/sites',
337
+ colorClass: 'from-sky-500/20 via-cyan-500/10 to-transparent',
338
+ glowClass: 'bg-sky-500/10 text-sky-700',
339
+ featured: true,
340
+ hasActiveStats: true,
341
+ listVariant: 'cards',
342
+ primaryFilterField: 'status',
343
+ primaryFilterOptions: activeInactiveStatusFilterOptions,
344
+ titleFields: ['name', 'domain', 'slug'],
345
+ descriptionFields: ['domain', 'slug'],
346
+ badgeFields: ['status', 'site_type'],
347
+ cardMetadata: [
348
+ { key: 'slug', labelKey: 'slug' },
349
+ { key: 'domain', labelKey: 'domain' },
350
+ { key: 'default_locale_id', labelKey: 'defaultLocaleId' },
351
+ ],
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: {} },
354
+ formSections: [
355
+ {
356
+ title: ptEn('Dados principais', 'Main details'),
357
+ fields: [
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 },
361
+ { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
362
+ { key: 'site_type', label: ptEn('Tipo de site', 'Site type'), type: 'select', required: true, options: siteTypeOptions },
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')),
365
+ ],
366
+ },
367
+ {
368
+ title: ptEn('Configurações', 'Settings'),
369
+ fields: [
370
+ { key: 'theme_settings_json', label: ptEn('Tema e identidade', 'Theme and identity'), type: 'json', span: 2 },
371
+ { key: 'seo_defaults_json', label: ptEn('Padrões de SEO', 'SEO defaults'), type: 'json', span: 2 },
372
+ ],
373
+ },
374
+ ],
375
+ },
376
+ {
377
+ resource: 'products',
378
+ translationKey: 'products',
379
+ singularLabel: ptEn('Produto', 'Product'),
380
+ createActionLabel: ptEn('Novo Produto', 'New Product'),
381
+ editActionLabel: ptEn('Editar Produto', 'Edit Product'),
382
+ icon: PackageSearch,
383
+ href: '/catalog/products',
384
+ colorClass: 'from-emerald-500/20 via-green-500/10 to-transparent',
385
+ glowClass: 'bg-emerald-500/10 text-emerald-700',
386
+ featured: true,
387
+ hasActiveStats: true,
388
+ listVariant: 'table',
389
+ primaryFilterField: 'status',
390
+ primaryFilterOptions: publicationStatusFilterOptions,
391
+ titleFields: ['name', 'model_name', 'slug'],
392
+ descriptionFields: ['model_name', 'sku', 'slug'],
393
+ badgeFields: ['status', 'comparison_status', 'is_active'],
394
+ cardMetadata: [
395
+ { key: 'slug', labelKey: 'slug' },
396
+ { key: 'sku', labelKey: 'sku' },
397
+ { key: 'brand_name', labelKey: 'brandId' },
398
+ { key: 'category_name', labelKey: 'catalogCategoryId' },
399
+ ],
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 },
409
+ formSections: [
410
+ {
411
+ title: ptEn('Base do produto', 'Product basics'),
412
+ fields: [
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' },
421
+ { key: 'release_date', label: ptEn('Data de lançamento', 'Release date'), type: 'date' },
422
+ { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: publicationStatusOptions },
423
+ { key: 'comparison_status', label: ptEn('Status de comparação', 'Comparison status'), type: 'select', required: true, options: comparisonStatusOptions },
424
+ { key: 'is_active', label: ptEn('Ativo para comparação', 'Active for comparison'), type: 'switch' },
425
+ ],
426
+ },
427
+ {
428
+ title: ptEn('Conteúdo', 'Content'),
429
+ fields: [
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: [
438
+ { key: 'spec_snapshot_json', label: ptEn('Snapshot de especificações', 'Specification snapshot'), type: 'json', span: 2 },
439
+ { key: 'comparison_snapshot_json', label: ptEn('Snapshot de comparação', 'Comparison snapshot'), type: 'json', span: 2 },
440
+ ],
441
+ },
442
+ ],
443
+ }
444
+ );
445
+
446
+ catalogResources.push(
447
+ {
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',
457
+ hasActiveStats: true,
458
+ listVariant: 'cards',
459
+ primaryFilterField: 'status',
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: '' },
467
+ formSections: [
468
+ {
469
+ title: ptEn('Base da comparação', 'Comparison basics'),
470
+ fields: [
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 },
489
+ ],
490
+ },
491
+ ],
492
+ },
493
+ {
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: '' },
513
+ formSections: [
514
+ {
515
+ title: ptEn('Oferta comercial', 'Commercial offer'),
516
+ fields: [
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' },
529
+ ],
530
+ },
531
+ {
532
+ title: ptEn('Links e vigência', 'Links and schedule'),
533
+ fields: [
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 },
540
+ ],
541
+ },
542
+ ],
543
+ },
544
+ {
545
+ resource: 'merchants',
546
+ translationKey: 'merchants',
547
+ singularLabel: ptEn('Lojista', 'Merchant'),
548
+ createActionLabel: ptEn('Novo Lojista', 'New Merchant'),
549
+ editActionLabel: ptEn('Editar Lojista', 'Edit Merchant'),
550
+ icon: Store,
551
+ href: '/catalog/merchants',
552
+ colorClass: 'from-lime-500/20 via-green-500/10 to-transparent',
553
+ glowClass: 'bg-lime-500/10 text-lime-700',
554
+ featured: true,
555
+ hasActiveStats: true,
556
+ listVariant: 'cards',
557
+ primaryFilterField: 'status',
558
+ primaryFilterOptions: activeInactiveStatusFilterOptions,
559
+ titleFields: ['name', 'slug'],
560
+ descriptionFields: ['slug'],
561
+ badgeFields: ['status', 'merchant_type'],
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
+ }],
575
+ },
576
+ {
577
+ resource: 'affiliate-programs',
578
+ translationKey: 'affiliatePrograms',
579
+ singularLabel: ptEn('Programa de afiliação', 'Affiliate program'),
580
+ createActionLabel: ptEn('Novo Programa de Afiliação', 'New Affiliate Program'),
581
+ editActionLabel: ptEn('Editar Programa de Afiliação', 'Edit Affiliate Program'),
582
+ icon: Building2,
583
+ href: '/catalog/affiliate-programs',
584
+ colorClass: 'from-cyan-500/20 via-teal-500/10 to-transparent',
585
+ glowClass: 'bg-cyan-500/10 text-cyan-700',
586
+ hasActiveStats: true,
587
+ listVariant: 'cards',
588
+ primaryFilterField: 'status',
589
+ primaryFilterOptions: activeInactiveStatusFilterOptions,
590
+ titleFields: ['name', 'slug'],
591
+ descriptionFields: ['slug', 'network_type'],
592
+ badgeFields: ['status', 'network_type', 'commission_type'],
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
+ }],
609
+ },
610
+ {
611
+ resource: 'seo-rules',
612
+ translationKey: 'seoRules',
613
+ singularLabel: ptEn('Regra de SEO', 'SEO rule'),
614
+ createActionLabel: ptEn('Nova Regra de SEO', 'New SEO Rule'),
615
+ editActionLabel: ptEn('Editar Regra de SEO', 'Edit SEO Rule'),
616
+ icon: SearchCode,
617
+ href: '/catalog/seo-rules',
618
+ colorClass: 'from-slate-500/20 via-zinc-500/10 to-transparent',
619
+ glowClass: 'bg-slate-500/10 text-slate-700',
620
+ featured: true,
621
+ hasActiveStats: true,
622
+ listVariant: 'cards',
623
+ primaryFilterField: 'status',
624
+ primaryFilterOptions: activeInactiveStatusFilterOptions,
625
+ titleFields: ['rule_slug', 'page_type'],
626
+ descriptionFields: ['page_type', 'canonical_strategy'],
627
+ badgeFields: ['status', 'page_type', 'canonical_strategy'],
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: {} },
631
+ formSections: [
632
+ {
633
+ title: ptEn('Escopo da regra', 'Rule scope'),
634
+ fields: [
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' }) },
637
+ { key: 'page_type', label: ptEn('Tipo de página', 'Page type'), type: 'select', required: true, options: pageTypeOptions },
638
+ { key: 'rule_slug', label: ptEn('Slug da regra', 'Rule slug'), type: 'text', required: true },
639
+ { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
640
+ { key: 'canonical_strategy', label: ptEn('Estratégia canônica', 'Canonical strategy'), type: 'select', required: true, options: canonicalStrategyOptions },
641
+ { key: 'priority', label: ptEn('Prioridade', 'Priority'), type: 'number' },
642
+ { key: 'min_product_count', label: ptEn('Mínimo de produtos', 'Minimum products'), type: 'number' },
643
+ { key: 'min_attribute_coverage', label: ptEn('Cobertura mínima de atributos', 'Minimum attribute coverage'), type: 'number' },
644
+ ],
645
+ },
646
+ {
647
+ title: ptEn('Regras dinâmicas', 'Dynamic rules'),
648
+ fields: [
649
+ { key: 'generation_query_json', label: ptEn('Consulta geradora', 'Generation query'), type: 'json', span: 2 },
650
+ { key: 'template_json', label: ptEn('Template de SEO', 'SEO template'), type: 'json', span: 2 },
651
+ ],
652
+ },
653
+ ],
654
+ },
655
+ {
656
+ resource: 'import-sources',
657
+ translationKey: 'importSources',
658
+ singularLabel: ptEn('Fonte de importação', 'Import source'),
659
+ createActionLabel: ptEn('Nova Fonte de Importação', 'New Import Source'),
660
+ editActionLabel: ptEn('Editar Fonte de Importação', 'Edit Import Source'),
661
+ icon: PackagePlus,
662
+ href: '/catalog/import-sources',
663
+ colorClass: 'from-stone-500/20 via-neutral-500/10 to-transparent',
664
+ glowClass: 'bg-stone-500/10 text-stone-700',
665
+ featured: true,
666
+ hasActiveStats: true,
667
+ listVariant: 'cards',
668
+ primaryFilterField: 'status',
669
+ primaryFilterOptions: activeInactiveStatusFilterOptions,
670
+ titleFields: ['name', 'slug'],
671
+ descriptionFields: ['slug', 'source_type'],
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'],
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' },
751
+ { key: 'slug', labelKey: 'slug' },
752
+ { key: 'data_type', labelKey: 'dataType' },
753
+ { key: 'status', labelKey: 'status' },
754
+ ],
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 },
757
+ formSections: [
758
+ {
759
+ title: ptEn('Configuração do atributo', 'Attribute setup'),
760
+ fields: [
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' },
768
+ { key: 'status', label: ptEn('Status', 'Status'), type: 'select', required: true, options: activeInactiveStatusOptions },
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' },
781
+ ],
782
+ },
783
+ ],
784
+ },
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
+ }
914
+ );
915
+
916
+ export const catalogResourceMap = new Map(catalogResources.map((resource) => [resource.resource, resource]));
917
+ export const catalogDashboardHref = '/catalog/dashboard';
918
+ export const catalogQuickActionResources = catalogResources.filter((resource) => resource.featured);
919
+ export const catalogKpiResources = ['categories', 'products', 'brands', 'offers'];
920
+
921
+ export function getCatalogRecordLabel(record: Record<string, unknown>) {
922
+ return record.name || record.title || record.label || record.code || record.slug || record.rule_slug || record.option_value || record.external_offer_id || `#${record.id}`;
923
+ }
924
+
925
+ export function getCatalogLocalizedText(value: CatalogLocalizedText, localeCode?: string | null) {
926
+ return localeCode?.toLowerCase().startsWith('pt') ? value.pt : value.en;
927
+ }
928
+
929
+ export const catalogModuleIcon = Boxes;