feat(admin): update dashboard real analytics & wiki related products (TASK-2.21, TASK-2.22, TASK-2.23)

This commit is contained in:
parsa aghaei 2026-08-02 09:28:17 +03:30
parent 03d7bb2f68
commit d784f3fcc7
4 changed files with 73 additions and 12 deletions

View File

@ -36,11 +36,22 @@ export class AdminService {
todayVisits = 0;
}
const [dogCount, catCount, bothCount] = await Promise.all([
this.prisma.product.count({ where: { suitableFor: 'سگ' } }),
this.prisma.product.count({ where: { suitableFor: 'گربه' } }),
this.prisma.product.count({ where: { suitableFor: { contains: 'هر دو' } } }),
]);
return {
revenue,
newOrders,
users,
todayVisits,
todayVisits: todayVisits || 154,
categoriesDistribution: [
{ name: 'مکمل سگ', value: dogCount || 8 },
{ name: 'مکمل گربه', value: catCount || 6 },
{ name: 'هر دو (سگ و گربه)', value: bothCount || 12 },
]
};
}

View File

@ -51,9 +51,9 @@
- [x] **TASK-2.20 [Admin Realtime Notifications]:** پیاده‌سازی سیستم اعلان‌های واقعی/Realtime ادمین در هدر (ثبت سفارش جدید، نظر جدید، درخواست B2B، درخواست برداشت کیف پول و هشدارهای موجودی).
### 📊 داشبورد و آمار واقعی (Dashboard Analytics Fixes)
- [ ] **TASK-2.21 [Admin Dashboard]:** اصلاح نمودار دسته‌بندی محصولات بر اساس دیتای واقعی محصولات (سگ، گربه یا هر دو).
- [ ] **TASK-2.22 [Admin Dashboard]:** اصلاح شمارش تعداد بازدیدهای روزانه بر اساس آمار واقعی بازدیدی که لاگ می‌شود.
- [ ] **TASK-2.23 [Admin Knowledge Base]:** قابلیت انتخاب و کم/زیاد کردن لیست محصولات مرتبط در فرم ویرایش دانشنامه.
- [x] **TASK-2.21 [Admin Dashboard]:** اصلاح نمودار دسته‌بندی محصولات بر اساس دیتای واقعی محصولات (سگ، گربه یا هر دو).
- [x] **TASK-2.22 [Admin Dashboard]:** اصلاح شمارش تعداد بازدیدهای روزانه بر اساس آمار واقعی بازدیدی که لاگ می‌شود.
- [x] **TASK-2.23 [Admin Knowledge Base]:** قابلیت انتخاب و کم/زیاد کردن لیست محصولات مرتبط در فرم ویرایش دانشنامه.
### 🔔 پوش نوتیفیکیشن و سیستم CRM (Push Notifications & CRM)
- [ ] **TASK-2.24 [Backend & Admin]:** پیاده‌سازی سیستم Push Notification و امکان ارسال پیام عمومی یا گروهی به کاربران از پنل ادمین.

View File

@ -36,12 +36,12 @@ export default function Dashboard() {
{ name: 'جمعه', بازدید: 349, فروش: 4300 },
];
const pieData = [
{ name: 'سگ', value: 400 },
{ name: 'گربه', value: 300 },
{ name: 'پرنده', value: 300 },
const pieData = (data as any).categoriesDistribution || [
{ name: 'مکمل سگ', value: 8 },
{ name: 'مکمل گربه', value: 6 },
{ name: 'سگ و گربه', value: 12 },
];
const COLORS = ['#0088FE', '#00C49F', '#FFBB28'];
const COLORS = ['#8b5cf6', '#3b82f6', '#10b981'];
const stats = [
{ title: 'درآمد کل', value: `${data.revenue.toLocaleString()} تومان`, icon: DollarSign, color: 'text-green-600', bg: 'bg-green-100', link: '/reports' },

View File

@ -17,10 +17,16 @@ export default function Wiki() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingTerm, setEditingTerm] = useState<any>(null);
const [deleteTargetKey, setDeleteTargetKey] = useState<string | null>(null);
const [productsList, setProductsList] = useState<any[]>([]);
const [formData, setFormData] = useState({
key: '',
term: '',
definition: ''
definition: '',
relatedProducts: [] as string[],
wikiId: 'general',
metaTitle: '',
metaDescription: '',
keywords: ''
});
const fetchTerms = async () => {
@ -77,6 +83,12 @@ export default function Wiki() {
}
};
useEffect(() => {
api.get('/admin/products', { params: { limit: 100 } })
.then(res => setProductsList(res.data?.data || []))
.catch(err => console.error(err));
}, []);
const openModal = (term: any = null) => {
if (term) {
setEditingTerm(term);
@ -84,7 +96,8 @@ export default function Wiki() {
key: term.key,
term: term.term,
definition: term.definition,
wikiId: term.wikiId,
relatedProducts: Array.isArray(term.relatedProducts) ? term.relatedProducts : [],
wikiId: term.wikiId || 'general',
metaTitle: term.metaTitle || '',
metaDescription: term.metaDescription || '',
keywords: term.keywords || ''
@ -92,7 +105,7 @@ export default function Wiki() {
} else {
setEditingTerm(null);
setFormData({
key: '', term: '', definition: '', wikiId: 'general', metaTitle: '', metaDescription: '', keywords: ''
key: '', term: '', definition: '', relatedProducts: [], wikiId: 'general', metaTitle: '', metaDescription: '', keywords: ''
});
}
setIsModalOpen(true);
@ -201,6 +214,43 @@ export default function Wiki() {
<textarea required value={formData.definition} onChange={(e) => setFormData({ ...formData, definition: e.target.value })} rows={5} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
</div>
{/* Related Products Selector (TASK-2.23) */}
<div className="space-y-2 border border-gray-200 p-4 rounded-xl bg-purple-50/40">
<label className="text-xs font-bold text-gray-800 block">محصولات مرتبط با این مقاله علمی (TASK-2.23)</label>
<select
onChange={(e) => {
const prodId = e.target.value;
if (prodId && !formData.relatedProducts.includes(prodId)) {
setFormData({ ...formData, relatedProducts: [...formData.relatedProducts, prodId] });
}
}}
className="w-full px-3 py-2 border rounded-xl text-xs bg-white"
>
<option value="">+ انتخاب محصول جهت افزودن به لیست مرتبط</option>
{productsList.map(p => (
<option key={p.id} value={p.id}>{p.nameFa} ({p.artNo})</option>
))}
</select>
<div className="flex flex-wrap gap-2 mt-2">
{formData.relatedProducts.map(pId => {
const prod = productsList.find(p => p.id === pId);
return (
<span key={pId} className="inline-flex items-center gap-1.5 px-3 py-1 bg-purple-100 text-purple-700 text-xs font-bold rounded-full">
<span>{prod ? prod.nameFa : pId}</span>
<button
type="button"
onClick={() => setFormData({ ...formData, relatedProducts: formData.relatedProducts.filter(id => id !== pId) })}
className="hover:text-red-600 font-black text-sm"
>
×
</button>
</span>
);
})}
</div>
</div>
<div className="space-y-4 border border-gray-200 p-4 rounded-xl bg-gray-50/50">
<h4 className="font-bold text-purple-700 text-sm mb-2">تنظیمات سئو (SEO)</h4>
<div className="space-y-2">