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 (
+
+
+ {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 };