diff --git a/app/src/admin/AnalyticsPanel.jsx b/app/src/admin/AnalyticsPanel.jsx
new file mode 100644
index 0000000..da19c1e
--- /dev/null
+++ b/app/src/admin/AnalyticsPanel.jsx
@@ -0,0 +1,375 @@
+import { useState, useEffect } from 'react';
+import { useNav, useToast, EmptyState, pillBtn, inputStyle } from '../../teknav-ui.jsx';
+import { adminApi } from '../lib/admin-api.js';
+import { BRAND, PanelState, SEO_SEVERITY, actionBtn } from './Shared.jsx';
+
+function SeoAuditPanel() {
+ const { navigate } = useNav();
+ const [audit, setAudit] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(false);
+ const [page, setPage] = useState(0);
+ const PAGE_SIZE = 15;
+
+ useEffect(() => {
+ setLoading(true);
+ setError(false);
+ adminApi.getSeoAudit()
+ .then(setAudit)
+ .catch(() => setError(true))
+ .finally(() => setLoading(false));
+ }, []);
+
+ if (loading || error || !audit) return ;
+
+ const issues = (audit.items ?? []).filter(item => item.missing?.length || item.needsRefresh);
+ const totalIssues = issues.reduce((sum, item) => sum + (item.missing?.length ?? 0) + (item.needsRefresh ? 1 : 0), 0);
+ const pageItems = issues.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
+ const totalPages = Math.ceil(issues.length / PAGE_SIZE);
+
+ const getSeverity = (key) => {
+ if (['metaDescription', 'ogTitle', 'ogDescription'].includes(key)) return 'error';
+ if (['keywords', 'ogImage', 'canonicalPath'].includes(key)) return 'warning';
+ return 'info';
+ };
+
+ return (
+
+
+
+ {totalIssues.toLocaleString('fa-IR')} مشکل یافت شد در {issues.length.toLocaleString('fa-IR')} مقاله
+
+ {issues.length === 0 &&
✓ همه مقالهها سالم هستند}
+
+ {issues.length > 0 && (
+
+
+
+
+ {['مقاله', 'مشکلات', 'شدت'].map(h => | {h} | )}
+
+
+
+ {pageItems.map((item, i) => {
+ const allKeys = [...(item.missing ?? []), ...(item.needsRefresh ? ['needsRefresh'] : [])];
+ const worstSeverity = allKeys.some(k => getSeverity(k) === 'error') ? 'error' : allKeys.some(k => getSeverity(k) === 'warning') ? 'warning' : 'info';
+ const sev = SEO_SEVERITY[worstSeverity];
+ return (
+
+ |
+
+ |
+
+
+ {allKeys.map(k => {
+ const s = SEO_SEVERITY[getSeverity(k)];
+ return {k};
+ })}
+
+ |
+
+ {sev.label}
+ |
+
+ );
+ })}
+
+
+ {totalPages > 1 && (
+
+
+ صفحه {(page + 1).toLocaleString('fa-IR')} از {totalPages.toLocaleString('fa-IR')}
+
+
+ )}
+
+ )}
+
+ );
+}
+
+function EngagementAnalyticsPanel() {
+ const [engagement, setEngagement] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(false);
+
+ useEffect(() => {
+ setLoading(true);
+ setError(false);
+ adminApi.getEngagementAnalytics()
+ .then(setEngagement)
+ .catch(() => setError(true))
+ .finally(() => setLoading(false));
+ }, []);
+
+ if (loading || error || !engagement) return ;
+
+ const byType = engagement.byType ?? [];
+ const totalEvents = byType.reduce((sum, item) => sum + Number(item.count ?? 0), 0);
+ const topArticles = (engagement.topArticles ?? []).slice(0, 5);
+ const weeklyData = engagement.weeklyData ?? [];
+
+ const barMax = Math.max(...weeklyData.map(d => d.count ?? 0), 1);
+
+ return (
+
+ {weeklyData.length > 0 && (
+
+
رویدادهای ۷ روز گذشته
+
+ {weeklyData.map((d, i) => (
+
+
{Number(d.count ?? 0).toLocaleString('fa-IR')}
+
+
{d.date ?? ''}
+
+ ))}
+
+
+ )}
+
+
+
انواع رویداد
+ {byType.length === 0 ?
: (
+
+
+
+ {['نوع رویداد', 'تعداد', 'درصد'].map(h => | {h} | )}
+
+
+
+ {byType.map(item => (
+
+ | {item.type} |
+ {Number(item.count ?? 0).toLocaleString('fa-IR')} |
+
+ {totalEvents > 0 ? `${Math.round(Number(item.count ?? 0) / totalEvents * 100)}٪` : '—'}
+ |
+
+ ))}
+
+
+ )}
+
+
+ {topArticles.length > 0 && (
+
+
۵ مقاله برتر بر اساس تعامل
+ {topArticles.map((article, i) => (
+
+ {article.title}
+ {Number(article.eventCount ?? 0).toLocaleString('fa-IR')} رویداد
+
+ ))}
+
+ )}
+
+ );
+}
+
+function Phase9AnalyticsPanel() {
+ const [subTab, setSubTab] = useState('overview');
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(false);
+ useEffect(() => {
+ let cancelled = false;
+ adminApi.getAnalytics()
+ .then(analytics => { if (!cancelled) setData(analytics); })
+ .catch(() => { if (!cancelled) setError(true); })
+ .finally(() => { if (!cancelled) setLoading(false); });
+ return () => { cancelled = true; };
+ }, []);
+
+ const tabs = [
+ { id: 'overview', label: 'مروری' },
+ { id: 'engagement', label: 'تعامل' },
+ ];
+
+ const tabBtn = (id) => ({
+ background: subTab === id ? BRAND.primary : 'transparent',
+ color: subTab === id ? '#fff' : '#5F6B6D',
+ border: `1px solid ${subTab === id ? BRAND.primary : '#E4DDD2'}`,
+ borderRadius: 8, padding: '6px 16px', fontSize: 12, fontWeight: 600,
+ cursor: 'pointer', fontFamily: 'Vazirmatn,sans-serif', transition: 'all 0.15s',
+ });
+
+ return (
+
+
+ {tabs.map(t => )}
+
+
+ {subTab === 'engagement' ?
: (
+ <>
+ {(loading || error || !data) ?
: (
+
+
+ {[['نظرها', data.comments], ['ذخیرهها', data.saved], ['مشترکان خبرنامه', data.subscribers]].map(([label, value]) => (
+
+
{label}
+
{Number(value ?? 0).toLocaleString('fa-IR')}
+
+ ))}
+
+
+
مقالههای برتر
+ {(data.topArticles ?? []).length === 0 ?
: data.topArticles.map((article) => (
+
+ {article.title}
+ {Number(article.views ?? 0).toLocaleString('fa-IR')} بازدید
+
+ ))}
+
+
+ )}
+ >
+ )}
+
+ );
+}
+
+function JobsPanel() {
+ const [items, setItems] = useState([]);
+ const [filter, setFilter] = useState('');
+ const [loading, setLoading] = useState(true);
+ const [actionMsg, setActionMsg] = useState('');
+
+ const load = (status = filter) => {
+ setLoading(true);
+ adminApi.listJobs(status ? { status } : {}).then((data) => {
+ setItems(data);
+ setLoading(false);
+ }).catch(() => setLoading(false));
+ };
+
+ useEffect(() => { load(); }, []);
+
+ const moderate = async (id, status) => {
+ await adminApi.moderateJob(id, { status });
+ setActionMsg(`وضعیت به "${status}" تغییر یافت`);
+ setTimeout(() => setActionMsg(''), 2500);
+ load();
+ };
+
+ const statusLabel = { pending: 'در انتظار', approved: 'تأیید شده', rejected: 'رد شده' };
+ const statusColor = { pending: '#D49A2A', approved: '#0F6B73', rejected: '#C46A4D' };
+
+ return (
+
+
+
+ {actionMsg && {actionMsg}}
+
+ {loading &&
در حال بارگذاری...
}
+ {!loading && items.length === 0 &&
هیچ آگهی یافت نشد
}
+ {!loading && items.length > 0 && (
+
+
+
+
+ {['شرکت', 'عنوان', 'موقعیت', 'وضعیت', 'تاریخ', 'عملیات'].map((h) => (
+ | {h} |
+ ))}
+
+
+
+ {items.map((job) => (
+
+ | {job.company} |
+ {job.title} |
+ {job.location}{job.remote ? ' (ریموت)' : ''} |
+
+
+ {statusLabel[job.status] ?? job.status}
+
+ |
+ {new Date(job.createdAt).toLocaleDateString('fa-IR')} |
+
+
+ {job.status !== 'approved' && (
+
+ )}
+ {job.status !== 'rejected' && (
+
+ )}
+
+ |
+
+ ))}
+
+
+
+ )}
+
+ );
+}
+
+function ClientErrorsPanel() {
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ adminApi.getClientErrors().then((d) => { setData(d); setLoading(false); }).catch(() => setLoading(false));
+ }, []);
+
+ if (loading) return در حال بارگذاری...
;
+ if (!data) return خطا در بارگذاری اطلاعات
;
+
+ const maxCount = Math.max(...(data.topErrors?.map((e) => e.count) ?? [1]), 1);
+
+ return (
+
+ {data.dailyCounts?.length > 0 && (
+
+
خطاها در ۳۰ روز گذشته
+
+ {data.dailyCounts.slice(-30).map((d) => {
+ const h = Math.max(4, Math.round((d.count / maxCount) * 56));
+ return (
+
+ );
+ })}
+
+
+ )}
+
+
+
پرتکرارترین خطاها
+ {(data.topErrors ?? []).length === 0 &&
خطایی ثبت نشده است
}
+ {(data.topErrors ?? []).map((err, i) => (
+
+
{err.message}
+
+ {err.count} بار
+ {new Date(err.lastSeen).toLocaleDateString('fa-IR')}
+
+
+ ))}
+
+
+
+
آخرین خطاهای کلاینت
+ {(data.recent ?? []).length === 0 &&
خطایی ثبت نشده است
}
+ {(data.recent ?? []).map((err) => (
+
+
{err.message}
+
+ {err.url && {err.url}}
+ {new Date(err.createdAt).toLocaleDateString('fa-IR')}
+
+
+ ))}
+
+
+ );
+}
+
+export { SeoAuditPanel, EngagementAnalyticsPanel, Phase9AnalyticsPanel, JobsPanel, ClientErrorsPanel };