@hed-hog/catalog 0.0.294 → 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 +409 -409
- package/dist/catalog.service.d.ts.map +1 -1
- package/dist/catalog.service.js +14 -6
- package/dist/catalog.service.js.map +1 -1
- package/hedhog/data/catalog_attribute.yaml +202 -202
- package/hedhog/data/catalog_attribute_option.yaml +109 -109
- package/hedhog/data/catalog_category.yaml +47 -47
- package/hedhog/data/catalog_category_attribute.yaml +209 -209
- package/hedhog/data/menu.yaml +133 -133
- package/hedhog/data/role.yaml +7 -7
- package/hedhog/data/route.yaml +72 -72
- package/hedhog/frontend/app/[resource]/page.tsx.ejs +391 -391
- package/hedhog/frontend/app/_components/catalog-ai-form-assist-dialog.tsx.ejs +340 -340
- package/hedhog/frontend/app/_components/catalog-resource-form-sheet.tsx.ejs +907 -907
- package/hedhog/frontend/app/_lib/catalog-resources.tsx.ejs +929 -929
- package/hedhog/frontend/app/dashboard/page.tsx.ejs +14 -83
- package/hedhog/frontend/messages/en.json +389 -389
- package/hedhog/frontend/messages/pt.json +389 -389
- package/hedhog/table/catalog_affiliate_program.yaml +41 -41
- package/hedhog/table/catalog_attribute.yaml +67 -67
- package/hedhog/table/catalog_attribute_group.yaml +18 -18
- package/hedhog/table/catalog_attribute_option.yaml +40 -40
- package/hedhog/table/catalog_brand.yaml +34 -34
- package/hedhog/table/catalog_category.yaml +40 -40
- package/hedhog/table/catalog_category_attribute.yaml +37 -37
- package/hedhog/table/catalog_click_event.yaml +50 -50
- package/hedhog/table/catalog_comparison.yaml +19 -19
- 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 +30 -30
- package/hedhog/table/catalog_product_attribute_value.yaml +44 -44
- package/hedhog/table/catalog_product_category.yaml +13 -13
- 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 +42 -42
- package/hedhog/table/catalog_seo_page_rule.yaml +10 -10
- package/hedhog/table/catalog_similarity_rule.yaml +33 -33
- package/hedhog/table/catalog_site.yaml +21 -21
- package/hedhog/table/catalog_site_category.yaml +12 -12
- package/package.json +6 -6
- package/src/catalog-resource.config.ts +132 -132
- package/src/catalog.controller.ts +91 -91
- package/src/catalog.module.ts +16 -16
- package/src/catalog.service.ts +1585 -1573
- package/src/index.ts +1 -1
- package/src/language/en.json +4 -4
- package/src/language/pt.json +4 -4
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
4
|
-
import { Badge } from '@/components/ui/badge';
|
|
5
|
-
import { Button } from '@/components/ui/button';
|
|
3
|
+
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
4
|
+
import { Badge } from '@/components/ui/badge';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
6
|
import {
|
|
7
7
|
Command,
|
|
8
8
|
CommandEmpty,
|
|
@@ -52,54 +52,54 @@ import Image from 'next/image';
|
|
|
52
52
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
53
53
|
import { useForm } from 'react-hook-form';
|
|
54
54
|
import { toast } from 'sonner';
|
|
55
|
-
import { z } from 'zod';
|
|
56
|
-
import { CatalogAiFormAssistDialog } from './catalog-ai-form-assist-dialog';
|
|
57
|
-
import {
|
|
58
|
-
catalogResourceMap,
|
|
59
|
-
getCatalogLocalizedText,
|
|
60
|
-
getCatalogRecordLabel,
|
|
55
|
+
import { z } from 'zod';
|
|
56
|
+
import { CatalogAiFormAssistDialog } from './catalog-ai-form-assist-dialog';
|
|
57
|
+
import {
|
|
58
|
+
catalogResourceMap,
|
|
59
|
+
getCatalogLocalizedText,
|
|
60
|
+
getCatalogRecordLabel,
|
|
61
61
|
type CatalogFormFieldDefinition,
|
|
62
62
|
type CatalogResourceDefinition,
|
|
63
63
|
} from '../_lib/catalog-resources';
|
|
64
64
|
|
|
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 = {
|
|
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 = {
|
|
103
103
|
open: boolean;
|
|
104
104
|
onOpenChange: (open: boolean) => void;
|
|
105
105
|
resource: string;
|
|
@@ -246,10 +246,10 @@ function serializeInitialValue(
|
|
|
246
246
|
return rawValue === null || rawValue === undefined ? '' : String(rawValue);
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
function buildDefaultValues(
|
|
250
|
-
resourceConfig: CatalogResourceDefinition,
|
|
251
|
-
payload: CatalogRecord
|
|
252
|
-
) {
|
|
249
|
+
function buildDefaultValues(
|
|
250
|
+
resourceConfig: CatalogResourceDefinition,
|
|
251
|
+
payload: CatalogRecord
|
|
252
|
+
) {
|
|
253
253
|
const defaults: CatalogFormValues = {};
|
|
254
254
|
|
|
255
255
|
for (const section of resourceConfig.formSections) {
|
|
@@ -258,27 +258,27 @@ function buildDefaultValues(
|
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
|
|
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
|
-
}
|
|
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
|
+
}
|
|
282
282
|
|
|
283
283
|
function normalizeSubmitValue(
|
|
284
284
|
field: CatalogFormFieldDefinition,
|
|
@@ -323,227 +323,227 @@ function normalizeSubmitValue(
|
|
|
323
323
|
return String(rawValue || '').trim() || null;
|
|
324
324
|
}
|
|
325
325
|
|
|
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
|
-
}
|
|
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
|
+
}
|
|
547
547
|
|
|
548
548
|
function extractRecordId(record: CatalogRecord) {
|
|
549
549
|
return Number(record.id ?? record.category_id ?? record.content_id ?? 0);
|
|
@@ -779,7 +779,7 @@ function CatalogRelationField({
|
|
|
779
779
|
);
|
|
780
780
|
}
|
|
781
781
|
|
|
782
|
-
function CatalogUploadField({
|
|
782
|
+
function CatalogUploadField({
|
|
783
783
|
field,
|
|
784
784
|
value,
|
|
785
785
|
onChange,
|
|
@@ -932,387 +932,387 @@ function CatalogUploadField({
|
|
|
932
932
|
|
|
933
933
|
{isUploading ? <Progress value={progress} /> : null}
|
|
934
934
|
</div>
|
|
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({
|
|
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({
|
|
1316
1316
|
open,
|
|
1317
1317
|
onOpenChange,
|
|
1318
1318
|
resource,
|
|
@@ -1322,28 +1322,28 @@ export function CatalogResourceFormSheet({
|
|
|
1322
1322
|
recordId,
|
|
1323
1323
|
onSuccess,
|
|
1324
1324
|
}: FormSheetProps) {
|
|
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
|
-
);
|
|
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
|
+
);
|
|
1340
1340
|
|
|
1341
1341
|
const form = useForm<CatalogFormValues>({
|
|
1342
1342
|
resolver: zodResolver(formSchema),
|
|
1343
1343
|
defaultValues: buildDefaultValues(resourceConfig, resourceConfig.template),
|
|
1344
1344
|
});
|
|
1345
1345
|
|
|
1346
|
-
const { data: currentRecord, isLoading } = useQuery<CatalogRecord>({
|
|
1346
|
+
const { data: currentRecord, isLoading } = useQuery<CatalogRecord>({
|
|
1347
1347
|
queryKey: ['catalog-record-details', resource, recordId],
|
|
1348
1348
|
queryFn: async () => {
|
|
1349
1349
|
const response = await request({
|
|
@@ -1352,185 +1352,185 @@ export function CatalogResourceFormSheet({
|
|
|
1352
1352
|
});
|
|
1353
1353
|
|
|
1354
1354
|
return response.data as CatalogRecord;
|
|
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);
|
|
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);
|
|
1458
1458
|
} catch (error) {
|
|
1459
1459
|
toast.error(
|
|
1460
1460
|
error instanceof Error ? error.message : t('toasts.saveError')
|
|
1461
1461
|
);
|
|
1462
1462
|
}
|
|
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}>
|
|
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}>
|
|
1534
1534
|
<SheetContent className="w-full overflow-x-hidden overflow-y-auto sm:max-w-2xl">
|
|
1535
1535
|
<SheetHeader>
|
|
1536
1536
|
<SheetTitle>{submitLabel}</SheetTitle>
|
|
@@ -1543,30 +1543,30 @@ export function CatalogResourceFormSheet({
|
|
|
1543
1543
|
Carregando dados do registro...
|
|
1544
1544
|
</div>
|
|
1545
1545
|
) : (
|
|
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}
|
|
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}
|
|
1570
1570
|
className="min-w-0 max-w-full space-y-4"
|
|
1571
1571
|
>
|
|
1572
1572
|
<div>
|
|
@@ -1746,24 +1746,24 @@ export function CatalogResourceFormSheet({
|
|
|
1746
1746
|
</div>
|
|
1747
1747
|
))}
|
|
1748
1748
|
</div>
|
|
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
|
-
)}
|
|
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
|
+
)}
|
|
1767
1767
|
|
|
1768
1768
|
<SheetFooter className="border-t">
|
|
1769
1769
|
<div className="w-full">
|