@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
4
|
+
import { Badge } from '@/components/ui/badge';
|
|
4
5
|
import { Button } from '@/components/ui/button';
|
|
5
6
|
import {
|
|
6
7
|
Command,
|
|
@@ -52,6 +53,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
52
53
|
import { useForm } from 'react-hook-form';
|
|
53
54
|
import { toast } from 'sonner';
|
|
54
55
|
import { z } from 'zod';
|
|
56
|
+
import { CatalogAiFormAssistDialog } from './catalog-ai-form-assist-dialog';
|
|
55
57
|
import {
|
|
56
58
|
catalogResourceMap,
|
|
57
59
|
getCatalogLocalizedText,
|
|
@@ -63,6 +65,40 @@ import {
|
|
|
63
65
|
type CatalogRecord = Record<string, unknown>;
|
|
64
66
|
type CatalogFormValues = Record<string, unknown>;
|
|
65
67
|
type RelationOption = { id: number; label: string };
|
|
68
|
+
type ProductAttributeMetadata = CatalogRecord & {
|
|
69
|
+
id: number;
|
|
70
|
+
data_type?: string;
|
|
71
|
+
unit?: string | null;
|
|
72
|
+
options?: CatalogRecord[];
|
|
73
|
+
category_attribute?: CatalogRecord;
|
|
74
|
+
};
|
|
75
|
+
type ProductAttributeDraft = {
|
|
76
|
+
attribute_id: number;
|
|
77
|
+
data_type: string;
|
|
78
|
+
attribute_option_id: number | null;
|
|
79
|
+
value_text: string;
|
|
80
|
+
value_number: number | null;
|
|
81
|
+
value_boolean: boolean | null;
|
|
82
|
+
value_unit: string;
|
|
83
|
+
source_type: string;
|
|
84
|
+
confidence_score: number | null;
|
|
85
|
+
is_verified: boolean;
|
|
86
|
+
};
|
|
87
|
+
type CatalogAttributePayload = {
|
|
88
|
+
category?: CatalogRecord;
|
|
89
|
+
groups?: Array<{
|
|
90
|
+
id: number | null;
|
|
91
|
+
name: string;
|
|
92
|
+
slug: string;
|
|
93
|
+
attributes: Array<
|
|
94
|
+
CatalogRecord & {
|
|
95
|
+
options?: CatalogRecord[];
|
|
96
|
+
value?: CatalogRecord | null;
|
|
97
|
+
category_attribute?: CatalogRecord;
|
|
98
|
+
}
|
|
99
|
+
>;
|
|
100
|
+
}>;
|
|
101
|
+
};
|
|
66
102
|
type FormSheetProps = {
|
|
67
103
|
open: boolean;
|
|
68
104
|
onOpenChange: (open: boolean) => void;
|
|
@@ -225,6 +261,25 @@ function buildDefaultValues(
|
|
|
225
261
|
return defaults;
|
|
226
262
|
}
|
|
227
263
|
|
|
264
|
+
function buildCurrentFormPayload(
|
|
265
|
+
resourceConfig: CatalogResourceDefinition,
|
|
266
|
+
values: CatalogFormValues,
|
|
267
|
+
) {
|
|
268
|
+
const payload: CatalogRecord = {};
|
|
269
|
+
|
|
270
|
+
for (const section of resourceConfig.formSections) {
|
|
271
|
+
for (const field of section.fields) {
|
|
272
|
+
try {
|
|
273
|
+
payload[field.key] = normalizeSubmitValue(field, values[field.key]);
|
|
274
|
+
} catch {
|
|
275
|
+
payload[field.key] = values[field.key] ?? null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return payload;
|
|
281
|
+
}
|
|
282
|
+
|
|
228
283
|
function normalizeSubmitValue(
|
|
229
284
|
field: CatalogFormFieldDefinition,
|
|
230
285
|
rawValue: unknown
|
|
@@ -271,6 +326,225 @@ function normalizeSubmitValue(
|
|
|
271
326
|
return rawValue;
|
|
272
327
|
}
|
|
273
328
|
|
|
329
|
+
function buildAttributeDrafts(payload?: CatalogAttributePayload | null) {
|
|
330
|
+
const drafts: Record<number, ProductAttributeDraft> = {};
|
|
331
|
+
|
|
332
|
+
for (const group of payload?.groups ?? []) {
|
|
333
|
+
for (const attribute of group.attributes ?? []) {
|
|
334
|
+
const attributeId = Number(attribute.id ?? 0);
|
|
335
|
+
const value = (attribute.value ?? null) as CatalogRecord | null;
|
|
336
|
+
|
|
337
|
+
if (!attributeId) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
drafts[attributeId] = {
|
|
342
|
+
attribute_id: attributeId,
|
|
343
|
+
data_type: String(attribute.data_type ?? 'text'),
|
|
344
|
+
attribute_option_id:
|
|
345
|
+
value?.attribute_option_id === null ||
|
|
346
|
+
value?.attribute_option_id === undefined
|
|
347
|
+
? null
|
|
348
|
+
: Number(value.attribute_option_id),
|
|
349
|
+
value_text: String(value?.value_text ?? ''),
|
|
350
|
+
value_number:
|
|
351
|
+
value?.value_number === null || value?.value_number === undefined
|
|
352
|
+
? null
|
|
353
|
+
: Number(value.value_number),
|
|
354
|
+
value_boolean:
|
|
355
|
+
typeof value?.value_boolean === 'boolean' ? value.value_boolean : null,
|
|
356
|
+
value_unit: String(value?.value_unit ?? attribute.unit ?? ''),
|
|
357
|
+
source_type: String(value?.source_type ?? 'manual'),
|
|
358
|
+
confidence_score:
|
|
359
|
+
value?.confidence_score === null ||
|
|
360
|
+
value?.confidence_score === undefined
|
|
361
|
+
? null
|
|
362
|
+
: Number(value.confidence_score),
|
|
363
|
+
is_verified: Boolean(value?.is_verified ?? false),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return drafts;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function createEmptyProductAttributeDraft(
|
|
372
|
+
attributeId: number,
|
|
373
|
+
dataType = 'text',
|
|
374
|
+
unit = '',
|
|
375
|
+
): ProductAttributeDraft {
|
|
376
|
+
return {
|
|
377
|
+
attribute_id: attributeId,
|
|
378
|
+
data_type: dataType,
|
|
379
|
+
attribute_option_id: null,
|
|
380
|
+
value_text: '',
|
|
381
|
+
value_number: null,
|
|
382
|
+
value_boolean: null,
|
|
383
|
+
value_unit: unit,
|
|
384
|
+
source_type: 'manual',
|
|
385
|
+
confidence_score: null,
|
|
386
|
+
is_verified: false,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function normalizeProductAttributeDraft(
|
|
391
|
+
attribute: ProductAttributeMetadata,
|
|
392
|
+
draft?: ProductAttributeDraft,
|
|
393
|
+
) {
|
|
394
|
+
const dataType = String(attribute.data_type ?? draft?.data_type ?? 'text');
|
|
395
|
+
const normalized = {
|
|
396
|
+
...(draft ?? createEmptyProductAttributeDraft(Number(attribute.id), dataType, String(attribute.unit ?? ''))),
|
|
397
|
+
attribute_id: Number(attribute.id),
|
|
398
|
+
data_type: dataType,
|
|
399
|
+
value_unit: String(draft?.value_unit ?? attribute.unit ?? ''),
|
|
400
|
+
} satisfies ProductAttributeDraft;
|
|
401
|
+
|
|
402
|
+
if (dataType === 'option') {
|
|
403
|
+
return {
|
|
404
|
+
...normalized,
|
|
405
|
+
value_text: '',
|
|
406
|
+
value_number: null,
|
|
407
|
+
value_boolean: null,
|
|
408
|
+
value_unit: '',
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (dataType === 'number') {
|
|
413
|
+
return {
|
|
414
|
+
...normalized,
|
|
415
|
+
attribute_option_id: null,
|
|
416
|
+
value_text: '',
|
|
417
|
+
value_boolean: null,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (dataType === 'boolean') {
|
|
422
|
+
return {
|
|
423
|
+
...normalized,
|
|
424
|
+
attribute_option_id: null,
|
|
425
|
+
value_text: '',
|
|
426
|
+
value_number: null,
|
|
427
|
+
value_unit: '',
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
...normalized,
|
|
433
|
+
attribute_option_id: null,
|
|
434
|
+
value_number: null,
|
|
435
|
+
value_boolean: null,
|
|
436
|
+
value_unit: dataType === 'text' || dataType === 'long_text' ? '' : normalized.value_unit,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function hasMeaningfulProductAttributeValue(
|
|
441
|
+
attribute: ProductAttributeMetadata,
|
|
442
|
+
draft: ProductAttributeDraft,
|
|
443
|
+
) {
|
|
444
|
+
const dataType = String(attribute.data_type ?? draft.data_type ?? 'text');
|
|
445
|
+
|
|
446
|
+
if (dataType === 'option') {
|
|
447
|
+
return draft.attribute_option_id !== null && draft.attribute_option_id !== undefined;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (dataType === 'number') {
|
|
451
|
+
return draft.value_number !== null && draft.value_number !== undefined;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (dataType === 'boolean') {
|
|
455
|
+
return draft.value_boolean !== null && draft.value_boolean !== undefined;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return String(draft.value_text ?? '').trim().length > 0;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function buildProductAttributeSubmission(
|
|
462
|
+
payload: CatalogAttributePayload | null,
|
|
463
|
+
drafts: Record<number, ProductAttributeDraft>,
|
|
464
|
+
localeCode?: string | null,
|
|
465
|
+
) {
|
|
466
|
+
const errors: Record<number, string> = {};
|
|
467
|
+
const values: Array<Record<string, unknown>> = [];
|
|
468
|
+
const isPt = localeCode?.startsWith('pt');
|
|
469
|
+
|
|
470
|
+
for (const group of payload?.groups ?? []) {
|
|
471
|
+
for (const attribute of group.attributes as ProductAttributeMetadata[]) {
|
|
472
|
+
const attributeId = Number(attribute.id ?? 0);
|
|
473
|
+
|
|
474
|
+
if (!attributeId) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const normalized = normalizeProductAttributeDraft(attribute, drafts[attributeId]);
|
|
479
|
+
const hasValue = hasMeaningfulProductAttributeValue(attribute, normalized);
|
|
480
|
+
const relation = (attribute.category_attribute ?? {}) as CatalogRecord;
|
|
481
|
+
const optionIds = new Set(
|
|
482
|
+
(attribute.options ?? [])
|
|
483
|
+
.map((option) => Number(option.id ?? 0))
|
|
484
|
+
.filter((value) => value > 0),
|
|
485
|
+
);
|
|
486
|
+
const attributeLabel = String(
|
|
487
|
+
attribute.name ?? attribute.label ?? attribute.slug ?? `#${attributeId}`,
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
if (
|
|
491
|
+
normalized.attribute_option_id !== null &&
|
|
492
|
+
normalized.attribute_option_id !== undefined &&
|
|
493
|
+
!optionIds.has(Number(normalized.attribute_option_id))
|
|
494
|
+
) {
|
|
495
|
+
errors[attributeId] = isPt
|
|
496
|
+
? `Selecione uma opção válida para ${attributeLabel}.`
|
|
497
|
+
: `Select a valid option for ${attributeLabel}.`;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (relation.is_required === true && !hasValue) {
|
|
502
|
+
errors[attributeId] = isPt
|
|
503
|
+
? `${attributeLabel} é obrigatório para esta categoria.`
|
|
504
|
+
: `${attributeLabel} is required for this category.`;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (!hasValue) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
values.push({
|
|
513
|
+
attribute_id: attributeId,
|
|
514
|
+
attribute_option_id: normalized.attribute_option_id,
|
|
515
|
+
value_text: String(normalized.value_text ?? '').trim() || null,
|
|
516
|
+
value_number: normalized.value_number,
|
|
517
|
+
value_boolean: normalized.value_boolean,
|
|
518
|
+
value_unit: String(normalized.value_unit ?? '').trim() || null,
|
|
519
|
+
source_type: normalized.source_type,
|
|
520
|
+
confidence_score: normalized.confidence_score,
|
|
521
|
+
is_verified: normalized.is_verified,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
errors,
|
|
528
|
+
values,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function buildCurrentProductAttributeValues(
|
|
533
|
+
drafts: Record<number, ProductAttributeDraft>,
|
|
534
|
+
) {
|
|
535
|
+
return Object.values(drafts).map((draft) => ({
|
|
536
|
+
attribute_id: draft.attribute_id,
|
|
537
|
+
attribute_option_id: draft.attribute_option_id,
|
|
538
|
+
value_text: draft.value_text,
|
|
539
|
+
value_number: draft.value_number,
|
|
540
|
+
value_boolean: draft.value_boolean,
|
|
541
|
+
value_unit: draft.value_unit,
|
|
542
|
+
source_type: draft.source_type,
|
|
543
|
+
confidence_score: draft.confidence_score,
|
|
544
|
+
is_verified: draft.is_verified,
|
|
545
|
+
}));
|
|
546
|
+
}
|
|
547
|
+
|
|
274
548
|
function extractRecordId(record: CatalogRecord) {
|
|
275
549
|
return Number(record.id ?? record.category_id ?? record.content_id ?? 0);
|
|
276
550
|
}
|
|
@@ -661,6 +935,383 @@ function CatalogUploadField({
|
|
|
661
935
|
);
|
|
662
936
|
}
|
|
663
937
|
|
|
938
|
+
function ProductAttributesSection({
|
|
939
|
+
open,
|
|
940
|
+
recordId,
|
|
941
|
+
categoryId,
|
|
942
|
+
initialCategoryId,
|
|
943
|
+
values,
|
|
944
|
+
onChange,
|
|
945
|
+
errors,
|
|
946
|
+
onSchemaChange,
|
|
947
|
+
}: {
|
|
948
|
+
open: boolean;
|
|
949
|
+
recordId: number | null;
|
|
950
|
+
categoryId: number | null;
|
|
951
|
+
initialCategoryId: number | null;
|
|
952
|
+
values: Record<number, ProductAttributeDraft>;
|
|
953
|
+
onChange: (next: Record<number, ProductAttributeDraft>) => void;
|
|
954
|
+
errors: Record<number, string>;
|
|
955
|
+
onSchemaChange: (payload: CatalogAttributePayload | null) => void;
|
|
956
|
+
}) {
|
|
957
|
+
const { request, currentLocaleCode } = useApp();
|
|
958
|
+
const loadExistingValues =
|
|
959
|
+
Boolean(recordId) && categoryId !== null && categoryId === initialCategoryId;
|
|
960
|
+
|
|
961
|
+
const { data, isLoading } = useQuery<CatalogAttributePayload | null>({
|
|
962
|
+
queryKey: [
|
|
963
|
+
'catalog-product-attributes-editor',
|
|
964
|
+
recordId,
|
|
965
|
+
categoryId,
|
|
966
|
+
initialCategoryId,
|
|
967
|
+
],
|
|
968
|
+
queryFn: async () => {
|
|
969
|
+
if (!categoryId) {
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const response = await request({
|
|
974
|
+
url: loadExistingValues
|
|
975
|
+
? `/catalog/products/${recordId}/attributes`
|
|
976
|
+
: `/catalog/categories/${categoryId}/attributes`,
|
|
977
|
+
method: 'GET',
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
return (response.data ?? null) as CatalogAttributePayload | null;
|
|
981
|
+
},
|
|
982
|
+
enabled: open && Boolean(categoryId),
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
useEffect(() => {
|
|
986
|
+
onSchemaChange(data ?? null);
|
|
987
|
+
}, [data, onSchemaChange]);
|
|
988
|
+
|
|
989
|
+
useEffect(() => {
|
|
990
|
+
if (!categoryId) {
|
|
991
|
+
onSchemaChange(null);
|
|
992
|
+
onChange({});
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
onChange(buildAttributeDrafts(data));
|
|
997
|
+
}, [categoryId, data, onChange, onSchemaChange]);
|
|
998
|
+
|
|
999
|
+
const updateDraft = (attributeId: number, patch: Partial<ProductAttributeDraft>) => {
|
|
1000
|
+
const baseDraft = values[attributeId] ?? createEmptyProductAttributeDraft(attributeId);
|
|
1001
|
+
onChange({
|
|
1002
|
+
...values,
|
|
1003
|
+
[attributeId]: {
|
|
1004
|
+
...baseDraft,
|
|
1005
|
+
...patch,
|
|
1006
|
+
},
|
|
1007
|
+
});
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
if (!categoryId) {
|
|
1011
|
+
return (
|
|
1012
|
+
<section className="space-y-2 rounded-xl border border-dashed p-4">
|
|
1013
|
+
<h3 className="text-sm font-semibold">
|
|
1014
|
+
{currentLocaleCode?.startsWith('pt')
|
|
1015
|
+
? 'Atributos estruturados'
|
|
1016
|
+
: 'Structured attributes'}
|
|
1017
|
+
</h3>
|
|
1018
|
+
<p className="text-sm text-muted-foreground">
|
|
1019
|
+
{currentLocaleCode?.startsWith('pt')
|
|
1020
|
+
? 'Selecione uma categoria para carregar os atributos do produto.'
|
|
1021
|
+
: 'Select a category to load the product attributes.'}
|
|
1022
|
+
</p>
|
|
1023
|
+
</section>
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
return (
|
|
1028
|
+
<section className="space-y-4 rounded-xl border bg-muted/10 p-4">
|
|
1029
|
+
<div className="space-y-1">
|
|
1030
|
+
<h3 className="text-sm font-semibold">
|
|
1031
|
+
{currentLocaleCode?.startsWith('pt')
|
|
1032
|
+
? 'Atributos estruturados'
|
|
1033
|
+
: 'Structured attributes'}
|
|
1034
|
+
</h3>
|
|
1035
|
+
<p className="text-sm text-muted-foreground">
|
|
1036
|
+
{currentLocaleCode?.startsWith('pt')
|
|
1037
|
+
? 'A categoria define os campos comparáveis e filtráveis do produto.'
|
|
1038
|
+
: 'The category defines the product fields used for comparison and filters.'}
|
|
1039
|
+
</p>
|
|
1040
|
+
</div>
|
|
1041
|
+
|
|
1042
|
+
{isLoading ? (
|
|
1043
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
1044
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
1045
|
+
{currentLocaleCode?.startsWith('pt')
|
|
1046
|
+
? 'Carregando atributos...'
|
|
1047
|
+
: 'Loading attributes...'}
|
|
1048
|
+
</div>
|
|
1049
|
+
) : !(data?.groups?.length) ? (
|
|
1050
|
+
<p className="text-sm text-muted-foreground">
|
|
1051
|
+
{currentLocaleCode?.startsWith('pt')
|
|
1052
|
+
? 'Nenhum atributo configurado para esta categoria.'
|
|
1053
|
+
: 'No attributes configured for this category.'}
|
|
1054
|
+
</p>
|
|
1055
|
+
) : (
|
|
1056
|
+
data.groups.map((group) => (
|
|
1057
|
+
<div key={`${group.slug}-${group.id ?? 'general'}`} className="space-y-3">
|
|
1058
|
+
<div>
|
|
1059
|
+
<h4 className="text-sm font-medium">{group.name}</h4>
|
|
1060
|
+
</div>
|
|
1061
|
+
|
|
1062
|
+
<div className="grid gap-4">
|
|
1063
|
+
{group.attributes.map((attribute) => {
|
|
1064
|
+
const attributeId = Number(attribute.id);
|
|
1065
|
+
const draft = normalizeProductAttributeDraft(
|
|
1066
|
+
attribute as ProductAttributeMetadata,
|
|
1067
|
+
values[attributeId],
|
|
1068
|
+
);
|
|
1069
|
+
const dataType = String(attribute.data_type ?? 'text');
|
|
1070
|
+
const relation = (attribute.category_attribute ?? {}) as CatalogRecord;
|
|
1071
|
+
const attributeBadges = [
|
|
1072
|
+
relation.is_required
|
|
1073
|
+
? currentLocaleCode?.startsWith('pt')
|
|
1074
|
+
? 'Obrigatório'
|
|
1075
|
+
: 'Required'
|
|
1076
|
+
: null,
|
|
1077
|
+
relation.is_highlight
|
|
1078
|
+
? currentLocaleCode?.startsWith('pt')
|
|
1079
|
+
? 'Destaque'
|
|
1080
|
+
: 'Highlight'
|
|
1081
|
+
: null,
|
|
1082
|
+
relation.is_filter_visible
|
|
1083
|
+
? currentLocaleCode?.startsWith('pt')
|
|
1084
|
+
? 'Filtro'
|
|
1085
|
+
: 'Filter'
|
|
1086
|
+
: null,
|
|
1087
|
+
relation.is_comparison_visible
|
|
1088
|
+
? currentLocaleCode?.startsWith('pt')
|
|
1089
|
+
? 'Comparação'
|
|
1090
|
+
: 'Comparison'
|
|
1091
|
+
: null,
|
|
1092
|
+
].filter((item): item is string => Boolean(item));
|
|
1093
|
+
|
|
1094
|
+
return (
|
|
1095
|
+
<div key={attributeId} className="rounded-lg border bg-background p-3">
|
|
1096
|
+
<div className="mb-3 space-y-1">
|
|
1097
|
+
<div className="flex items-center justify-between gap-3">
|
|
1098
|
+
<div className="space-y-1">
|
|
1099
|
+
<div className="font-medium">
|
|
1100
|
+
{String(attribute.name ?? attribute.label ?? attribute.slug)}
|
|
1101
|
+
{relation.is_required ? (
|
|
1102
|
+
<span className="ml-1 text-destructive">*</span>
|
|
1103
|
+
) : null}
|
|
1104
|
+
</div>
|
|
1105
|
+
{attributeBadges.length ? (
|
|
1106
|
+
<div className="flex flex-wrap gap-2">
|
|
1107
|
+
{attributeBadges.map((badge) => (
|
|
1108
|
+
<Badge key={`${attributeId}-${badge}`} variant="secondary">
|
|
1109
|
+
{badge}
|
|
1110
|
+
</Badge>
|
|
1111
|
+
))}
|
|
1112
|
+
</div>
|
|
1113
|
+
) : null}
|
|
1114
|
+
</div>
|
|
1115
|
+
{attribute.unit ? (
|
|
1116
|
+
<span className="text-xs text-muted-foreground">
|
|
1117
|
+
{String(attribute.unit)}
|
|
1118
|
+
</span>
|
|
1119
|
+
) : null}
|
|
1120
|
+
</div>
|
|
1121
|
+
{attribute.description ? (
|
|
1122
|
+
<p className="text-xs text-muted-foreground">
|
|
1123
|
+
{String(attribute.description)}
|
|
1124
|
+
</p>
|
|
1125
|
+
) : null}
|
|
1126
|
+
{errors[attributeId] ? (
|
|
1127
|
+
<p className="text-xs font-medium text-destructive">
|
|
1128
|
+
{errors[attributeId]}
|
|
1129
|
+
</p>
|
|
1130
|
+
) : null}
|
|
1131
|
+
</div>
|
|
1132
|
+
|
|
1133
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
1134
|
+
{dataType === 'option' ? (
|
|
1135
|
+
<div className="space-y-2 sm:col-span-2">
|
|
1136
|
+
<FormLabel>
|
|
1137
|
+
{currentLocaleCode?.startsWith('pt') ? 'Opção' : 'Option'}
|
|
1138
|
+
</FormLabel>
|
|
1139
|
+
<Select
|
|
1140
|
+
value={
|
|
1141
|
+
draft?.attribute_option_id
|
|
1142
|
+
? String(draft.attribute_option_id)
|
|
1143
|
+
: ''
|
|
1144
|
+
}
|
|
1145
|
+
onValueChange={(next) =>
|
|
1146
|
+
updateDraft(attributeId, {
|
|
1147
|
+
data_type: dataType,
|
|
1148
|
+
attribute_option_id: next ? Number(next) : null,
|
|
1149
|
+
})
|
|
1150
|
+
}
|
|
1151
|
+
>
|
|
1152
|
+
<SelectTrigger>
|
|
1153
|
+
<SelectValue
|
|
1154
|
+
placeholder={
|
|
1155
|
+
currentLocaleCode?.startsWith('pt')
|
|
1156
|
+
? 'Selecione uma opção'
|
|
1157
|
+
: 'Select an option'
|
|
1158
|
+
}
|
|
1159
|
+
/>
|
|
1160
|
+
</SelectTrigger>
|
|
1161
|
+
<SelectContent>
|
|
1162
|
+
{(attribute.options ?? []).map((option) => (
|
|
1163
|
+
<SelectItem key={String(option.id)} value={String(option.id)}>
|
|
1164
|
+
{String(option.label ?? option.option_value)}
|
|
1165
|
+
</SelectItem>
|
|
1166
|
+
))}
|
|
1167
|
+
</SelectContent>
|
|
1168
|
+
</Select>
|
|
1169
|
+
</div>
|
|
1170
|
+
) : null}
|
|
1171
|
+
|
|
1172
|
+
{dataType === 'text' || dataType === 'long_text' ? (
|
|
1173
|
+
<div className="space-y-2 sm:col-span-2">
|
|
1174
|
+
<FormLabel>{currentLocaleCode?.startsWith('pt') ? 'Valor' : 'Value'}</FormLabel>
|
|
1175
|
+
{dataType === 'long_text' ? (
|
|
1176
|
+
<Textarea
|
|
1177
|
+
value={draft?.value_text ?? ''}
|
|
1178
|
+
onChange={(event) =>
|
|
1179
|
+
updateDraft(attributeId, {
|
|
1180
|
+
data_type: dataType,
|
|
1181
|
+
value_text: event.target.value,
|
|
1182
|
+
})
|
|
1183
|
+
}
|
|
1184
|
+
/>
|
|
1185
|
+
) : (
|
|
1186
|
+
<Input
|
|
1187
|
+
value={draft?.value_text ?? ''}
|
|
1188
|
+
onChange={(event) =>
|
|
1189
|
+
updateDraft(attributeId, {
|
|
1190
|
+
data_type: dataType,
|
|
1191
|
+
value_text: event.target.value,
|
|
1192
|
+
})
|
|
1193
|
+
}
|
|
1194
|
+
/>
|
|
1195
|
+
)}
|
|
1196
|
+
</div>
|
|
1197
|
+
) : null}
|
|
1198
|
+
|
|
1199
|
+
{dataType === 'number' ? (
|
|
1200
|
+
<>
|
|
1201
|
+
<div className="space-y-2">
|
|
1202
|
+
<FormLabel>{currentLocaleCode?.startsWith('pt') ? 'Número' : 'Number'}</FormLabel>
|
|
1203
|
+
<Input
|
|
1204
|
+
type="number"
|
|
1205
|
+
value={
|
|
1206
|
+
draft?.value_number === null ||
|
|
1207
|
+
draft?.value_number === undefined
|
|
1208
|
+
? ''
|
|
1209
|
+
: String(draft.value_number)
|
|
1210
|
+
}
|
|
1211
|
+
onChange={(event) =>
|
|
1212
|
+
updateDraft(attributeId, {
|
|
1213
|
+
data_type: dataType,
|
|
1214
|
+
value_number:
|
|
1215
|
+
event.target.value === ''
|
|
1216
|
+
? null
|
|
1217
|
+
: Number(event.target.value),
|
|
1218
|
+
})
|
|
1219
|
+
}
|
|
1220
|
+
/>
|
|
1221
|
+
</div>
|
|
1222
|
+
<div className="space-y-2">
|
|
1223
|
+
<FormLabel>{currentLocaleCode?.startsWith('pt') ? 'Unidade' : 'Unit'}</FormLabel>
|
|
1224
|
+
<Input
|
|
1225
|
+
value={draft?.value_unit ?? ''}
|
|
1226
|
+
onChange={(event) =>
|
|
1227
|
+
updateDraft(attributeId, {
|
|
1228
|
+
data_type: dataType,
|
|
1229
|
+
value_unit: event.target.value,
|
|
1230
|
+
})
|
|
1231
|
+
}
|
|
1232
|
+
/>
|
|
1233
|
+
</div>
|
|
1234
|
+
</>
|
|
1235
|
+
) : null}
|
|
1236
|
+
|
|
1237
|
+
{dataType === 'boolean' ? (
|
|
1238
|
+
<div className="space-y-2 sm:col-span-2">
|
|
1239
|
+
<FormLabel>{currentLocaleCode?.startsWith('pt') ? 'Valor' : 'Value'}</FormLabel>
|
|
1240
|
+
<Select
|
|
1241
|
+
value={
|
|
1242
|
+
draft?.value_boolean === null ||
|
|
1243
|
+
draft?.value_boolean === undefined
|
|
1244
|
+
? ''
|
|
1245
|
+
: draft.value_boolean
|
|
1246
|
+
? 'true'
|
|
1247
|
+
: 'false'
|
|
1248
|
+
}
|
|
1249
|
+
onValueChange={(next) =>
|
|
1250
|
+
updateDraft(attributeId, {
|
|
1251
|
+
data_type: dataType,
|
|
1252
|
+
value_boolean:
|
|
1253
|
+
next === ''
|
|
1254
|
+
? null
|
|
1255
|
+
: next === 'true',
|
|
1256
|
+
})
|
|
1257
|
+
}
|
|
1258
|
+
>
|
|
1259
|
+
<SelectTrigger>
|
|
1260
|
+
<SelectValue
|
|
1261
|
+
placeholder={
|
|
1262
|
+
currentLocaleCode?.startsWith('pt')
|
|
1263
|
+
? 'Selecione um valor'
|
|
1264
|
+
: 'Select a value'
|
|
1265
|
+
}
|
|
1266
|
+
/>
|
|
1267
|
+
</SelectTrigger>
|
|
1268
|
+
<SelectContent>
|
|
1269
|
+
<SelectItem value="true">
|
|
1270
|
+
{currentLocaleCode?.startsWith('pt') ? 'Sim' : 'Yes'}
|
|
1271
|
+
</SelectItem>
|
|
1272
|
+
<SelectItem value="false">
|
|
1273
|
+
{currentLocaleCode?.startsWith('pt') ? 'Não' : 'No'}
|
|
1274
|
+
</SelectItem>
|
|
1275
|
+
</SelectContent>
|
|
1276
|
+
</Select>
|
|
1277
|
+
</div>
|
|
1278
|
+
) : null}
|
|
1279
|
+
|
|
1280
|
+
<div className="flex items-center justify-between rounded-md border px-3 py-2 sm:col-span-2">
|
|
1281
|
+
<div>
|
|
1282
|
+
<div className="text-sm font-medium">
|
|
1283
|
+
{currentLocaleCode?.startsWith('pt')
|
|
1284
|
+
? 'Valor verificado'
|
|
1285
|
+
: 'Verified value'}
|
|
1286
|
+
</div>
|
|
1287
|
+
<div className="text-xs text-muted-foreground">
|
|
1288
|
+
{currentLocaleCode?.startsWith('pt')
|
|
1289
|
+
? 'Marque quando o dado já tiver sido validado.'
|
|
1290
|
+
: 'Enable when the value has been validated.'}
|
|
1291
|
+
</div>
|
|
1292
|
+
</div>
|
|
1293
|
+
<Switch
|
|
1294
|
+
checked={Boolean(draft?.is_verified)}
|
|
1295
|
+
onCheckedChange={(checked) =>
|
|
1296
|
+
updateDraft(attributeId, {
|
|
1297
|
+
data_type: dataType,
|
|
1298
|
+
is_verified: checked,
|
|
1299
|
+
})
|
|
1300
|
+
}
|
|
1301
|
+
/>
|
|
1302
|
+
</div>
|
|
1303
|
+
</div>
|
|
1304
|
+
</div>
|
|
1305
|
+
);
|
|
1306
|
+
})}
|
|
1307
|
+
</div>
|
|
1308
|
+
</div>
|
|
1309
|
+
))
|
|
1310
|
+
)}
|
|
1311
|
+
</section>
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
664
1315
|
export function CatalogResourceFormSheet({
|
|
665
1316
|
open,
|
|
666
1317
|
onOpenChange,
|
|
@@ -674,6 +1325,14 @@ export function CatalogResourceFormSheet({
|
|
|
674
1325
|
const { request, currentLocaleCode } = useApp();
|
|
675
1326
|
const t = useTranslations('catalog');
|
|
676
1327
|
const isEditing = Boolean(recordId);
|
|
1328
|
+
const [productAttributeValues, setProductAttributeValues] = useState<
|
|
1329
|
+
Record<number, ProductAttributeDraft>
|
|
1330
|
+
>({});
|
|
1331
|
+
const [productAttributeSchema, setProductAttributeSchema] =
|
|
1332
|
+
useState<CatalogAttributePayload | null>(null);
|
|
1333
|
+
const [productAttributeErrors, setProductAttributeErrors] = useState<
|
|
1334
|
+
Record<number, string>
|
|
1335
|
+
>({});
|
|
677
1336
|
const formSchema = useMemo(
|
|
678
1337
|
() => buildFormSchema(resourceConfig),
|
|
679
1338
|
[resourceConfig]
|
|
@@ -696,9 +1355,20 @@ export function CatalogResourceFormSheet({
|
|
|
696
1355
|
},
|
|
697
1356
|
enabled: open && Boolean(recordId),
|
|
698
1357
|
});
|
|
1358
|
+
const selectedCategoryId =
|
|
1359
|
+
resource === 'products'
|
|
1360
|
+
? Number(form.watch('catalog_category_id') ?? 0) || null
|
|
1361
|
+
: null;
|
|
1362
|
+
const initialCategoryId =
|
|
1363
|
+
resource === 'products'
|
|
1364
|
+
? Number(currentRecord?.catalog_category_id ?? 0) || null
|
|
1365
|
+
: null;
|
|
699
1366
|
|
|
700
1367
|
useEffect(() => {
|
|
701
1368
|
if (!open) {
|
|
1369
|
+
setProductAttributeValues({});
|
|
1370
|
+
setProductAttributeSchema(null);
|
|
1371
|
+
setProductAttributeErrors({});
|
|
702
1372
|
return;
|
|
703
1373
|
}
|
|
704
1374
|
|
|
@@ -706,6 +1376,10 @@ export function CatalogResourceFormSheet({
|
|
|
706
1376
|
form.reset(buildDefaultValues(resourceConfig, payload));
|
|
707
1377
|
}, [currentRecord, form, open, resourceConfig]);
|
|
708
1378
|
|
|
1379
|
+
useEffect(() => {
|
|
1380
|
+
setProductAttributeErrors({});
|
|
1381
|
+
}, [selectedCategoryId]);
|
|
1382
|
+
|
|
709
1383
|
const submitLabel = isEditing
|
|
710
1384
|
? getCatalogLocalizedText(resourceConfig.editActionLabel, currentLocaleCode)
|
|
711
1385
|
: getCatalogLocalizedText(
|
|
@@ -723,6 +1397,35 @@ export function CatalogResourceFormSheet({
|
|
|
723
1397
|
}
|
|
724
1398
|
}
|
|
725
1399
|
|
|
1400
|
+
let attributeSubmission:
|
|
1401
|
+
| ReturnType<typeof buildProductAttributeSubmission>
|
|
1402
|
+
| null = null;
|
|
1403
|
+
|
|
1404
|
+
if (resource === 'products') {
|
|
1405
|
+
if (selectedCategoryId && !productAttributeSchema) {
|
|
1406
|
+
toast.error(
|
|
1407
|
+
currentLocaleCode?.startsWith('pt')
|
|
1408
|
+
? 'Aguarde o carregamento dos atributos da categoria antes de salvar.'
|
|
1409
|
+
: 'Wait for the category attributes to load before saving.',
|
|
1410
|
+
);
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
attributeSubmission = buildProductAttributeSubmission(
|
|
1415
|
+
productAttributeSchema,
|
|
1416
|
+
productAttributeValues,
|
|
1417
|
+
currentLocaleCode,
|
|
1418
|
+
);
|
|
1419
|
+
|
|
1420
|
+
if (Object.keys(attributeSubmission.errors).length > 0) {
|
|
1421
|
+
setProductAttributeErrors(attributeSubmission.errors);
|
|
1422
|
+
toast.error(
|
|
1423
|
+
Object.values(attributeSubmission.errors)[0] ?? t('toasts.saveError'),
|
|
1424
|
+
);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
726
1429
|
const response = await request({
|
|
727
1430
|
url: isEditing
|
|
728
1431
|
? `/catalog/${resource}/${recordId}`
|
|
@@ -732,6 +1435,23 @@ export function CatalogResourceFormSheet({
|
|
|
732
1435
|
});
|
|
733
1436
|
|
|
734
1437
|
const savedRecord = (response.data || payload) as CatalogRecord;
|
|
1438
|
+
|
|
1439
|
+
if (resource === 'products') {
|
|
1440
|
+
const savedId = extractRecordId(savedRecord);
|
|
1441
|
+
|
|
1442
|
+
if (savedId) {
|
|
1443
|
+
await request({
|
|
1444
|
+
url: `/catalog/products/${savedId}/attributes`,
|
|
1445
|
+
method: 'PUT',
|
|
1446
|
+
data: {
|
|
1447
|
+
values: attributeSubmission?.values ?? [],
|
|
1448
|
+
},
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
setProductAttributeErrors({});
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
735
1455
|
toast.success(t('toasts.saveSuccess', { resource: resourceTitle }));
|
|
736
1456
|
onOpenChange(false);
|
|
737
1457
|
await onSuccess(savedRecord);
|
|
@@ -742,6 +1462,73 @@ export function CatalogResourceFormSheet({
|
|
|
742
1462
|
}
|
|
743
1463
|
};
|
|
744
1464
|
|
|
1465
|
+
const handleApplyAiSuggestion = (suggestion: {
|
|
1466
|
+
fields?: Record<string, unknown>;
|
|
1467
|
+
product_attributes?: Array<Record<string, unknown>>;
|
|
1468
|
+
}) => {
|
|
1469
|
+
const fieldMap = new Map(
|
|
1470
|
+
resourceConfig.formSections.flatMap((section) =>
|
|
1471
|
+
section.fields.map((field) => [field.key, field] as const),
|
|
1472
|
+
),
|
|
1473
|
+
);
|
|
1474
|
+
|
|
1475
|
+
for (const [key, value] of Object.entries(suggestion.fields ?? {})) {
|
|
1476
|
+
const field = fieldMap.get(key);
|
|
1477
|
+
|
|
1478
|
+
if (!field) {
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
form.setValue(
|
|
1483
|
+
key,
|
|
1484
|
+
serializeInitialValue(field, value) as never,
|
|
1485
|
+
{ shouldDirty: true, shouldTouch: true },
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
if (resource === 'products' && suggestion.product_attributes?.length) {
|
|
1490
|
+
const nextDrafts = { ...productAttributeValues };
|
|
1491
|
+
|
|
1492
|
+
for (const attribute of suggestion.product_attributes) {
|
|
1493
|
+
const attributeId = Number(attribute.attribute_id ?? 0);
|
|
1494
|
+
|
|
1495
|
+
if (!attributeId) {
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
nextDrafts[attributeId] = {
|
|
1500
|
+
attribute_id: attributeId,
|
|
1501
|
+
data_type: String(attribute.data_type ?? nextDrafts[attributeId]?.data_type ?? 'text'),
|
|
1502
|
+
attribute_option_id:
|
|
1503
|
+
attribute.attribute_option_id === null ||
|
|
1504
|
+
attribute.attribute_option_id === undefined
|
|
1505
|
+
? null
|
|
1506
|
+
: Number(attribute.attribute_option_id),
|
|
1507
|
+
value_text: String(attribute.value_text ?? ''),
|
|
1508
|
+
value_number:
|
|
1509
|
+
attribute.value_number === null || attribute.value_number === undefined
|
|
1510
|
+
? null
|
|
1511
|
+
: Number(attribute.value_number),
|
|
1512
|
+
value_boolean:
|
|
1513
|
+
typeof attribute.value_boolean === 'boolean'
|
|
1514
|
+
? attribute.value_boolean
|
|
1515
|
+
: null,
|
|
1516
|
+
value_unit: String(attribute.value_unit ?? ''),
|
|
1517
|
+
source_type: String(attribute.source_type ?? 'manual'),
|
|
1518
|
+
confidence_score:
|
|
1519
|
+
attribute.confidence_score === null ||
|
|
1520
|
+
attribute.confidence_score === undefined
|
|
1521
|
+
? null
|
|
1522
|
+
: Number(attribute.confidence_score),
|
|
1523
|
+
is_verified: Boolean(attribute.is_verified ?? false),
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
setProductAttributeValues(nextDrafts);
|
|
1528
|
+
setProductAttributeErrors({});
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
|
|
745
1532
|
return (
|
|
746
1533
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
747
1534
|
<SheetContent className="w-full overflow-x-hidden overflow-y-auto sm:max-w-2xl">
|
|
@@ -762,6 +1549,21 @@ export function CatalogResourceFormSheet({
|
|
|
762
1549
|
className="min-w-0 space-y-6 px-4 pb-4"
|
|
763
1550
|
onSubmit={form.handleSubmit(handleSubmit)}
|
|
764
1551
|
>
|
|
1552
|
+
<div className="flex justify-end">
|
|
1553
|
+
<CatalogAiFormAssistDialog
|
|
1554
|
+
resource={resource}
|
|
1555
|
+
resourceConfig={resourceConfig}
|
|
1556
|
+
disabled={form.formState.isSubmitting || isLoading}
|
|
1557
|
+
getCurrentValues={() =>
|
|
1558
|
+
buildCurrentFormPayload(resourceConfig, form.getValues())
|
|
1559
|
+
}
|
|
1560
|
+
getCurrentAttributeValues={() =>
|
|
1561
|
+
buildCurrentProductAttributeValues(productAttributeValues)
|
|
1562
|
+
}
|
|
1563
|
+
onApply={handleApplyAiSuggestion}
|
|
1564
|
+
/>
|
|
1565
|
+
</div>
|
|
1566
|
+
|
|
765
1567
|
{resourceConfig.formSections.map((section) => (
|
|
766
1568
|
<section
|
|
767
1569
|
key={section.title.en}
|
|
@@ -946,6 +1748,19 @@ export function CatalogResourceFormSheet({
|
|
|
946
1748
|
</div>
|
|
947
1749
|
</section>
|
|
948
1750
|
))}
|
|
1751
|
+
|
|
1752
|
+
{resource === 'products' ? (
|
|
1753
|
+
<ProductAttributesSection
|
|
1754
|
+
open={open}
|
|
1755
|
+
recordId={recordId}
|
|
1756
|
+
categoryId={selectedCategoryId}
|
|
1757
|
+
initialCategoryId={initialCategoryId}
|
|
1758
|
+
values={productAttributeValues}
|
|
1759
|
+
onChange={setProductAttributeValues}
|
|
1760
|
+
errors={productAttributeErrors}
|
|
1761
|
+
onSchemaChange={setProductAttributeSchema}
|
|
1762
|
+
/>
|
|
1763
|
+
) : null}
|
|
949
1764
|
</form>
|
|
950
1765
|
</Form>
|
|
951
1766
|
)}
|