Files
teknav-bak/app/src/admin/Dashboard.jsx
T
arsam 882d5445a5 Import source: app/src/admin/Dashboard
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:24:00 +03:30

469 lines
27 KiB
React
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };