@hed-hog/catalog 0.0.293 → 0.0.295
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +391 -361
- package/dist/catalog-resource.config.d.ts.map +1 -1
- package/dist/catalog-resource.config.js +51 -24
- package/dist/catalog-resource.config.js.map +1 -1
- package/dist/catalog.controller.d.ts +420 -0
- package/dist/catalog.controller.d.ts.map +1 -1
- package/dist/catalog.controller.js +98 -0
- package/dist/catalog.controller.js.map +1 -1
- package/dist/catalog.module.d.ts.map +1 -1
- package/dist/catalog.module.js +5 -1
- package/dist/catalog.module.js.map +1 -1
- package/dist/catalog.service.d.ts +216 -1
- package/dist/catalog.service.d.ts.map +1 -1
- package/dist/catalog.service.js +1121 -7
- package/dist/catalog.service.js.map +1 -1
- package/hedhog/data/catalog_attribute.yaml +202 -0
- package/hedhog/data/catalog_attribute_option.yaml +109 -0
- package/hedhog/data/catalog_category.yaml +47 -0
- package/hedhog/data/catalog_category_attribute.yaml +209 -0
- package/hedhog/data/menu.yaml +46 -12
- package/hedhog/data/role.yaml +7 -7
- package/hedhog/data/route.yaml +64 -0
- package/hedhog/frontend/app/[resource]/page.tsx.ejs +358 -0
- package/hedhog/frontend/app/_components/catalog-ai-form-assist-dialog.tsx.ejs +340 -0
- package/hedhog/frontend/app/_components/catalog-resource-form-sheet.tsx.ejs +815 -0
- package/hedhog/frontend/app/_lib/catalog-resources.tsx.ejs +504 -736
- package/hedhog/frontend/app/dashboard/page.tsx.ejs +14 -83
- package/hedhog/frontend/messages/en.json +150 -60
- package/hedhog/frontend/messages/pt.json +185 -95
- package/hedhog/table/catalog_affiliate_program.yaml +41 -41
- package/hedhog/table/catalog_attribute.yaml +22 -7
- package/hedhog/table/catalog_attribute_group.yaml +18 -18
- package/hedhog/table/catalog_attribute_option.yaml +40 -0
- package/hedhog/table/catalog_brand.yaml +34 -34
- package/hedhog/table/catalog_category.yaml +40 -0
- package/hedhog/table/catalog_category_attribute.yaml +13 -7
- package/hedhog/table/catalog_click_event.yaml +50 -50
- package/hedhog/table/catalog_comparison.yaml +3 -6
- package/hedhog/table/catalog_comparison_highlight.yaml +39 -39
- package/hedhog/table/catalog_comparison_item.yaml +30 -30
- package/hedhog/table/catalog_content_relation.yaml +42 -42
- package/hedhog/table/catalog_import_run.yaml +33 -33
- package/hedhog/table/catalog_import_source.yaml +24 -24
- package/hedhog/table/catalog_merchant.yaml +29 -29
- package/hedhog/table/catalog_offer.yaml +83 -83
- package/hedhog/table/catalog_price_history.yaml +34 -34
- package/hedhog/table/catalog_product.yaml +5 -3
- package/hedhog/table/catalog_product_attribute_value.yaml +15 -2
- package/hedhog/table/catalog_product_category.yaml +3 -3
- package/hedhog/table/catalog_product_image.yaml +34 -34
- package/hedhog/table/catalog_product_score.yaml +38 -38
- package/hedhog/table/catalog_product_site.yaml +47 -47
- package/hedhog/table/catalog_product_tag.yaml +19 -19
- package/hedhog/table/catalog_score_criterion.yaml +25 -8
- package/hedhog/table/catalog_seo_page_rule.yaml +2 -2
- package/hedhog/table/catalog_similarity_rule.yaml +19 -6
- package/hedhog/table/catalog_site.yaml +8 -0
- package/hedhog/table/catalog_site_category.yaml +3 -3
- package/package.json +7 -7
- package/src/catalog-resource.config.ts +51 -24
- package/src/catalog.controller.ts +67 -0
- package/src/catalog.module.ts +5 -1
- package/src/catalog.service.ts +1531 -6
- package/src/index.ts +1 -1
- package/src/language/en.json +4 -4
- package/src/language/pt.json +4 -4
package/hedhog/data/menu.yaml
CHANGED
|
@@ -28,6 +28,23 @@
|
|
|
28
28
|
- where:
|
|
29
29
|
slug: admin-catalog
|
|
30
30
|
|
|
31
|
+
- menu_id:
|
|
32
|
+
where:
|
|
33
|
+
slug: /catalog
|
|
34
|
+
icon: folders
|
|
35
|
+
url: /catalog/categories
|
|
36
|
+
name:
|
|
37
|
+
en: Categories
|
|
38
|
+
pt: Categorias
|
|
39
|
+
slug: /catalog/categories
|
|
40
|
+
order: 72
|
|
41
|
+
relations:
|
|
42
|
+
role:
|
|
43
|
+
- where:
|
|
44
|
+
slug: admin
|
|
45
|
+
- where:
|
|
46
|
+
slug: admin-catalog
|
|
47
|
+
|
|
31
48
|
- menu_id:
|
|
32
49
|
where:
|
|
33
50
|
slug: /catalog
|
|
@@ -37,7 +54,7 @@
|
|
|
37
54
|
en: Brands
|
|
38
55
|
pt: Marcas
|
|
39
56
|
slug: /catalog/brands
|
|
40
|
-
order:
|
|
57
|
+
order: 73
|
|
41
58
|
relations:
|
|
42
59
|
role:
|
|
43
60
|
- where:
|
|
@@ -54,7 +71,7 @@
|
|
|
54
71
|
en: Sites
|
|
55
72
|
pt: Sites
|
|
56
73
|
slug: /catalog/sites
|
|
57
|
-
order:
|
|
74
|
+
order: 74
|
|
58
75
|
relations:
|
|
59
76
|
role:
|
|
60
77
|
- where:
|
|
@@ -71,7 +88,7 @@
|
|
|
71
88
|
en: Products
|
|
72
89
|
pt: Produtos
|
|
73
90
|
slug: /catalog/products
|
|
74
|
-
order:
|
|
91
|
+
order: 75
|
|
75
92
|
relations:
|
|
76
93
|
role:
|
|
77
94
|
- where:
|
|
@@ -88,7 +105,7 @@
|
|
|
88
105
|
en: Attribute Groups
|
|
89
106
|
pt: Grupos de atributos
|
|
90
107
|
slug: /catalog/attribute-groups
|
|
91
|
-
order:
|
|
108
|
+
order: 76
|
|
92
109
|
relations:
|
|
93
110
|
role:
|
|
94
111
|
- where:
|
|
@@ -105,7 +122,24 @@
|
|
|
105
122
|
en: Attributes
|
|
106
123
|
pt: Atributos
|
|
107
124
|
slug: /catalog/attributes
|
|
108
|
-
order:
|
|
125
|
+
order: 77
|
|
126
|
+
relations:
|
|
127
|
+
role:
|
|
128
|
+
- where:
|
|
129
|
+
slug: admin
|
|
130
|
+
- where:
|
|
131
|
+
slug: admin-catalog
|
|
132
|
+
|
|
133
|
+
- menu_id:
|
|
134
|
+
where:
|
|
135
|
+
slug: /catalog
|
|
136
|
+
icon: list-filter
|
|
137
|
+
url: /catalog/attribute-options
|
|
138
|
+
name:
|
|
139
|
+
en: Attribute Options
|
|
140
|
+
pt: Opcoes de atributos
|
|
141
|
+
slug: /catalog/attribute-options
|
|
142
|
+
order: 78
|
|
109
143
|
relations:
|
|
110
144
|
role:
|
|
111
145
|
- where:
|
|
@@ -122,7 +156,7 @@
|
|
|
122
156
|
en: Category Attributes
|
|
123
157
|
pt: Atributos por categoria
|
|
124
158
|
slug: /catalog/category-attributes
|
|
125
|
-
order:
|
|
159
|
+
order: 79
|
|
126
160
|
relations:
|
|
127
161
|
role:
|
|
128
162
|
- where:
|
|
@@ -139,7 +173,7 @@
|
|
|
139
173
|
en: Comparisons
|
|
140
174
|
pt: Comparacoes
|
|
141
175
|
slug: /catalog/comparisons
|
|
142
|
-
order:
|
|
176
|
+
order: 80
|
|
143
177
|
relations:
|
|
144
178
|
role:
|
|
145
179
|
- where:
|
|
@@ -156,7 +190,7 @@
|
|
|
156
190
|
en: Offers
|
|
157
191
|
pt: Ofertas
|
|
158
192
|
slug: /catalog/offers
|
|
159
|
-
order:
|
|
193
|
+
order: 81
|
|
160
194
|
relations:
|
|
161
195
|
role:
|
|
162
196
|
- where:
|
|
@@ -173,7 +207,7 @@
|
|
|
173
207
|
en: Merchants
|
|
174
208
|
pt: Lojistas
|
|
175
209
|
slug: /catalog/merchants
|
|
176
|
-
order:
|
|
210
|
+
order: 82
|
|
177
211
|
relations:
|
|
178
212
|
role:
|
|
179
213
|
- where:
|
|
@@ -190,7 +224,7 @@
|
|
|
190
224
|
en: Affiliate Programs
|
|
191
225
|
pt: Programas de afiliacao
|
|
192
226
|
slug: /catalog/affiliate-programs
|
|
193
|
-
order:
|
|
227
|
+
order: 83
|
|
194
228
|
relations:
|
|
195
229
|
role:
|
|
196
230
|
- where:
|
|
@@ -207,7 +241,7 @@
|
|
|
207
241
|
en: SEO Rules
|
|
208
242
|
pt: Regras de SEO
|
|
209
243
|
slug: /catalog/seo-rules
|
|
210
|
-
order:
|
|
244
|
+
order: 84
|
|
211
245
|
relations:
|
|
212
246
|
role:
|
|
213
247
|
- where:
|
|
@@ -224,7 +258,7 @@
|
|
|
224
258
|
en: Import Sources
|
|
225
259
|
pt: Fontes de importacao
|
|
226
260
|
slug: /catalog/import-sources
|
|
227
|
-
order:
|
|
261
|
+
order: 85
|
|
228
262
|
relations:
|
|
229
263
|
role:
|
|
230
264
|
- where:
|
package/hedhog/data/role.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
- slug: admin-catalog
|
|
2
|
-
name:
|
|
3
|
-
en: Catalog Administrator
|
|
4
|
-
pt: Administrador de Catálogo
|
|
5
|
-
description:
|
|
6
|
-
en: Administrator with access to catalog, comparisons, offers and SEO rules.
|
|
7
|
-
pt: Administrador com acesso ao catálogo, comparações, ofertas e regras de SEO.
|
|
1
|
+
- slug: admin-catalog
|
|
2
|
+
name:
|
|
3
|
+
en: Catalog Administrator
|
|
4
|
+
pt: Administrador de Catálogo
|
|
5
|
+
description:
|
|
6
|
+
en: Administrator with access to catalog, comparisons, offers and SEO rules.
|
|
7
|
+
pt: Administrador com acesso ao catálogo, comparações, ofertas e regras de SEO.
|
package/hedhog/data/route.yaml
CHANGED
|
@@ -54,3 +54,67 @@
|
|
|
54
54
|
slug: admin
|
|
55
55
|
- where:
|
|
56
56
|
slug: admin-catalog
|
|
57
|
+
- url: /catalog/categories/tree
|
|
58
|
+
method: GET
|
|
59
|
+
relations:
|
|
60
|
+
role:
|
|
61
|
+
- where:
|
|
62
|
+
slug: admin
|
|
63
|
+
- where:
|
|
64
|
+
slug: admin-catalog
|
|
65
|
+
- url: /catalog/categories/:id/attributes
|
|
66
|
+
method: GET
|
|
67
|
+
relations:
|
|
68
|
+
role:
|
|
69
|
+
- where:
|
|
70
|
+
slug: admin
|
|
71
|
+
- where:
|
|
72
|
+
slug: admin-catalog
|
|
73
|
+
- url: /catalog/products/:id/attributes
|
|
74
|
+
method: GET
|
|
75
|
+
relations:
|
|
76
|
+
role:
|
|
77
|
+
- where:
|
|
78
|
+
slug: admin
|
|
79
|
+
- where:
|
|
80
|
+
slug: admin-catalog
|
|
81
|
+
- url: /catalog/products/:id/structured
|
|
82
|
+
method: GET
|
|
83
|
+
relations:
|
|
84
|
+
role:
|
|
85
|
+
- where:
|
|
86
|
+
slug: admin
|
|
87
|
+
- where:
|
|
88
|
+
slug: admin-catalog
|
|
89
|
+
- url: /catalog/products/:id/comparison-payload
|
|
90
|
+
method: GET
|
|
91
|
+
relations:
|
|
92
|
+
role:
|
|
93
|
+
- where:
|
|
94
|
+
slug: admin
|
|
95
|
+
- where:
|
|
96
|
+
slug: admin-catalog
|
|
97
|
+
- url: /catalog/forms/:resource/ai-assist
|
|
98
|
+
method: POST
|
|
99
|
+
relations:
|
|
100
|
+
role:
|
|
101
|
+
- where:
|
|
102
|
+
slug: admin
|
|
103
|
+
- where:
|
|
104
|
+
slug: admin-catalog
|
|
105
|
+
- url: /catalog/products/:id/attributes
|
|
106
|
+
method: PUT
|
|
107
|
+
relations:
|
|
108
|
+
role:
|
|
109
|
+
- where:
|
|
110
|
+
slug: admin
|
|
111
|
+
- where:
|
|
112
|
+
slug: admin-catalog
|
|
113
|
+
- url: /catalog/products/:id/materialize-snapshots
|
|
114
|
+
method: POST
|
|
115
|
+
relations:
|
|
116
|
+
role:
|
|
117
|
+
- where:
|
|
118
|
+
slug: admin
|
|
119
|
+
- where:
|
|
120
|
+
slug: admin-catalog
|
|
@@ -26,8 +26,16 @@ import {
|
|
|
26
26
|
AlertDialogHeader,
|
|
27
27
|
AlertDialogTitle,
|
|
28
28
|
} from '@/components/ui/alert-dialog';
|
|
29
|
+
import { Badge } from '@/components/ui/badge';
|
|
29
30
|
import { Button } from '@/components/ui/button';
|
|
30
31
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
32
|
+
import {
|
|
33
|
+
Dialog,
|
|
34
|
+
DialogContent,
|
|
35
|
+
DialogDescription,
|
|
36
|
+
DialogHeader,
|
|
37
|
+
DialogTitle,
|
|
38
|
+
} from '@/components/ui/dialog';
|
|
31
39
|
import {
|
|
32
40
|
Table,
|
|
33
41
|
TableBody,
|
|
@@ -58,6 +66,20 @@ type ResourceStats = {
|
|
|
58
66
|
};
|
|
59
67
|
|
|
60
68
|
type CatalogRecord = Record<string, unknown>;
|
|
69
|
+
type ProductAttributePayload = {
|
|
70
|
+
groups?: Array<{
|
|
71
|
+
id: number | null;
|
|
72
|
+
name: string;
|
|
73
|
+
slug: string;
|
|
74
|
+
attributes: Array<
|
|
75
|
+
CatalogRecord & {
|
|
76
|
+
options?: CatalogRecord[];
|
|
77
|
+
value?: CatalogRecord | null;
|
|
78
|
+
category_attribute?: CatalogRecord;
|
|
79
|
+
}
|
|
80
|
+
>;
|
|
81
|
+
}>;
|
|
82
|
+
};
|
|
61
83
|
|
|
62
84
|
function humanizeValue(value: string) {
|
|
63
85
|
return value
|
|
@@ -65,6 +87,306 @@ function humanizeValue(value: string) {
|
|
|
65
87
|
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
66
88
|
}
|
|
67
89
|
|
|
90
|
+
function CatalogProductPreviewDialog({
|
|
91
|
+
open,
|
|
92
|
+
onOpenChange,
|
|
93
|
+
productId,
|
|
94
|
+
}: {
|
|
95
|
+
open: boolean;
|
|
96
|
+
onOpenChange: (open: boolean) => void;
|
|
97
|
+
productId: number | null;
|
|
98
|
+
}) {
|
|
99
|
+
const { request, currentLocaleCode } = useApp();
|
|
100
|
+
const t = useTranslations('catalog');
|
|
101
|
+
const isPt = currentLocaleCode?.startsWith('pt');
|
|
102
|
+
const { data: product, isLoading: isLoadingProduct } = useQuery<CatalogRecord | null>({
|
|
103
|
+
queryKey: ['catalog-product-preview', productId],
|
|
104
|
+
queryFn: async () => {
|
|
105
|
+
if (!productId) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const response = await request({
|
|
110
|
+
url: `/catalog/products/${productId}`,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return (response.data ?? null) as CatalogRecord | null;
|
|
114
|
+
},
|
|
115
|
+
enabled: open && Boolean(productId),
|
|
116
|
+
});
|
|
117
|
+
const { data: attributes, isLoading: isLoadingAttributes } =
|
|
118
|
+
useQuery<ProductAttributePayload | null>({
|
|
119
|
+
queryKey: ['catalog-product-preview-attributes', productId],
|
|
120
|
+
queryFn: async () => {
|
|
121
|
+
if (!productId) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const response = await request({
|
|
126
|
+
url: `/catalog/products/${productId}/attributes`,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return (response.data ?? null) as ProductAttributePayload | null;
|
|
130
|
+
},
|
|
131
|
+
enabled: open && Boolean(productId),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const formatAttributeValue = (attribute: CatalogRecord & {
|
|
135
|
+
options?: CatalogRecord[];
|
|
136
|
+
value?: CatalogRecord | null;
|
|
137
|
+
}) => {
|
|
138
|
+
const value = (attribute.value ?? null) as CatalogRecord | null;
|
|
139
|
+
|
|
140
|
+
if (!value) {
|
|
141
|
+
return '-';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const dataType = String(attribute.data_type ?? 'text');
|
|
145
|
+
|
|
146
|
+
if (dataType === 'option') {
|
|
147
|
+
const option = (attribute.options ?? []).find(
|
|
148
|
+
(item) => Number(item.id ?? 0) === Number(value.attribute_option_id ?? 0),
|
|
149
|
+
);
|
|
150
|
+
return String(option?.label ?? option?.option_value ?? value.value_text ?? '-');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (dataType === 'number') {
|
|
154
|
+
const numberValue =
|
|
155
|
+
value.value_number === null || value.value_number === undefined
|
|
156
|
+
? null
|
|
157
|
+
: Number(value.value_number);
|
|
158
|
+
|
|
159
|
+
if (numberValue === null) {
|
|
160
|
+
return '-';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const unit = String(value.value_unit ?? attribute.unit ?? '').trim();
|
|
164
|
+
return unit ? `${numberValue} ${unit}` : String(numberValue);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (dataType === 'boolean') {
|
|
168
|
+
if (value.value_boolean === null || value.value_boolean === undefined) {
|
|
169
|
+
return '-';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return value.value_boolean
|
|
173
|
+
? t('resource.booleans.true')
|
|
174
|
+
: t('resource.booleans.false');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return String(value.value_text ?? '-');
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const renderJsonBlock = (value: unknown) => {
|
|
181
|
+
if (!value || (typeof value === 'object' && Object.keys(value as object).length === 0)) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<pre className="overflow-x-auto rounded-md border bg-muted/30 p-3 text-xs">
|
|
187
|
+
{JSON.stringify(value, null, 2)}
|
|
188
|
+
</pre>
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const specSnapshotBlock = renderJsonBlock(product?.spec_snapshot_json);
|
|
193
|
+
const comparisonSnapshotBlock = renderJsonBlock(product?.comparison_snapshot_json);
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
197
|
+
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-4xl">
|
|
198
|
+
<DialogHeader>
|
|
199
|
+
<DialogTitle>
|
|
200
|
+
{String(product?.name ?? (isPt ? 'Visualizar produto' : 'View product'))}
|
|
201
|
+
</DialogTitle>
|
|
202
|
+
<DialogDescription>
|
|
203
|
+
{isPt
|
|
204
|
+
? 'Dados básicos, atributos técnicos por grupo, imagens e snapshots secundários.'
|
|
205
|
+
: 'Basic data, technical attributes by group, images, and secondary snapshots.'}
|
|
206
|
+
</DialogDescription>
|
|
207
|
+
</DialogHeader>
|
|
208
|
+
|
|
209
|
+
{isLoadingProduct || isLoadingAttributes ? (
|
|
210
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
211
|
+
<Eye className="size-4" />
|
|
212
|
+
{isPt ? 'Carregando visualização do produto...' : 'Loading product preview...'}
|
|
213
|
+
</div>
|
|
214
|
+
) : !product ? (
|
|
215
|
+
<p className="text-sm text-muted-foreground">
|
|
216
|
+
{isPt ? 'Produto não encontrado.' : 'Product not found.'}
|
|
217
|
+
</p>
|
|
218
|
+
) : (
|
|
219
|
+
<div className="space-y-6">
|
|
220
|
+
<Card>
|
|
221
|
+
<CardHeader>
|
|
222
|
+
<CardTitle>{isPt ? 'Dados básicos do produto' : 'Product basics'}</CardTitle>
|
|
223
|
+
</CardHeader>
|
|
224
|
+
<CardContent className="grid gap-4 sm:grid-cols-2">
|
|
225
|
+
<div>
|
|
226
|
+
<div className="text-xs text-muted-foreground">
|
|
227
|
+
{t('resource.fields.brandId')}
|
|
228
|
+
</div>
|
|
229
|
+
<div className="font-medium">
|
|
230
|
+
{String(product.brand_name ?? product.brand_id ?? '-')}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
<div>
|
|
234
|
+
<div className="text-xs text-muted-foreground">
|
|
235
|
+
{t('resource.fields.catalogCategoryId')}
|
|
236
|
+
</div>
|
|
237
|
+
<div className="font-medium">
|
|
238
|
+
{String(product.category_name ?? product.catalog_category_id ?? '-')}
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
<div>
|
|
242
|
+
<div className="text-xs text-muted-foreground">{t('resource.fields.status')}</div>
|
|
243
|
+
<div className="font-medium">
|
|
244
|
+
{String(product.status ?? '-')}
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
<div>
|
|
248
|
+
<div className="text-xs text-muted-foreground">{t('resource.fields.isActive')}</div>
|
|
249
|
+
<div className="font-medium">
|
|
250
|
+
{product.is_active === true
|
|
251
|
+
? t('resource.booleans.true')
|
|
252
|
+
: t('resource.booleans.false')}
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
<div>
|
|
256
|
+
<div className="text-xs text-muted-foreground">{t('resource.fields.sku')}</div>
|
|
257
|
+
<div className="font-medium">{String(product.sku ?? '-')}</div>
|
|
258
|
+
</div>
|
|
259
|
+
<div>
|
|
260
|
+
<div className="text-xs text-muted-foreground">GTIN</div>
|
|
261
|
+
<div className="font-medium">{String(product.gtin ?? '-')}</div>
|
|
262
|
+
</div>
|
|
263
|
+
</CardContent>
|
|
264
|
+
</Card>
|
|
265
|
+
|
|
266
|
+
<Card>
|
|
267
|
+
<CardHeader>
|
|
268
|
+
<CardTitle>{isPt ? 'Atributos técnicos' : 'Technical attributes'}</CardTitle>
|
|
269
|
+
<CardDescription>
|
|
270
|
+
{isPt
|
|
271
|
+
? 'Organizados pelos grupos configurados na categoria do produto.'
|
|
272
|
+
: 'Organized by the groups configured for the product category.'}
|
|
273
|
+
</CardDescription>
|
|
274
|
+
</CardHeader>
|
|
275
|
+
<CardContent className="space-y-6">
|
|
276
|
+
{!(attributes?.groups?.length) ? (
|
|
277
|
+
<p className="text-sm text-muted-foreground">
|
|
278
|
+
{isPt
|
|
279
|
+
? 'Nenhum atributo técnico configurado para este produto.'
|
|
280
|
+
: 'No technical attributes configured for this product.'}
|
|
281
|
+
</p>
|
|
282
|
+
) : (
|
|
283
|
+
attributes.groups.map((group) => (
|
|
284
|
+
<div key={`${group.slug}-${group.id ?? 'general'}`} className="space-y-3">
|
|
285
|
+
<div className="text-sm font-semibold">{group.name}</div>
|
|
286
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
287
|
+
{group.attributes.map((attribute) => {
|
|
288
|
+
const relation = (attribute.category_attribute ?? {}) as CatalogRecord;
|
|
289
|
+
const badges = [
|
|
290
|
+
relation.is_highlight
|
|
291
|
+
? isPt
|
|
292
|
+
? 'Destaque'
|
|
293
|
+
: 'Highlight'
|
|
294
|
+
: null,
|
|
295
|
+
relation.is_filter_visible
|
|
296
|
+
? isPt
|
|
297
|
+
? 'Filtro'
|
|
298
|
+
: 'Filter'
|
|
299
|
+
: null,
|
|
300
|
+
relation.is_comparison_visible
|
|
301
|
+
? isPt
|
|
302
|
+
? 'Comparação'
|
|
303
|
+
: 'Comparison'
|
|
304
|
+
: null,
|
|
305
|
+
].filter((item): item is string => Boolean(item));
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<div key={String(attribute.id)} className="rounded-lg border p-3">
|
|
309
|
+
<div className="flex items-start justify-between gap-3">
|
|
310
|
+
<div className="space-y-1">
|
|
311
|
+
<div className="font-medium">
|
|
312
|
+
{String(attribute.name ?? attribute.label ?? attribute.slug)}
|
|
313
|
+
</div>
|
|
314
|
+
<div className="text-sm text-muted-foreground">
|
|
315
|
+
{formatAttributeValue(attribute)}
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
{badges.length ? (
|
|
319
|
+
<div className="flex flex-wrap justify-end gap-2">
|
|
320
|
+
{badges.map((badge) => (
|
|
321
|
+
<Badge key={`${attribute.id}-${badge}`} variant="secondary">
|
|
322
|
+
{badge}
|
|
323
|
+
</Badge>
|
|
324
|
+
))}
|
|
325
|
+
</div>
|
|
326
|
+
) : null}
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
);
|
|
330
|
+
})}
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
))
|
|
334
|
+
)}
|
|
335
|
+
</CardContent>
|
|
336
|
+
</Card>
|
|
337
|
+
|
|
338
|
+
{Array.isArray(product.catalog_product_image) &&
|
|
339
|
+
product.catalog_product_image.length > 0 ? (
|
|
340
|
+
<Card>
|
|
341
|
+
<CardHeader>
|
|
342
|
+
<CardTitle>{isPt ? 'Imagens do produto' : 'Product images'}</CardTitle>
|
|
343
|
+
</CardHeader>
|
|
344
|
+
<CardContent className="grid gap-3 sm:grid-cols-2">
|
|
345
|
+
{product.catalog_product_image.map((image) => (
|
|
346
|
+
<div
|
|
347
|
+
key={String(image.id)}
|
|
348
|
+
className="rounded-lg border p-3 text-sm"
|
|
349
|
+
>
|
|
350
|
+
<div className="font-medium">
|
|
351
|
+
#{String(image.id)} · {String(image.role ?? 'gallery')}
|
|
352
|
+
</div>
|
|
353
|
+
<div className="text-muted-foreground">
|
|
354
|
+
File #{String(image.file_id ?? '-')}
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
))}
|
|
358
|
+
</CardContent>
|
|
359
|
+
</Card>
|
|
360
|
+
) : null}
|
|
361
|
+
|
|
362
|
+
<Card>
|
|
363
|
+
<CardHeader>
|
|
364
|
+
<CardTitle>{isPt ? 'Snapshots secundários' : 'Secondary snapshots'}</CardTitle>
|
|
365
|
+
<CardDescription>
|
|
366
|
+
{isPt
|
|
367
|
+
? 'Mantidos como apoio para cache e leituras legadas.'
|
|
368
|
+
: 'Kept as support for cache and legacy reads.'}
|
|
369
|
+
</CardDescription>
|
|
370
|
+
</CardHeader>
|
|
371
|
+
<CardContent className="space-y-4">
|
|
372
|
+
{specSnapshotBlock}
|
|
373
|
+
{comparisonSnapshotBlock}
|
|
374
|
+
{!specSnapshotBlock && !comparisonSnapshotBlock ? (
|
|
375
|
+
<p className="text-sm text-muted-foreground">
|
|
376
|
+
{isPt
|
|
377
|
+
? 'Nenhum snapshot secundário disponível.'
|
|
378
|
+
: 'No secondary snapshots available.'}
|
|
379
|
+
</p>
|
|
380
|
+
) : null}
|
|
381
|
+
</CardContent>
|
|
382
|
+
</Card>
|
|
383
|
+
</div>
|
|
384
|
+
)}
|
|
385
|
+
</DialogContent>
|
|
386
|
+
</Dialog>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
68
390
|
export default function CatalogResourcePage() {
|
|
69
391
|
const params = useParams<{ resource: string }>();
|
|
70
392
|
const resource = params?.resource ?? '';
|
|
@@ -79,6 +401,7 @@ export default function CatalogResourcePage() {
|
|
|
79
401
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
80
402
|
const [deleteId, setDeleteId] = useState<number | null>(null);
|
|
81
403
|
const [editingRecordId, setEditingRecordId] = useState<number | null>(null);
|
|
404
|
+
const [viewingRecordId, setViewingRecordId] = useState<number | null>(null);
|
|
82
405
|
const resourceTitle = config
|
|
83
406
|
? t(`resources.${config.translationKey}.title`)
|
|
84
407
|
: t('unsupportedResource');
|
|
@@ -364,6 +687,10 @@ export default function CatalogResourcePage() {
|
|
|
364
687
|
setSheetOpen(true);
|
|
365
688
|
};
|
|
366
689
|
|
|
690
|
+
const openView = (record: CatalogRecord) => {
|
|
691
|
+
setViewingRecordId(Number(record.id));
|
|
692
|
+
};
|
|
693
|
+
|
|
367
694
|
const handleDelete = async () => {
|
|
368
695
|
if (!deleteId) {
|
|
369
696
|
return;
|
|
@@ -527,6 +854,15 @@ export default function CatalogResourcePage() {
|
|
|
527
854
|
))}
|
|
528
855
|
<TableCell className="text-right">
|
|
529
856
|
<div className="flex justify-end gap-2">
|
|
857
|
+
{resource === 'products' ? (
|
|
858
|
+
<Button
|
|
859
|
+
variant="outline"
|
|
860
|
+
size="icon"
|
|
861
|
+
onClick={() => openView(record)}
|
|
862
|
+
>
|
|
863
|
+
<Eye className="size-4" />
|
|
864
|
+
</Button>
|
|
865
|
+
) : null}
|
|
530
866
|
<Button
|
|
531
867
|
variant="outline"
|
|
532
868
|
size="icon"
|
|
@@ -574,6 +910,16 @@ export default function CatalogResourcePage() {
|
|
|
574
910
|
badges={getBadges(record)}
|
|
575
911
|
metadata={getCardMetadata(record)}
|
|
576
912
|
actions={[
|
|
913
|
+
...(resource === 'products'
|
|
914
|
+
? [
|
|
915
|
+
{
|
|
916
|
+
label: t('openResource'),
|
|
917
|
+
onClick: () => openView(record),
|
|
918
|
+
variant: 'outline' as const,
|
|
919
|
+
icon: <Eye className="size-4" />,
|
|
920
|
+
},
|
|
921
|
+
]
|
|
922
|
+
: []),
|
|
577
923
|
{
|
|
578
924
|
label: t('edit'),
|
|
579
925
|
onClick: () => openEdit(record),
|
|
@@ -623,6 +969,18 @@ export default function CatalogResourcePage() {
|
|
|
623
969
|
}}
|
|
624
970
|
/>
|
|
625
971
|
|
|
972
|
+
{resource === 'products' ? (
|
|
973
|
+
<CatalogProductPreviewDialog
|
|
974
|
+
open={viewingRecordId !== null}
|
|
975
|
+
onOpenChange={(open) => {
|
|
976
|
+
if (!open) {
|
|
977
|
+
setViewingRecordId(null);
|
|
978
|
+
}
|
|
979
|
+
}}
|
|
980
|
+
productId={viewingRecordId}
|
|
981
|
+
/>
|
|
982
|
+
) : null}
|
|
983
|
+
|
|
626
984
|
<AlertDialog
|
|
627
985
|
open={deleteId !== null}
|
|
628
986
|
onOpenChange={(open) => !open && setDeleteId(null)}
|