From 00839017ff50e5e1b4b0a3d3d29fc590beb4b0cd Mon Sep 17 00:00:00 2001 From: parsa aghaei Date: Wed, 2 Sep 2026 15:50:01 +0330 Subject: [PATCH] feat(products): remove mock static fallbacks and add dynamic specialist recommendation management in database and admin panel --- backend/src/admin/admin.service.ts | 2 + backend/src/admin/dto/product.dto.ts | 9 ++ frontend/admin-panel/src/pages/Products.tsx | 143 +++++++++++++++++- .../application/app/catalog/CatalogClient.tsx | 8 +- .../components/catalog/CatalogPageSpread.tsx | 29 +++- frontend/application/lib/data/products.ts | 2 +- .../lib/services/productService.ts | 89 +++++------ 7 files changed, 216 insertions(+), 66 deletions(-) diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts index 353bb27..748c569 100644 --- a/backend/src/admin/admin.service.ts +++ b/backend/src/admin/admin.service.ts @@ -476,6 +476,7 @@ export class AdminService { ...(data.expectedResults !== undefined ? { expectedResults: data.expectedResults } : {}), + ...(data.specialist !== undefined ? { specialist: data.specialist } : {}), ...(data.isPreorder !== undefined ? { isPreorder: data.isPreorder } : {}), ...(data.preorderDeposit !== undefined ? { preorderDeposit: data.preorderDeposit } @@ -616,6 +617,7 @@ export class AdminService { ...(data.expectedResults !== undefined ? { expectedResults: data.expectedResults } : {}), + ...(data.specialist !== undefined ? { specialist: data.specialist } : {}), ...(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 2596f9c..4bb38ed 100644 --- a/backend/src/admin/dto/product.dto.ts +++ b/backend/src/admin/dto/product.dto.ts @@ -332,6 +332,15 @@ export class ProductDto { @IsArray() expectedResults?: Array<{ icon: string; text: string }>; + @ApiPropertyOptional({ description: 'توصیه و نظر پزشک متخصص (Specialist Endorsement)' }) + @IsOptional() + specialist?: { + name?: string; + title?: string; + image?: string; + message?: 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 b0f4881..7060d8b 100644 --- a/frontend/admin-panel/src/pages/Products.tsx +++ b/frontend/admin-panel/src/pages/Products.tsx @@ -232,7 +232,7 @@ export default function Products() { const [isModalOpen, setIsModalOpen] = useState(false); const [previewImage, setPreviewImage] = useState<{ url: string; title: string; subtitle?: string; artNo?: string } | null>(null); const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false); - const [mediaTargetField, setMediaTargetField] = useState<'imageUrl' | 'ogImage' | 'podcastUrl' | 'podcastCover' | 'videoUrl' | 'videoCover' | 'pdfUrl' | 'pdfCover' | 'gallery'>('imageUrl'); + const [mediaTargetField, setMediaTargetField] = useState<'imageUrl' | 'ogImage' | 'podcastUrl' | 'podcastCover' | 'videoUrl' | 'videoCover' | 'pdfUrl' | 'pdfCover' | 'specialistImage' | 'gallery'>('imageUrl'); const [editingProduct, setEditingProduct] = useState(null); const [deleteTargetId, setDeleteTargetId] = useState(null); const [activeTab, setActiveTab] = useState('general'); @@ -297,6 +297,12 @@ export default function Products() { preorderDeposit: '' as number | string, roundingStep: '' as number | string, roundingMode: '' as string, + specialist: { + name: '', + title: '', + image: '', + message: '', + }, }); // Smart Pricing & Rounding state @@ -482,6 +488,14 @@ export default function Products() { ? (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)) : ''); + const rawSpecialist = dosageConfig.specialist || (product as unknown as { specialist?: any }).specialist; + const initialSpecialist = rawSpecialist && typeof rawSpecialist === 'object' ? { + name: rawSpecialist.name || '', + title: rawSpecialist.title || '', + image: rawSpecialist.image || '', + message: rawSpecialist.message || '', + } : { name: '', title: '', image: '', message: '' }; + setFormData({ artNo: product.artNo || '', nameFa: product.nameFa || '', @@ -542,6 +556,7 @@ export default function Products() { preorderDeposit: product.preorderDeposit !== undefined && product.preorderDeposit !== null ? product.preorderDeposit : '', roundingStep: product.roundingStep !== undefined && product.roundingStep !== null ? product.roundingStep : '', roundingMode: product.roundingMode || '', + specialist: initialSpecialist, }); } else { setEditingProduct(null); @@ -574,7 +589,8 @@ export default function Products() { podcastUrl: '', podcastTitle: '', podcastDescription: '', podcastCover: '', videoUrl: '', videoTitle: '', videoDescription: '', videoCover: '', pdfUrl: '', pdfTitle: '', pdfDescription: '', pdfCover: '', - symptoms: [], isPreorder: false, preorderDeposit: '' + symptoms: [], isPreorder: false, preorderDeposit: '', + specialist: { name: '', title: '', image: '', message: '' } }); } setIsModalOpen(true); @@ -916,7 +932,15 @@ export default function Products() { slug: derivedSlug, symptoms: formData.symptoms || [], isPreorder: Boolean(formData.isPreorder), - preorderDeposit: formData.preorderDeposit ? String(formData.preorderDeposit) : undefined + preorderDeposit: formData.preorderDeposit ? String(formData.preorderDeposit) : undefined, + specialist: formData.specialist && (formData.specialist.name?.trim() || formData.specialist.message?.trim()) + ? { + name: (formData.specialist.name || '').trim(), + title: (formData.specialist.title || '').trim(), + image: (formData.specialist.image || '').trim(), + message: (formData.specialist.message || '').trim(), + } + : null, }; if (editingProduct) { @@ -1822,6 +1846,103 @@ export default function Products() { + + {/* Specialist Endorsement Card */} +
+
+

+ + توصیه و نظر پزشک متخصص (Specialist Endorsement) +

+ {formData.specialist?.name && ( + + )} +
+

+ در صورت تکمیل این بخش، کادر نظر و توصیه بالینی پزشک در کاتالوگ و صفحه محصول نمایش داده می‌شود. در صورت خالی ماندن، هیچ کادری نمایش داده نخواهد شد. +

+ +
+
+ + setFormData({ + ...formData, + specialist: { ...(formData.specialist || { name: '', title: '', image: '', message: '' }), name: e.target.value } + })} + placeholder="مثال: دکتر علیرضا رضایی یا Dr. Klaus Henning" + className="w-full px-3 py-2 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-bold font-vazir" + /> +
+ +
+ + setFormData({ + ...formData, + specialist: { ...(formData.specialist || { name: '', title: '', image: '', message: '' }), title: e.target.value } + })} + placeholder="مثال: متخصص جراحی استخوان و مفاصل دامپزشکی" + className="w-full px-3 py-2 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-bold font-vazir" + /> +
+
+ +
+ +
+ setFormData({ + ...formData, + specialist: { ...(formData.specialist || { name: '', title: '', image: '', message: '' }), image: e.target.value } + })} + placeholder="آدرس تصویر یا انتخاب از رسانه‌ها (/uploads/doctor.jpg)" + className="flex-1 px-3 py-2 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono" + dir="ltr" + /> + +
+
+ +
+ +