Import source: app/src/admin/Dashboard

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:24:00 +03:30
parent 06f3e68d9c
commit 882d5445a5
+468
View File
@@ -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 && <div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.4)', zIndex: 299, display: 'none' }} className="admin-overlay" />}
<aside className={`admin-sidebar${open ? ' open' : ''}`} style={{ width: 256, background: `linear-gradient(180deg,${BRAND.primaryDark} 0%,${BRAND.primary} 52%,#0A3136 100%)`, minHeight: '100%', padding: '22px 0', display: 'flex', flexDirection: 'column', flexShrink: 0, boxShadow: '-14px 0 40px rgba(15,42,46,0.16)' }}>
<div style={{ padding: '0 22px 22px', borderBottom: '1px solid rgba(250,247,240,0.14)' }}>
<div style={{ fontSize: 24, fontWeight: 900, color: '#fff', marginBottom: 4, fontFamily: "'PelakFA','Vazirmatn',sans-serif" }}>تکناو</div>
<div style={{ fontSize: 12, color: 'rgba(255,255,255,0.72)', letterSpacing: '0.02em' }}>پنل تولید محتوا</div>
</div>
<div style={{ padding: '18px 12px', flex: 1 }}>
{items.map(item => (
<button key={item.id} onClick={() => onTab(item.id)} style={{
display: 'flex', alignItems: 'center', gap: 12, width: '100%', textAlign: 'right',
padding: '12px 14px', marginBottom: 6, background: activeTab === item.id ? 'rgba(250,247,240,0.96)' : 'transparent',
border: '1px solid', borderColor: activeTab === item.id ? 'rgba(250,247,240,0.95)' : 'transparent',
borderRadius: 12,
color: activeTab === item.id ? BRAND.primary : 'rgba(255,255,255,0.74)', fontSize: 13,
fontFamily: 'Vazirmatn,sans-serif', fontWeight: activeTab === item.id ? 800 : 600, cursor: 'pointer', transition: 'all 0.18s',
boxShadow: activeTab === item.id ? '0 12px 28px rgba(0,0,0,0.18)' : 'none',
}}>
<span style={{ fontSize: 15, width: 26, height: 26, borderRadius: 8, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', background: activeTab === item.id ? `${BRAND.primary}14` : 'rgba(255,255,255,0.10)' }}>{iconNode(item.icon)}</span>
{item.label}
</button>
))}
</div>
<div style={{ padding: '16px 20px', borderTop: '1px solid rgba(250,247,240,0.14)' }}>
<div style={{ fontSize: 12, color: 'rgba(255,255,255,0.72)', marginBottom: 6 }}>{user?.name}</div>
<RoleBadge role={user?.role} />
</div>
</aside>
</>
);
}
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 (
<div style={{ marginBottom: 28 }}>
<PanelState loading={loading} error={error} empty={!loading && !error && (!statsData || statsData.totalArticles === 0)} emptyTitle="هنوز داده‌ای برای آمار پنل ثبت نشده است" />
</div>
);
}
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 (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(180px,1fr))', gap: 16, marginBottom: 28 }}>
{stats.map(s => (
<div key={s.label} style={{ background: '#fff', border: '1px solid #E4DDD2', borderRadius: 12, padding: '20px 20px', borderTop: `3px solid ${s.color}` }}>
<div style={{ fontSize: 24, fontWeight: 900, color: s.color, marginBottom: 4 }}>{s.value}</div>
<div style={{ fontSize: 13, color: '#5F6B6D' }}>{s.label}</div>
</div>
))}
</div>
);
}
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 (
<div style={{ background: '#fff', border: '1px solid #E4DDD2', borderRadius: 12, padding: 18, marginBottom: 20, direction: 'rtl' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'center', flexWrap: 'wrap', marginBottom: 16 }}>
<strong style={{ fontSize: 14, color: BRAND.ink }}>وضعیت زنده</strong>
<span style={{ fontSize: 12, color: connected ? '#2F8F6B' : '#C94C4C', display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: connected ? '#2F8F6B' : '#C94C4C', animation: connected ? 'pulseRing 2s infinite' : 'none' }} />
{connected ? 'متصل' : 'قطع'}
</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))', gap: 12, marginBottom: 16 }}>
<div style={{ background: '#FAF7F0', padding: 12, borderRadius: 8, textAlign: 'center' }}>
<div style={{ fontSize: 20, fontWeight: 900, color: BRAND.primary }}>{liveData.count.toLocaleString('fa-IR')}</div>
<div style={{ fontSize: 11, color: '#5F6B6D' }}>بازدید کل</div>
</div>
<div style={{ background: '#FAF7F0', padding: 12, borderRadius: 8, textAlign: 'center' }}>
<div style={{ fontSize: 20, fontWeight: 900, color: '#2F8F6B' }}>{liveData.users.toLocaleString('fa-IR')}</div>
<div style={{ fontSize: 11, color: '#5F6B6D' }}>کاربر لاگین‌شده</div>
</div>
<div style={{ background: '#FAF7F0', padding: 12, borderRadius: 8, textAlign: 'center' }}>
<div style={{ fontSize: 20, fontWeight: 900, color: '#D49A2A' }}>{liveData.guests.toLocaleString('fa-IR')}</div>
<div style={{ fontSize: 11, color: '#5F6B6D' }}>مهمان</div>
</div>
<div style={{ background: '#FAF7F0', padding: 12, borderRadius: 8, textAlign: 'center' }}>
<div style={{ fontSize: 20, fontWeight: 900, color: '#C46A4D' }}>{liveData.commentsPerMin.toLocaleString('fa-IR')}</div>
<div style={{ fontSize: 11, color: '#5F6B6D' }}>نظر بر دقیقه</div>
</div>
</div>
<div style={{ fontSize: 12, fontWeight: 700, color: BRAND.ink, marginBottom: 8, borderTop: '1px solid #E4DDD2', paddingTop: 16 }}>آخرین رخدادها</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{events.length === 0 ? <div style={{ fontSize: 12, color: '#5F6B6D' }}>هنوز رخدادی ثبت نشده...</div> : null}
{events.slice(0, 6).map((event, idx) => (
<div key={`${event.ts}-${idx}`} style={{ fontSize: 11, color: '#5F6B6D', background: '#F4EFE6', padding: '6px 10px', borderRadius: 6 }}>
<span style={{ fontWeight: 600, color: BRAND.ink }}>{event.actor}</span>: {event.action || event.type} روی {event.target}
</div>
))}
</div>
</div>
);
}
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 <div style={{ color: '#5F6B6D', fontSize: 13, padding: '20px 0', direction: 'rtl' }}>در حال بارگذاری...</div>;
if (error) return <EmptyState title="اتصال به API فعالیت‌ها برقرار نشد" subtitle="فعالیت نمونه نمایش داده نمی‌شود." />;
if (log.length === 0) return <div style={{ color: '#5F6B6D', fontSize: 13, padding: '20px 0', direction: 'rtl' }}>هنوز فعالیتی ثبت نشده است.</div>;
return (
<div>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 12 }}>
<a href={adminApi.activityExportUrl({ limit: 5000 })} style={{ ...pillBtn, background: '#0F6B7318', color: '#0F6B73', textDecoration: 'none', fontSize: 12 }}>خروجی CSV</a>
</div>
{log.slice(0, 20).map(item => (
<div key={item.id} style={{ display: 'flex', gap: 14, padding: '14px 0', borderBottom: '1px solid #E4DDD2', direction: 'rtl', alignItems: 'flex-start' }}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: typeColors[item.type] || '#5F6B6D', marginTop: 6, flexShrink: 0 }} />
<div style={{ flex: 1 }}>
<span style={{ fontWeight: 600, color: '#263238', fontSize: 13 }}>{typeof item.user === 'string' ? item.user : (item.user?.name ?? 'کاربر')}</span>
<span style={{ color: '#5F6B6D', fontSize: 13 }}> — {item.action}: </span>
<span style={{ color: '#0F6B73', fontSize: 13 }}>{item.target}</span>
</div>
<span style={{ fontSize: 11, color: '#5F6B6D', whiteSpace: 'nowrap', flexShrink: 0 }}>
{item.time || (item.createdAt ? new Date(item.createdAt).toLocaleDateString('fa-IR') : '')}
</span>
</div>
))}
</div>
);
}
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 <PanelState loading={loading} error={error} empty={!loading && !error && cats.length === 0} emptyTitle="هنوز آمار دسته‌بندی از API دریافت نشده است" />;
}
return cats.map(c => (
<div key={c.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0', borderBottom: '1px solid #E4DDD2' }}>
<span style={{ fontSize: 13, color: '#263238' }}>{c.name}</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ width: Math.max((c.articleCount || 0) * 2, 20), height: 6, background: c.color || BRAND.primary, borderRadius: 3, opacity: 0.7 }} />
<span style={{ fontSize: 12, color: '#5F6B6D' }}>{c.articleCount ?? 0}</span>
</div>
</div>
));
}
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 <PanelState loading={loading} error={error} empty={!loading && !error && items.length === 0} emptyTitle="هنوز مقاله‌ای برای رتبه‌بندی بازدید وجود ندارد" />;
}
return items.map(article => (
<button key={article.id} onClick={() => navigate('/article/' + article.slug)} style={{ width: '100%', border: 'none', background: 'transparent', borderBottom: '1px solid #E4DDD2', padding: '10px 0', textAlign: 'right', cursor: 'pointer', fontFamily: 'Vazirmatn,sans-serif' }}>
<div style={{ fontSize: 13, fontWeight: 700, color: '#263238', lineHeight: 1.6 }}>{article.title}</div>
<div style={{ fontSize: 11, color: '#5F6B6D', marginTop: 2 }}>{(article.views ?? 0).toLocaleString('fa-IR')} بازدید · {article.status}</div>
</button>
));
}
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 <PanelState loading={loading} error={error} empty={!loading && !error} emptyTitle="هنوز داده‌ای برای آمار پنل ثبت نشده است" />;
}
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 (
<div style={{ display: 'grid', gap: 14, marginBottom: 28 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(170px,1fr))', gap: 14 }}>
{stats.map(([label, value, color]) => (
<div key={label} style={{ background: '#fff', border: '1px solid #E4DDD2', borderRadius: 12, padding: '18px 20px', borderTop: `3px solid ${color}` }}>
<div style={{ fontSize: 24, fontWeight: 900, color, marginBottom: 4 }}>{Number(value ?? 0).toLocaleString('fa-IR')}</div>
<div style={{ fontSize: 12, color: '#5F6B6D' }}>{label}</div>
</div>
))}
</div>
<div style={{ background: '#fff', border: '1px solid #E4DDD2', borderRadius: 12, padding: 16, direction: 'rtl' }}>
<div style={{ fontSize: 13, fontWeight: 800, color: BRAND.ink, marginBottom: 10 }}>پوشش زنده پنل</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
{(metrics.realtime?.eventsByType ?? []).slice(0, 8).map((item) => (
<span key={item.type} style={{ background: '#F4EFE6', border: '1px solid #E4DDD2', borderRadius: 999, padding: '5px 10px', fontSize: 12, color: BRAND.ink }}>{item.type}: {Number(item.count ?? 0).toLocaleString('fa-IR')}</span>
))}
<span style={{ background: '#F4EFE6', border: '1px solid #E4DDD2', borderRadius: 999, padding: '5px 10px', fontSize: 12, color: BRAND.ink }}>اعلان خوانده‌نشده: {Number(metrics.realtime?.unreadNotifications ?? 0).toLocaleString('fa-IR')}</span>
</div>
</div>
</div>
);
}
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) => <h2 style={{ fontSize: 22, fontWeight: 900, color: BRAND.ink, margin: '0 0 20px', direction: 'rtl', fontFamily: "'PelakFA','Vazirmatn',sans-serif" }}>{t}</h2>;
const renderContent = () => {
if (tab === 'create' || editingArticle) return (
<ArticleEditor existing={editingArticle} role={user.role} onSave={() => { setEditingArticle(null); setTab('articles'); }} />
);
if (tab === 'articles') return (
<>
{sectionTitle(user.role === 'reviewer' ? 'مقاله‌های در انتظار بررسی' : 'مدیریت مقاله‌ها')}
<ArticlesManagement role={user.role} onEdit={(a) => { setEditingArticle(a); setTab('edit'); }} />
</>
);
if (tab === 'users' && user.role === 'admin') return <>{sectionTitle('مدیریت کاربران')}<UserManagement /></>;
if (tab === 'authors' && (user.role === 'admin' || user.role === 'editor')) return <>{sectionTitle('مدیریت نویسندگان')}<AuthorsManagement /></>;
if (tab === 'media' && (user.role === 'admin' || user.role === 'editor')) return <>{sectionTitle('کتابخانه رسانه')}<MediaLibrary role={user.role} /></>;
if (tab === 'comments' && (user.role === 'admin' || user.role === 'editor')) return <>{sectionTitle('مدیریت نظرها')}<CommentsModeration /></>;
if (tab === 'analytics') return <>{sectionTitle('تحلیل محتوا')}<Phase9AnalyticsPanel /></>;
if (tab === 'reviews') return <>{sectionTitle('گردش بررسی مقاله‌ها')}<ReviewsPanel /></>;
if (tab === 'newsletter' && (user.role === 'admin' || user.role === 'editor')) return <>{sectionTitle('خبرنامه')}<NewsletterPanel /></>;
if (tab === 'series' && (user.role === 'admin' || user.role === 'editor')) return <>{sectionTitle('سری مقاله‌ها')}<SeriesManagementPanel /></>;
if (tab === 'categories') return <>{sectionTitle('مدیریت دسته‌بندی‌ها')}<CategoryManagement /></>;
if (tab === 'tags' && user.role === 'admin') return <>{sectionTitle('مدیریت برچسب‌ها')}<TagsManagement /></>;
if (tab === 'seo-audit' && user.role === 'admin') return <>{sectionTitle('ممیزی SEO')}<SeoAuditPanel /></>;
if (tab === 'jobs' && (user.role === 'admin' || user.role === 'editor')) return <>{sectionTitle('آگهی‌های شغلی')}<JobsPanel /></>;
if (tab === 'client-errors' && user.role === 'admin') return <>{sectionTitle('خطاهای کلاینت')}<ClientErrorsPanel /></>;
if (tab === 'activity' && user.role === 'admin') return <>{sectionTitle('گزارش فعالیت')}<ActivityLog /></>;
return (
<div style={{ direction: 'rtl' }}>
<h2 style={{ fontSize: 22, fontWeight: 800, color: '#263238', margin: '0 0 6px' }}>
خوش آمدید، {user.name}
</h2>
<p style={{ fontSize: 14, color: '#5F6B6D', margin: '0 0 28px' }}>نقش شما: <RoleBadge role={user.role} /></p>
{(user.role === 'admin' || user.role === 'editor') && <LiveAdminFeed />}
<RoleMetricsPanel role={user.role} />
{user.role === 'admin' && (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }} className="dash-grid">
<div style={{ background: '#fff', border: '1px solid #E4DDD2', borderRadius: 12, padding: 24 }}>
<h3 style={{ fontSize: 15, fontWeight: 700, color: '#263238', margin: '0 0 16px' }}>آخرین فعالیت‌ها</h3>
<ActivityLog />
</div>
<div style={{ background: '#fff', border: '1px solid #E4DDD2', borderRadius: 12, padding: 24 }}>
<h3 style={{ fontSize: 15, fontWeight: 700, color: '#263238', margin: '0 0 16px' }}>آمار دسته‌بندی‌ها</h3>
<DashboardCategoryStats />
</div>
<div style={{ background: '#fff', border: '1px solid #E4DDD2', borderRadius: 12, padding: 24 }}>
<h3 style={{ fontSize: 15, fontWeight: 700, color: '#263238', margin: '0 0 16px' }}>پربازدیدترین مقاله‌ها</h3>
<DashboardTopArticles />
</div>
</div>
)}
</div>
);
};
return (
<div style={{ minHeight: '100vh', display: 'flex', background: 'linear-gradient(135deg,#FAF7F0 0%,#F3EEE4 100%)', fontFamily: 'Vazirmatn,sans-serif', direction: 'rtl' }}>
<AdminSidebar activeTab={tab} onTab={(t) => { setTab(t); setSidebarOpen(false); }} user={user} open={sidebarOpen} onClose={() => setSidebarOpen(false)} />
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
<div style={{ background: 'rgba(250,247,240,0.86)', backdropFilter: 'blur(14px)', borderBottom: '1px solid #E4DDD2', padding: '0 28px', minHeight: 64, display: 'flex', alignItems: 'center', justifyContent: 'space-between', direction: 'rtl', gap: 16, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<button className="admin-mobile-toggle" onClick={() => setSidebarOpen(o => !o)} style={{ background: 'none', border: '1px solid #E4DDD2', borderRadius: 8, padding: '6px 10px', cursor: 'pointer', fontSize: 18, color: BRAND.ink }}>☰</button>
<div>
<span className="admin-topbar-title" style={{ fontSize: 14, fontWeight: 800, color: BRAND.ink }}>پنل مدیریت تکناو</span>
<div style={{ fontSize: 11, color: '#5F6B6D', marginTop: 2 }}>محیط تولید، بازبینی و انتشار مقاله</div>
</div>
</div>
<div style={{ display: 'flex', gap: 10 }}>
<button onClick={() => setTab('create')} style={{ ...pillBtn, background: BRAND.primary, color: '#fff', fontSize: 12 }}>مقاله جدید</button>
<button onClick={() => navigate('/')} style={{ ...pillBtn, background: `${BRAND.primary}12`, color: BRAND.primary, fontSize: 12 }}>بازگشت به سایت</button>
<button onClick={() => { logout(); navigate('/'); }} style={{ ...pillBtn, background: '#C94C4C18', color: '#C94C4C', fontSize: 12 }}>خروج</button>
</div>
</div>
<div style={{ flex: 1, padding: '30px clamp(18px,3vw,38px)', overflowY: 'auto' }}>
{renderContent()}
</div>
</div>
<style>{`
.dash-grid { grid-template-columns: 1fr 1fr; }
@media(max-width:768px){
.dash-grid { grid-template-columns: 1fr !important; }
}
@media(max-width:640px){
.admin-sidebar { display: none !important; }
.admin-sidebar.open { display: flex !important; position: fixed; top: 0; right: 0; bottom: 0; z-index: 300; width: 280px !important; overflow-y: auto; }
.admin-overlay { display: block !important; }
.admin-mobile-toggle { display: flex !important; }
.admin-topbar-title { font-size: 12px !important; }
}
.admin-mobile-toggle { display: none; }
`}</style>
</div>
);
}
export { AdminPanel, DashboardStats, LiveAdminFeed, ActivityLog, DashboardCategoryStats, DashboardTopArticles, RoleMetricsPanel, AdminSidebar };