feat(admin): implement symptoms management in admin panel product form

This commit is contained in:
parsa aghaei 2026-07-15 17:49:03 +03:30
parent 717a61a225
commit 8345410376
2 changed files with 80 additions and 12 deletions

View File

@ -101,7 +101,7 @@ export class AdminService {
const [data, total] = await Promise.all([
this.prisma.product.findMany({
where, skip, take: limit, orderBy: { createdAt: 'desc' },
include: { category: true }
include: { category: true, symptoms: true }
}),
this.prisma.product.count({ where })
]);
@ -114,7 +114,7 @@ export class AdminService {
}
async createProduct(data: any) {
return this.prisma.product.create({
const product = await this.prisma.product.create({
data: {
artNo: data.artNo,
nameFa: data.nameFa,
@ -138,10 +138,21 @@ export class AdminService {
slug: data.slug || data.artNo,
}
});
if (data.symptoms && Array.isArray(data.symptoms)) {
await this.prisma.productSymptom.createMany({
data: data.symptoms.map((s: string) => ({
productId: product.id,
symptom: s.trim()
})).filter((s: any) => s.symptom.length > 0)
});
}
return product;
}
async updateProduct(id: string, data: any) {
return this.prisma.product.update({
const product = await this.prisma.product.update({
where: { id },
data: {
artNo: data.artNo,
@ -166,6 +177,20 @@ export class AdminService {
slug: data.slug || data.artNo,
}
});
if (data.symptoms !== undefined && Array.isArray(data.symptoms)) {
await this.prisma.productSymptom.deleteMany({ where: { productId: id } });
if (data.symptoms.length > 0) {
await this.prisma.productSymptom.createMany({
data: data.symptoms.map((s: string) => ({
productId: id,
symptom: s.trim()
})).filter((s: any) => s.symptom.length > 0)
});
}
}
return product;
}
async deleteProduct(id: string) {

View File

@ -43,7 +43,8 @@ export default function Products() {
metaDescription: '',
keywords: '',
canonicalUrl: '',
slug: ''
slug: '',
symptoms: [] as string[]
});
const fetchData = async () => {
@ -104,14 +105,16 @@ export default function Products() {
metaDescription: product.metaDescription || '',
keywords: product.keywords || '',
canonicalUrl: product.canonicalUrl || '',
slug: product.slug || ''
slug: product.slug || '',
symptoms: product.symptoms ? product.symptoms.map((s: any) => s.symptom) : []
});
} else {
setEditingProduct(null);
setFormData({
artNo: '', nameFa: '', nameEn: '', scientificTagline: '', description: '', shortDescription: '', categoryId: '',
priceValue: 0, priceDisplay: '', unit: '', packageSize: 0, dosageLogic: '', suitableFor: 'سگ و گربه',
imageUrl: '', metaTitle: '', metaDescription: '', keywords: '', canonicalUrl: '', slug: ''
imageUrl: '', metaTitle: '', metaDescription: '', keywords: '', canonicalUrl: '', slug: '',
symptoms: []
});
}
setActiveTab('general');
@ -335,12 +338,52 @@ export default function Products() {
<label className="text-sm font-bold text-gray-700">توضیحات کامل</label>
<textarea rows={5} value={formData.description} onChange={(e) => setFormData({...formData, description: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-bold text-gray-700">منطق دوز مصرفی (Dosage Logic)</label>
<textarea rows={2} value={formData.dosageLogic} onChange={(e) => setFormData({...formData, dosageLogic: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
</div>
</div>
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-bold text-gray-700">منطق دوز مصرفی (Dosage Logic)</label>
<textarea rows={2} value={formData.dosageLogic} onChange={(e) => setFormData({...formData, dosageLogic: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-bold text-gray-700">علائم درمانی مرتبط (Symptom Tags)</label>
<div className="flex flex-wrap gap-2 p-3 bg-white border border-gray-200 rounded-xl min-h-[50px] items-center focus-within:border-purple-500 focus-within:ring-2 focus-within:ring-purple-200 transition-all">
{formData.symptoms && formData.symptoms.map((symptom, idx) => (
<span key={idx} className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold bg-purple-50 border border-purple-200 text-purple-700">
{symptom}
<button
type="button"
onClick={() => {
const newSymptoms = formData.symptoms.filter((_, i) => i !== idx);
setFormData({ ...formData, symptoms: newSymptoms });
}}
className="text-purple-400 hover:text-purple-600 focus:outline-none transition-colors"
>
<X className="w-3.5 h-3.5" />
</button>
</span>
))}
<input
type="text"
placeholder="تایپ و اینتر کنید..."
className="flex-1 bg-transparent border-none outline-none text-sm min-w-[150px] text-right font-vazir"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
const input = e.currentTarget;
const value = input.value.trim();
if (value && !formData.symptoms.includes(value)) {
setFormData({
...formData,
symptoms: [...(formData.symptoms || []), value]
});
input.value = '';
}
}
}}
/>
</div>
<p className="text-[11px] text-gray-400 font-medium">پس از نوشتن هر علامت درمانی (مثلاً «درد مفاصل»)، کلید Enter را فشار دهید.</p>
</div>
</div>
</div>
{/* Pricing Tab */}
<div className={activeTab === 'pricing' ? 'block space-y-4' : 'hidden'}>