diff --git a/app/src/admin/Dashboard.jsx b/app/src/admin/Dashboard.jsx new file mode 100644 index 0000000..89d8928 --- /dev/null +++ b/app/src/admin/Dashboard.jsx @@ -0,0 +1,468 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useAuth, useNav, useToast, RoleBadge, EmptyState, pillBtn, inputStyle } from '../../teknav-ui.jsx'; +import { adminApi } from '../lib/admin-api.js'; +import { iconNode } from '../lib/icon.jsx'; +import triangleAlertIcon from '../assets/icons/triangle-alert.svg'; +import { SIDEBAR_ITEMS, BRAND, PanelState, actionBtn, ADMIN_ROLE_NAMES } from './Shared.jsx'; +import { ArticleEditor } from './ArticleEditor.jsx'; +import { ArticlesManagement } from './ArticleList.jsx'; +import { UserManagement, CategoryManagement, TagsManagement, AuthorsManagement } from './UserManagement.jsx'; +import { MediaLibrary } from './MediaLibrary.jsx'; +import { CommentsModeration } from './CommentModeration.jsx'; +import { Phase9AnalyticsPanel, SeoAuditPanel, JobsPanel, ClientErrorsPanel } from './AnalyticsPanel.jsx'; +import { ReviewsPanel } from './ReviewPanel.jsx'; +import { NewsletterPanel } from './NewsletterPanel.jsx'; +import { SeriesManagementPanel } from './SeriesPanel.jsx'; + +function AdminSidebar({ activeTab, onTab, user, open, onClose }) { + const items = SIDEBAR_ITEMS[user?.role] || SIDEBAR_ITEMS.writer; + return ( + <> + {open &&
} + + + ); +} + +function DashboardStats({ role }) { + const [statsData, setStatsData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(false); + adminApi.getDashboard() + .then(res => { if (!cancelled) setStatsData(res); }) + .catch(() => { if (!cancelled) { setStatsData(null); setError(true); } }) + .finally(() => { if (!cancelled) setLoading(false); }); + return () => { cancelled = true; }; + }, []); + if (loading || error || !statsData || statsData.totalArticles === 0) { + return ( +
+ +
+ ); + } + const published = statsData.published ?? 0; + const pending = statsData.pending ?? 0; + const drafts = statsData.drafts ?? 0; + const totalViews = statsData.totalViews ?? 0; + + const adminStats = [ + { label: 'مقاله منتشرشده', value: published, color: '#2F8F6B', icon: '◉' }, + { label: 'در انتظار بررسی', value: pending, color: '#D08A22', icon: '◷' }, + { label: 'پیش‌نویس', value: drafts, color: '#5F6B6D', icon: '◐' }, + { label: 'مجموع بازدید', value: totalViews.toLocaleString('fa'), color: '#0F6B73', icon: '◈' }, + { label: 'کاربران', value: statsData.totalUsers ?? 0, color: '#3A7D5E', icon: '◐' }, + { label: 'رسانه‌ها', value: statsData.totalMedia ?? 0, color: '#C46A4D', icon: '▣' }, + ]; + + const editorStats = [ + { label: 'در انتظار تأیید', value: pending, color: '#D08A22', icon: '◷' }, + { label: 'منتشرشده', value: published, color: '#2F8F6B', icon: '◉' }, + { label: 'نیازمند اصلاح', value: statsData.needsRevision ?? 0, color: '#C94C4C', icon: triangleAlertIcon }, + ]; + + const writerStats = [ + { label: 'مقاله منتشرشده', value: published, color: '#2F8F6B', icon: '◉' }, + { label: 'پیش‌نویس', value: drafts, color: '#5F6B6D', icon: '◐' }, + { label: 'در انتظار بررسی', value: pending, color: '#D08A22', icon: '◷' }, + ]; + + const stats = role === 'admin' ? adminStats : role === 'editor' ? editorStats : writerStats; + + return ( +
+ {stats.map(s => ( +
+
{s.value}
+
{s.label}
+
+ ))} +
+ ); +} + +function LiveAdminFeed() { + const [liveData, setLiveData] = useState({ count: 0, guests: 0, users: 0, commentsPerMin: 0 }); + const [events, setEvents] = useState([]); + const [connected, setConnected] = useState(false); + + useEffect(() => { + const es = new EventSource('/api/v1/admin/stream', { withCredentials: true }); + es.onopen = () => setConnected(true); + es.onerror = () => setConnected(false); + es.addEventListener('visitor_update', event => { + try { + const parsed = JSON.parse(event.data); + setLiveData({ + count: parsed.count || 0, + guests: parsed.guests || 0, + users: parsed.users || 0, + commentsPerMin: parsed.commentsPerMin || 0 + }); + } catch {} + }); + es.addEventListener('activity', event => setEvents(list => [JSON.parse(event.data), ...list].slice(0, 50))); + return () => es.close(); + }, []); + + return ( +
+
+ وضعیت زنده + + + {connected ? 'متصل' : 'قطع'} + +
+ +
+
+
{liveData.count.toLocaleString('fa-IR')}
+
بازدید کل
+
+
+
{liveData.users.toLocaleString('fa-IR')}
+
کاربر لاگین‌شده
+
+
+
{liveData.guests.toLocaleString('fa-IR')}
+
مهمان
+
+
+
{liveData.commentsPerMin.toLocaleString('fa-IR')}
+
نظر بر دقیقه
+
+
+ +
آخرین رخدادها
+
+ {events.length === 0 ?
هنوز رخدادی ثبت نشده...
: null} + {events.slice(0, 6).map((event, idx) => ( +
+ {event.actor}: {event.action || event.type} روی {event.target} +
+ ))} +
+
+ ); +} + +function ActivityLog() { + const [log, setLog] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(false); + adminApi.listActivity({ limit: 50 }) + .then(items => { if (!cancelled) { setLog(Array.isArray(items) ? items : []); setLoading(false); } }) + .catch(() => { if (!cancelled) { setLog([]); setError(true); setLoading(false); } }); + return () => { cancelled = true; }; + }, []); + const typeColors = { publish: '#2F8F6B', submit: '#D49A2A', draft: '#5F6B6D', approve: '#2F8F6B', reject: '#C94C4C', user: '#0F6B73', tag: '#C76D4A', category: '#D49A2A', edit: '#0F6B73', role: '#C76D4A', delete: '#C94C4C', author: '#C76D4A', upload: '#2F8F6B', media: '#C46A4D', schedule: '#D49A2A' }; + if (loading) return
در حال بارگذاری...
; + if (error) return ; + if (log.length === 0) return
هنوز فعالیتی ثبت نشده است.
; + return ( +
+
+ خروجی CSV +
+ {log.slice(0, 20).map(item => ( +
+
+
+ {typeof item.user === 'string' ? item.user : (item.user?.name ?? 'کاربر')} + — {item.action}: + {item.target} +
+ + {item.time || (item.createdAt ? new Date(item.createdAt).toLocaleDateString('fa-IR') : '')} + +
+ ))} +
+ ); +} + +function DashboardCategoryStats() { + const [cats, setCats] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(false); + adminApi.listCategories() + .then(items => { if (!cancelled) setCats(items ?? []); }) + .catch(() => { if (!cancelled) { setCats([]); setError(true); } }) + .finally(() => { if (!cancelled) setLoading(false); }); + return () => { cancelled = true; }; + }, []); + + if (loading || error || cats.length === 0) { + return ; + } + + return cats.map(c => ( +
+ {c.name} +
+
+ {c.articleCount ?? 0} +
+
+ )); +} + +function DashboardTopArticles() { + const { navigate } = useNav(); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(false); + adminApi.getDashboard() + .then(res => { if (!cancelled) setItems(res?.topArticles ?? []); }) + .catch(() => { if (!cancelled) { setItems([]); setError(true); } }) + .finally(() => { if (!cancelled) setLoading(false); }); + return () => { cancelled = true; }; + }, []); + + if (loading || error || items.length === 0) { + return ; + } + + return items.map(article => ( + + )); +} + +function RoleMetricsPanel({ role }) { + const [metrics, setMetrics] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const load = useCallback(() => { + setError(false); + return adminApi.getPanelMetrics() + .then(setMetrics) + .catch(() => { setMetrics(null); setError(true); }) + .finally(() => setLoading(false)); + }, []); + + useEffect(() => { + let cancelled = false; + load(); + const timer = setInterval(() => { if (!cancelled) load(); }, 15000); + let es; + if (role === 'admin' || role === 'editor') { + es = new EventSource('/api/v1/admin/stream', { withCredentials: true }); + ['activity', 'view_update', 'visitor_update'].forEach((eventName) => { + es.addEventListener(eventName, () => load()); + }); + } + return () => { cancelled = true; clearInterval(timer); es?.close(); }; + }, [load, role]); + + if (loading || error || !metrics || metrics.statuses?.totalArticles === 0) { + return ; + } + + const eventTotal = (metrics.realtime?.eventsByType ?? []).reduce((sum, item) => sum + Number(item.count ?? 0), 0); + const sets = { + admin: [ + ['مقاله منتشرشده', metrics.statuses?.published, '#2F8F6B'], + ['در انتظار بررسی', metrics.statuses?.pending, '#D49A2A'], + ['نیازمند اصلاح', metrics.statuses?.needsRevision, '#C94C4C'], + ['بازدید زنده', metrics.engagement?.views, '#0F6B73'], + ['تعامل‌ها', (metrics.engagement?.comments ?? 0) + (metrics.engagement?.saved ?? 0), '#3A7D5E'], + ['ایراد کیفیت', metrics.quality?.recentWithIssues, '#C46A4D'], + ], + editor: [ + ['در انتظار تایید', metrics.statuses?.pending, '#D49A2A'], + ['منتشرشده', metrics.statuses?.published, '#2F8F6B'], + ['نیازمند اصلاح', metrics.statuses?.needsRevision, '#C94C4C'], + ['مشترک خبرنامه', metrics.growth?.newsletterSubscribers, '#0F6B73'], + ['رویدادهای زنده', eventTotal, '#3A7D5E'], + ], + writer: [ + ['منتشرشده', metrics.statuses?.published, '#2F8F6B'], + ['پیش‌نویس', metrics.statuses?.drafts, '#5F6B6D'], + ['در انتظار بررسی', metrics.statuses?.pending, '#D49A2A'], + ['بازدید نوشته‌ها', metrics.engagement?.views, '#0F6B73'], + ['دنبال‌کننده', metrics.engagement?.writerFollows, '#3A7D5E'], + ], + reviewer: [ + ['در صف بررسی', metrics.statuses?.pending, '#D49A2A'], + ['تاییدشده', metrics.workflow?.reviewsApproved, '#2F8F6B'], + ['نیازمند اصلاح', metrics.statuses?.needsRevision, '#C94C4C'], + ['ایراد کیفیت اخیر', metrics.quality?.recentWithIssues, '#0F6B73'], + ], + }; + const stats = sets[role] ?? sets.writer; + + return ( +
+
+ {stats.map(([label, value, color]) => ( +
+
{Number(value ?? 0).toLocaleString('fa-IR')}
+
{label}
+
+ ))} +
+
+
پوشش زنده پنل
+
+ {(metrics.realtime?.eventsByType ?? []).slice(0, 8).map((item) => ( + {item.type}: {Number(item.count ?? 0).toLocaleString('fa-IR')} + ))} + اعلان خوانده‌نشده: {Number(metrics.realtime?.unreadNotifications ?? 0).toLocaleString('fa-IR')} +
+
+
+ ); +} + +function AdminPanel() { + const { user, logout } = useAuth(); + const { navigate } = useNav(); + const [tab, setTab] = useState('dashboard'); + const [editingArticle, setEditingArticle] = useState(null); + const [sidebarOpen, setSidebarOpen] = useState(false); + + useEffect(() => { if (!user) navigate('/login'); }, [user]); + if (!user) return null; + + const sectionTitle = (t) =>

{t}

; + + const renderContent = () => { + if (tab === 'create' || editingArticle) return ( + { setEditingArticle(null); setTab('articles'); }} /> + ); + if (tab === 'articles') return ( + <> + {sectionTitle(user.role === 'reviewer' ? 'مقاله‌های در انتظار بررسی' : 'مدیریت مقاله‌ها')} + { setEditingArticle(a); setTab('edit'); }} /> + + ); + if (tab === 'users' && user.role === 'admin') return <>{sectionTitle('مدیریت کاربران')}; + if (tab === 'authors' && (user.role === 'admin' || user.role === 'editor')) return <>{sectionTitle('مدیریت نویسندگان')}; + if (tab === 'media' && (user.role === 'admin' || user.role === 'editor')) return <>{sectionTitle('کتابخانه رسانه')}; + if (tab === 'comments' && (user.role === 'admin' || user.role === 'editor')) return <>{sectionTitle('مدیریت نظرها')}; + if (tab === 'analytics') return <>{sectionTitle('تحلیل محتوا')}; + if (tab === 'reviews') return <>{sectionTitle('گردش بررسی مقاله‌ها')}; + if (tab === 'newsletter' && (user.role === 'admin' || user.role === 'editor')) return <>{sectionTitle('خبرنامه')}; + if (tab === 'series' && (user.role === 'admin' || user.role === 'editor')) return <>{sectionTitle('سری مقاله‌ها')}; + if (tab === 'categories') return <>{sectionTitle('مدیریت دسته‌بندی‌ها')}; + if (tab === 'tags' && user.role === 'admin') return <>{sectionTitle('مدیریت برچسب‌ها')}; + if (tab === 'seo-audit' && user.role === 'admin') return <>{sectionTitle('ممیزی SEO')}; + if (tab === 'jobs' && (user.role === 'admin' || user.role === 'editor')) return <>{sectionTitle('آگهی‌های شغلی')}; + if (tab === 'client-errors' && user.role === 'admin') return <>{sectionTitle('خطاهای کلاینت')}; + if (tab === 'activity' && user.role === 'admin') return <>{sectionTitle('گزارش فعالیت')}; + return ( +
+

+ خوش آمدید، {user.name} +

+

نقش شما:

+ {(user.role === 'admin' || user.role === 'editor') && } + + {user.role === 'admin' && ( +
+
+

آخرین فعالیت‌ها

+ +
+
+

آمار دسته‌بندی‌ها

+ +
+
+

پربازدیدترین مقاله‌ها

+ +
+
+ )} +
+ ); + }; + + return ( +
+ { setTab(t); setSidebarOpen(false); }} user={user} open={sidebarOpen} onClose={() => setSidebarOpen(false)} /> +
+
+
+ +
+ پنل مدیریت تکناو +
محیط تولید، بازبینی و انتشار مقاله
+
+
+
+ + + +
+
+
+ {renderContent()} +
+
+ +
+ ); +} + +export { AdminPanel, DashboardStats, LiveAdminFeed, ActivityLog, DashboardCategoryStats, DashboardTopArticles, RoleMetricsPanel, AdminSidebar };