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 };