@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.
- package/README.md +409 -379
- 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 +1111 -5
- 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 +133 -99
- package/hedhog/data/route.yaml +72 -8
- package/hedhog/frontend/app/[resource]/page.tsx.ejs +391 -33
- 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 +907 -92
- package/hedhog/frontend/app/_lib/catalog-resources.tsx.ejs +929 -1161
- package/hedhog/frontend/messages/en.json +389 -299
- package/hedhog/frontend/messages/pt.json +389 -299
- package/hedhog/table/catalog_attribute.yaml +67 -52
- package/hedhog/table/catalog_attribute_option.yaml +40 -0
- package/hedhog/table/catalog_category.yaml +40 -0
- package/hedhog/table/catalog_category_attribute.yaml +37 -31
- package/hedhog/table/catalog_comparison.yaml +19 -22
- package/hedhog/table/catalog_product.yaml +30 -28
- package/hedhog/table/catalog_product_attribute_value.yaml +44 -31
- package/hedhog/table/catalog_product_category.yaml +13 -13
- package/hedhog/table/catalog_score_criterion.yaml +42 -25
- package/hedhog/table/catalog_seo_page_rule.yaml +10 -10
- package/hedhog/table/catalog_similarity_rule.yaml +33 -20
- package/hedhog/table/catalog_site.yaml +21 -13
- package/hedhog/table/catalog_site_category.yaml +12 -12
- package/package.json +6 -6
- package/src/catalog-resource.config.ts +132 -105
- package/src/catalog.controller.ts +91 -24
- package/src/catalog.module.ts +16 -12
- package/src/catalog.service.ts +1569 -56
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
4
|
-
import {
|
|
3
|
+
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
4
|
+
import { Badge } from '@/components/ui/badge';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
5
6
|
import {
|
|
6
7
|
Command,
|
|
7
8
|
CommandEmpty,
|
|
@@ -51,19 +52,54 @@ import Image from 'next/image';
|
|
|
51
52
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
52
53
|
import { useForm } from 'react-hook-form';
|
|
53
54
|
import { toast } from 'sonner';
|
|
54
|
-
import { z } from 'zod';
|
|
55
|
-
import {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
import { z } from 'zod';
|
|
56
|
+
import { CatalogAiFormAssistDialog } from './catalog-ai-form-assist-dialog';
|
|
57
|
+
import {
|
|
58
|
+
catalogResourceMap,
|
|
59
|
+
getCatalogLocalizedText,
|
|
60
|
+
getCatalogRecordLabel,
|
|
59
61
|
type CatalogFormFieldDefinition,
|
|
60
62
|
type CatalogResourceDefinition,
|
|
61
63
|
} from '../_lib/catalog-resources';
|
|
62
64
|
|
|
63
|
-
type CatalogRecord = Record<string, unknown>;
|
|
64
|
-
type CatalogFormValues = Record<string, unknown>;
|
|
65
|
-
type RelationOption = { id: number; label: string };
|
|
66
|
-
type
|
|
65
|
+
type CatalogRecord = Record<string, unknown>;
|
|
66
|
+
type CatalogFormValues = Record<string, unknown>;
|
|
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
|
+
};
|
|
102
|
+
type FormSheetProps = {
|
|
67
103
|
open: boolean;
|
|
68
104
|
onOpenChange: (open: boolean) => void;
|
|
69
105
|
resource: string;
|
|
@@ -210,10 +246,10 @@ function serializeInitialValue(
|
|
|
210
246
|
return rawValue === null || rawValue === undefined ? '' : String(rawValue);
|
|
211
247
|
}
|
|
212
248
|
|
|
213
|
-
function buildDefaultValues(
|
|
214
|
-
resourceConfig: CatalogResourceDefinition,
|
|
215
|
-
payload: CatalogRecord
|
|
216
|
-
) {
|
|
249
|
+
function buildDefaultValues(
|
|
250
|
+
resourceConfig: CatalogResourceDefinition,
|
|
251
|
+
payload: CatalogRecord
|
|
252
|
+
) {
|
|
217
253
|
const defaults: CatalogFormValues = {};
|
|
218
254
|
|
|
219
255
|
for (const section of resourceConfig.formSections) {
|
|
@@ -222,8 +258,27 @@ function buildDefaultValues(
|
|
|
222
258
|
}
|
|
223
259
|
}
|
|
224
260
|
|
|
225
|
-
return defaults;
|
|
226
|
-
}
|
|
261
|
+
return defaults;
|
|
262
|
+
}
|
|
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
|
+
}
|
|
227
282
|
|
|
228
283
|
function normalizeSubmitValue(
|
|
229
284
|
field: CatalogFormFieldDefinition,
|
|
@@ -268,8 +323,227 @@ function normalizeSubmitValue(
|
|
|
268
323
|
return String(rawValue || '').trim() || null;
|
|
269
324
|
}
|
|
270
325
|
|
|
271
|
-
return rawValue;
|
|
272
|
-
}
|
|
326
|
+
return rawValue;
|
|
327
|
+
}
|
|
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
|
+
}
|
|
273
547
|
|
|
274
548
|
function extractRecordId(record: CatalogRecord) {
|
|
275
549
|
return Number(record.id ?? record.category_id ?? record.content_id ?? 0);
|
|
@@ -505,7 +779,7 @@ function CatalogRelationField({
|
|
|
505
779
|
);
|
|
506
780
|
}
|
|
507
781
|
|
|
508
|
-
function CatalogUploadField({
|
|
782
|
+
function CatalogUploadField({
|
|
509
783
|
field,
|
|
510
784
|
value,
|
|
511
785
|
onChange,
|
|
@@ -658,10 +932,387 @@ function CatalogUploadField({
|
|
|
658
932
|
|
|
659
933
|
{isUploading ? <Progress value={progress} /> : null}
|
|
660
934
|
</div>
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
|
|
935
|
+
);
|
|
936
|
+
}
|
|
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
|
+
|
|
1315
|
+
export function CatalogResourceFormSheet({
|
|
665
1316
|
open,
|
|
666
1317
|
onOpenChange,
|
|
667
1318
|
resource,
|
|
@@ -671,20 +1322,28 @@ export function CatalogResourceFormSheet({
|
|
|
671
1322
|
recordId,
|
|
672
1323
|
onSuccess,
|
|
673
1324
|
}: FormSheetProps) {
|
|
674
|
-
const { request, currentLocaleCode } = useApp();
|
|
675
|
-
const t = useTranslations('catalog');
|
|
676
|
-
const isEditing = Boolean(recordId);
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
1325
|
+
const { request, currentLocaleCode } = useApp();
|
|
1326
|
+
const t = useTranslations('catalog');
|
|
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
|
+
>({});
|
|
1336
|
+
const formSchema = useMemo(
|
|
1337
|
+
() => buildFormSchema(resourceConfig),
|
|
1338
|
+
[resourceConfig]
|
|
1339
|
+
);
|
|
681
1340
|
|
|
682
1341
|
const form = useForm<CatalogFormValues>({
|
|
683
1342
|
resolver: zodResolver(formSchema),
|
|
684
1343
|
defaultValues: buildDefaultValues(resourceConfig, resourceConfig.template),
|
|
685
1344
|
});
|
|
686
1345
|
|
|
687
|
-
const { data: currentRecord, isLoading } = useQuery<CatalogRecord>({
|
|
1346
|
+
const { data: currentRecord, isLoading } = useQuery<CatalogRecord>({
|
|
688
1347
|
queryKey: ['catalog-record-details', resource, recordId],
|
|
689
1348
|
queryFn: async () => {
|
|
690
1349
|
const response = await request({
|
|
@@ -693,57 +1352,185 @@ export function CatalogResourceFormSheet({
|
|
|
693
1352
|
});
|
|
694
1353
|
|
|
695
1354
|
return response.data as CatalogRecord;
|
|
696
|
-
},
|
|
697
|
-
enabled: open && Boolean(recordId),
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
1355
|
+
},
|
|
1356
|
+
enabled: open && Boolean(recordId),
|
|
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;
|
|
1366
|
+
|
|
1367
|
+
useEffect(() => {
|
|
1368
|
+
if (!open) {
|
|
1369
|
+
setProductAttributeValues({});
|
|
1370
|
+
setProductAttributeSchema(null);
|
|
1371
|
+
setProductAttributeErrors({});
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const payload = currentRecord || resourceConfig.template;
|
|
1376
|
+
form.reset(buildDefaultValues(resourceConfig, payload));
|
|
1377
|
+
}, [currentRecord, form, open, resourceConfig]);
|
|
1378
|
+
|
|
1379
|
+
useEffect(() => {
|
|
1380
|
+
setProductAttributeErrors({});
|
|
1381
|
+
}, [selectedCategoryId]);
|
|
1382
|
+
|
|
1383
|
+
const submitLabel = isEditing
|
|
1384
|
+
? getCatalogLocalizedText(resourceConfig.editActionLabel, currentLocaleCode)
|
|
1385
|
+
: getCatalogLocalizedText(
|
|
1386
|
+
resourceConfig.createActionLabel,
|
|
1387
|
+
currentLocaleCode
|
|
1388
|
+
);
|
|
1389
|
+
|
|
1390
|
+
const handleSubmit = async (values: CatalogFormValues) => {
|
|
1391
|
+
try {
|
|
1392
|
+
const payload: CatalogRecord = {};
|
|
1393
|
+
|
|
1394
|
+
for (const section of resourceConfig.formSections) {
|
|
1395
|
+
for (const field of section.fields) {
|
|
1396
|
+
payload[field.key] = normalizeSubmitValue(field, values[field.key]);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
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
|
+
|
|
1429
|
+
const response = await request({
|
|
1430
|
+
url: isEditing
|
|
1431
|
+
? `/catalog/${resource}/${recordId}`
|
|
1432
|
+
: `/catalog/${resource}`,
|
|
1433
|
+
method: isEditing ? 'PATCH' : 'POST',
|
|
1434
|
+
data: payload,
|
|
1435
|
+
});
|
|
1436
|
+
|
|
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
|
+
|
|
1455
|
+
toast.success(t('toasts.saveSuccess', { resource: resourceTitle }));
|
|
1456
|
+
onOpenChange(false);
|
|
1457
|
+
await onSuccess(savedRecord);
|
|
738
1458
|
} catch (error) {
|
|
739
1459
|
toast.error(
|
|
740
1460
|
error instanceof Error ? error.message : t('toasts.saveError')
|
|
741
1461
|
);
|
|
742
1462
|
}
|
|
743
|
-
};
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
<
|
|
1463
|
+
};
|
|
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
|
+
|
|
1532
|
+
return (
|
|
1533
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
747
1534
|
<SheetContent className="w-full overflow-x-hidden overflow-y-auto sm:max-w-2xl">
|
|
748
1535
|
<SheetHeader>
|
|
749
1536
|
<SheetTitle>{submitLabel}</SheetTitle>
|
|
@@ -756,15 +1543,30 @@ export function CatalogResourceFormSheet({
|
|
|
756
1543
|
Carregando dados do registro...
|
|
757
1544
|
</div>
|
|
758
1545
|
) : (
|
|
759
|
-
<Form {...form}>
|
|
760
|
-
<form
|
|
761
|
-
id={`catalog-form-${resource}`}
|
|
762
|
-
className="min-w-0 space-y-6 px-4 pb-4"
|
|
763
|
-
onSubmit={form.handleSubmit(handleSubmit)}
|
|
764
|
-
>
|
|
765
|
-
|
|
766
|
-
<
|
|
767
|
-
|
|
1546
|
+
<Form {...form}>
|
|
1547
|
+
<form
|
|
1548
|
+
id={`catalog-form-${resource}`}
|
|
1549
|
+
className="min-w-0 space-y-6 px-4 pb-4"
|
|
1550
|
+
onSubmit={form.handleSubmit(handleSubmit)}
|
|
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
|
+
|
|
1567
|
+
{resourceConfig.formSections.map((section) => (
|
|
1568
|
+
<section
|
|
1569
|
+
key={section.title.en}
|
|
768
1570
|
className="min-w-0 max-w-full space-y-4"
|
|
769
1571
|
>
|
|
770
1572
|
<div>
|
|
@@ -944,11 +1746,24 @@ export function CatalogResourceFormSheet({
|
|
|
944
1746
|
</div>
|
|
945
1747
|
))}
|
|
946
1748
|
</div>
|
|
947
|
-
</section>
|
|
948
|
-
))}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1749
|
+
</section>
|
|
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}
|
|
1764
|
+
</form>
|
|
1765
|
+
</Form>
|
|
1766
|
+
)}
|
|
952
1767
|
|
|
953
1768
|
<SheetFooter className="border-t">
|
|
954
1769
|
<div className="w-full">
|