From ceaab10fddbf1c5498b44b0e8e341f65803b8b06 Mon Sep 17 00:00:00 2001 From: parsa aghaei Date: Wed, 2 Sep 2026 12:09:57 +0330 Subject: [PATCH] feat(admin): add comprehensive product attributes management and dynamic analysis table --- backend/src/admin/admin.service.ts | 32 + backend/src/admin/dto/product.dto.ts | 28 + frontend/admin-panel/src/pages/Products.tsx | 414 +++- .../lib/services/productService.ts | 19 +- graphify-out/.graphify_labels.json | 13 +- graphify-out/.graphify_labels.json.sig | 2 +- graphify-out/2026-09-02/.graphify_labels.json | 4 +- graphify-out/2026-09-02/GRAPH_REPORT.md | 22 +- graphify-out/2026-09-02/graph.json | 382 ++-- graphify-out/GRAPH_REPORT.md | 63 +- graphify-out/cache/stat-index.json | 2 +- graphify-out/graph.html | 6 +- graphify-out/graph.json | 1994 ++++++++--------- graphify-out/manifest.json | 1254 +++++------ 14 files changed, 2362 insertions(+), 1873 deletions(-) diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts index a29c076..353bb27 100644 --- a/backend/src/admin/admin.service.ts +++ b/backend/src/admin/admin.service.ts @@ -460,6 +460,22 @@ export class AdminService { ...(data.keyHighlights !== undefined ? { keyHighlights: data.keyHighlights } : {}), + ...(data.keyBenefits !== undefined + ? { keyBenefits: data.keyBenefits } + : {}), + ...(data.analysis !== undefined ? { analysis: data.analysis } : {}), + ...(data.onSetOfAction !== undefined + ? { onSetOfAction: data.onSetOfAction } + : {}), + ...(data.specialBadge !== undefined + ? { specialBadge: data.specialBadge } + : {}), + ...(data.contraindications !== undefined + ? { contraindications: data.contraindications } + : {}), + ...(data.expectedResults !== undefined + ? { expectedResults: data.expectedResults } + : {}), ...(data.isPreorder !== undefined ? { isPreorder: data.isPreorder } : {}), ...(data.preorderDeposit !== undefined ? { preorderDeposit: data.preorderDeposit } @@ -584,6 +600,22 @@ export class AdminService { ...(data.keyHighlights !== undefined ? { keyHighlights: data.keyHighlights } : {}), + ...(data.keyBenefits !== undefined + ? { keyBenefits: data.keyBenefits } + : {}), + ...(data.analysis !== undefined ? { analysis: data.analysis } : {}), + ...(data.onSetOfAction !== undefined + ? { onSetOfAction: data.onSetOfAction } + : {}), + ...(data.specialBadge !== undefined + ? { specialBadge: data.specialBadge } + : {}), + ...(data.contraindications !== undefined + ? { contraindications: data.contraindications } + : {}), + ...(data.expectedResults !== undefined + ? { expectedResults: data.expectedResults } + : {}), ...(data.isPreorder !== undefined ? { isPreorder: data.isPreorder } : {}), ...(data.preorderDeposit !== undefined ? { preorderDeposit: data.preorderDeposit } diff --git a/backend/src/admin/dto/product.dto.ts b/backend/src/admin/dto/product.dto.ts index 5ea9f0a..2596f9c 100644 --- a/backend/src/admin/dto/product.dto.ts +++ b/backend/src/admin/dto/product.dto.ts @@ -304,6 +304,34 @@ export class ProductDto { @IsArray() keyHighlights?: string[]; + @ApiPropertyOptional({ description: 'کارت‌های شاخص «چرا این محصول؟» (۳ کارت مزایا)' }) + @IsOptional() + @IsArray() + keyBenefits?: Array<{ icon: string; title: string; description: string }>; + + @ApiPropertyOptional({ description: 'جدول آنالیز و درصد ترکیبات (Analytical Constituents)' }) + @IsOptional() + analysis?: Record; + + @ApiPropertyOptional({ description: 'زمان شروع اثربخشی (Onset of Action)' }) + @IsOptional() + @IsString() + onSetOfAction?: string; + + @ApiPropertyOptional({ description: 'برچسب ویژه محصول (Special Badge)' }) + @IsOptional() + @IsString() + specialBadge?: string; + + @ApiPropertyOptional({ description: 'موارد منع مصرف (Contraindications)' }) + @IsOptional() + contraindications?: string[] | string; + + @ApiPropertyOptional({ description: 'نتایج بالینی مورد انتظار (Expected Clinical Results)' }) + @IsOptional() + @IsArray() + expectedResults?: Array<{ icon: string; text: string }>; + @ApiPropertyOptional({ description: 'پیکربندی جامع دوز و داده‌های تکمیلی' }) @IsOptional() dosageConfig?: Record; diff --git a/frontend/admin-panel/src/pages/Products.tsx b/frontend/admin-panel/src/pages/Products.tsx index c9bb98c..62b876d 100644 --- a/frontend/admin-panel/src/pages/Products.tsx +++ b/frontend/admin-panel/src/pages/Products.tsx @@ -33,6 +33,14 @@ import { Thermometer, AlertTriangle, Sparkles, + Star, + FlaskConical, + Shield, + Heart, + Zap, + Clock, + Tag, + Activity, } from 'lucide-react'; import { toast } from 'react-hot-toast'; import api, { BASE_DOMAIN } from '../services/api'; @@ -279,6 +287,11 @@ export default function Products() { precautions: '', lifeStage: '', keyHighlights: ['', '', '', ''] as string[], + keyBenefits: [] as Array<{ icon: string; title: string; description: string }>, + analysis: [] as Array<{ key: string; value: string }>, + onSetOfAction: '', + specialBadge: '', + contraindications: '', symptoms: [] as string[], isPreorder: false, preorderDeposit: '' as number | string, @@ -422,6 +435,38 @@ export default function Products() { initialHighlights[3] || '', ]; + // Parse keyBenefits (Why this product / 3 Cards) + let initialBenefits: Array<{ icon: string; title: string; description: string }> = []; + const rawBenefits = dosageConfig.keyBenefits || (product as unknown as { keyBenefits?: any }).keyBenefits; + if (Array.isArray(rawBenefits)) { + initialBenefits = rawBenefits.map((b: any) => ({ + icon: b.icon || 'Star', + title: b.title || '', + description: b.description || '', + })); + } + + // Parse analytical constituents table (Key-Value) + let initialAnalysis: Array<{ key: string; value: string }> = []; + const rawAnalysis = dosageConfig.analysis || (product as unknown as { analysis?: any }).analysis || (product as unknown as { compositionTable?: any }).compositionTable; + if (rawAnalysis && typeof rawAnalysis === 'object' && !Array.isArray(rawAnalysis)) { + initialAnalysis = Object.entries(rawAnalysis).map(([k, v]) => ({ + key: k, + value: String(v || '') + })); + } else if (Array.isArray(rawAnalysis)) { + initialAnalysis = rawAnalysis.map((item: any) => ({ + key: item.key || item.title || '', + value: String(item.value || item.percent || '') + })); + } + + const initialContraindications = dosageConfig.contraindications + ? (Array.isArray(dosageConfig.contraindications) ? dosageConfig.contraindications.join('\n') : String(dosageConfig.contraindications)) + : ((product as unknown as { contraindications?: any }).contraindications + ? (Array.isArray((product as unknown as { contraindications?: any }).contraindications) ? (product as unknown as { contraindications?: any }).contraindications.join('\n') : String((product as unknown as { contraindications?: any }).contraindications)) + : ''); + setFormData({ artNo: product.artNo || '', nameFa: product.nameFa || '', @@ -442,11 +487,16 @@ export default function Products() { feedingAdvice: dosageConfig.feedingAdvice || product.feedingAdvice || product.dosageLogic || '', showFaqs: dosageConfig.showFaqs !== undefined ? Boolean(dosageConfig.showFaqs) : (product.showFaqs !== undefined ? Boolean(product.showFaqs) : true), faqs: Array.isArray(initialFaqs) ? initialFaqs : [], - paoMonths: dosageConfig.paoMonths !== undefined ? dosageConfig.paoMonths : (product.paoMonths !== undefined ? product.paoMonths : '12'), + paoMonths: dosageConfig.paoMonths !== undefined ? dosageConfig.paoMonths : (product.paoMonths !== undefined ? product.paoMonths : ''), storageInfo: dosageConfig.storageInfo || product.storageInfo || '', precautions: dosageConfig.precautions || product.precautions || '', - lifeStage: dosageConfig.lifeStage || product.lifeStage || 'مناسب تمام سنین و دوران بارداری و شیردهی', + lifeStage: dosageConfig.lifeStage || product.lifeStage || '', keyHighlights: paddedHighlights, + keyBenefits: initialBenefits, + analysis: initialAnalysis, + onSetOfAction: dosageConfig.onSetOfAction || (product as unknown as { onSetOfAction?: string }).onSetOfAction || '', + specialBadge: dosageConfig.specialBadge || (product as unknown as { specialBadge?: string }).specialBadge || '', + contraindications: initialContraindications, suitableFor: product.suitableFor || 'سگ و گربه', imageUrl: product.imageUrl || '', slug: product.slug || '', @@ -497,6 +547,11 @@ export default function Products() { precautions: '', lifeStage: '', keyHighlights: ['', '', '', ''], + keyBenefits: [], + analysis: [], + onSetOfAction: '', + specialBadge: '', + contraindications: '', suitableFor: 'سگ و گربه', imageUrl: '', metaTitle: '', metaDescription: '', keywords: '', canonicalUrl: '', slug: '', stockStatus: 'IN_STOCK', noIndex: false, noFollow: false, ogImage: '', featuredImageAlt: '', @@ -704,6 +759,22 @@ export default function Products() { try { const derivedSlug = (formData.slug || '').trim() || slugify(formData.nameEn || ''); + + // Prepare analysis object + const analysisObj: Record = {}; + if (formData.analysis && Array.isArray(formData.analysis)) { + formData.analysis.forEach(item => { + if (item && item.key?.trim() && item.value?.trim()) { + analysisObj[item.key.trim()] = item.value.trim(); + } + }); + } + + // Prepare keyBenefits array + const validBenefits = formData.keyBenefits + ? formData.keyBenefits.filter(b => b && (b.title?.trim() || b.description?.trim())) + : []; + const finalPayload: Record = { artNo: (formData.artNo || '').trim(), nameFa: (formData.nameFa || '').trim(), @@ -754,6 +825,11 @@ export default function Products() { precautions: (formData.precautions || '').trim() || undefined, lifeStage: (formData.lifeStage || '').trim() || undefined, keyHighlights: formData.keyHighlights ? formData.keyHighlights.filter(h => h && h.trim().length > 0) : [], + keyBenefits: validBenefits, + analysis: Object.keys(analysisObj).length > 0 ? analysisObj : undefined, + onSetOfAction: (formData.onSetOfAction || '').trim() || undefined, + specialBadge: (formData.specialBadge || '').trim() || undefined, + contraindications: (formData.contraindications || '').trim() || undefined, slug: derivedSlug, symptoms: formData.symptoms || [], isPreorder: Boolean(formData.isPreorder), @@ -1175,6 +1251,7 @@ export default function Products() { {[ { id: 'general', label: 'اطلاعات پایه' }, { id: 'technical', label: 'مشخصات علمی و نگهداری' }, + { id: 'benefits', label: 'مزایا و آنالیز ترکیبات' }, { id: 'faq', label: 'پرسش‌های متداول (FAQ)' }, { id: 'pricing', label: 'موجودی و قیمت' }, { id: 'seo', label: 'سئو (SEO)' }, @@ -1587,6 +1664,339 @@ export default function Products() { ))} + + {/* Clinical Presentation & Contraindications Card */} +
+

+ + برچسب ویژه، سرعت اثر و موارد منع مصرف +

+ +
+
+ + setFormData({ ...formData, specialBadge: e.target.value })} + placeholder="مثال: فرمولاسیون دارویی، پیشنهاد ویژه دامپزشک یا پرفروش‌ترین" + className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-bold font-vazir" + /> +
+ {['فرمولاسیون دارویی', 'پیشنهاد کلینیکی', 'پرفروش‌ترین کاتالوگ', 'تاییدیه GMP آلمان'].map((badge) => ( + + ))} +
+
+ +
+ + setFormData({ ...formData, onSetOfAction: e.target.value })} + placeholder="مثال: ۲ الی ۳ هفته پس از شروع مصرف مداوم" + className="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-bold font-vazir" + /> +
+ {['۲ الی ۴ هفته پس از شروع مصرف', 'تأثیر سریع در هفته اول', 'دوره کامل ۶۰ الی ۹۰ روزه'].map((onset) => ( + + ))} +
+
+ +
+ +