diff --git a/app/src/admin/UserManagement.jsx b/app/src/admin/UserManagement.jsx
new file mode 100644
index 0000000..c6bdb8e
--- /dev/null
+++ b/app/src/admin/UserManagement.jsx
@@ -0,0 +1,385 @@
+import { useState, useEffect, useMemo } from 'react';
+import { useToast, AuthorAvatar, RoleBadge, ConfirmModal, EmptyState, inputStyle, pillBtn } from '../../teknav-ui.jsx';
+import { adminApi } from '../lib/admin-api.js';
+import { BRAND, PanelState, actionBtn, ADMIN_ROLE_NAMES } from './Shared.jsx';
+
+function UserManagement() {
+ const toast = useToast();
+ const [search, setSearch] = useState('');
+ const [roleFilter, setRoleFilter] = useState('');
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(false);
+ const [selectedUser, setSelectedUser] = useState(null);
+
+ const refresh = () => {
+ setLoading(true);
+ setError(false);
+ return adminApi.listUsers()
+ .then(items => setUsers(items ?? []))
+ .catch(() => { setUsers([]); setError(true); })
+ .finally(() => setLoading(false));
+ };
+ useEffect(() => { refresh(); }, []);
+
+ const filtered = useMemo(() => {
+ let list = users;
+ if (search) { const q = search.toLowerCase(); list = list.filter(u => u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q)); }
+ if (roleFilter) list = list.filter(u => u.role === roleFilter);
+ return list;
+ }, [users, search, roleFilter]);
+
+ const toggleStatus = async (id, status) => {
+ try {
+ await adminApi.updateUser(id, { status: status === 'active' ? 'suspended' : 'active' });
+ toast('وضعیت کاربر تغییر کرد');
+ refresh();
+ } catch {
+ toast('خطا در تغییر وضعیت کاربر', 'error');
+ }
+ };
+ const changeRole = async (id, role) => {
+ try {
+ await adminApi.updateUser(id, { role });
+ toast('نقش کاربر تغییر کرد');
+ refresh();
+ } catch {
+ toast('خطا در تغییر نقش کاربر', 'error');
+ }
+ };
+
+ return (
+
+ {selectedUser && (
+
+
setSelectedUser(null)} style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.45)' }} />
+
+
جزئیات کاربر
+
+
+
+
{selectedUser.name}
+
{selectedUser.email}
+
+
+
+
+ {[['مقالهها', selectedUser.articleCount ?? '—'], ['تاریخ عضویت', selectedUser.joinDate], ['وضعیت', selectedUser.status === 'active' ? 'فعال' : 'غیرفعال']].map(([l, v]) => (
+
+ ))}
+
+
+
+
+ )}
+
+
+ setSearch(e.target.value)} placeholder="جستجوی کاربر..." style={{ ...inputStyle, maxWidth: 240 }} dir="rtl" />
+
+
+
+
+
+
+
+
+ {['نام', 'ایمیل', 'نقش', 'وضعیت', 'مقالهها', 'اقدام'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {filtered.map((u, i) => (
+
+ |
+
+ |
+ {u.email} |
+
+
+ |
+
+ {u.status === 'active' ? 'فعال' : 'غیرفعال'}
+ |
+ {u.articleCount ?? '—'} |
+
+
+
+
+
+ |
+
+ ))}
+
+
+
+
+ {!loading && !error && users.length > 0 && filtered.length === 0 &&
}
+
+
+ );
+}
+
+function CategoryManagement() {
+ const toast = useToast();
+ const [cats, setCats] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(false);
+ const [form, setForm] = useState({ name: '', description: '', color: '#0F6B73' });
+ const [confirm, setConfirm] = useState(null);
+
+ const refresh = () => {
+ setLoading(true);
+ setError(false);
+ return adminApi.listCategories()
+ .then(items => setCats(items ?? []))
+ .catch(() => { setCats([]); setError(true); })
+ .finally(() => setLoading(false));
+ };
+ useEffect(() => { refresh(); }, []);
+ const addCat = async () => {
+ if (!form.name.trim()) { toast('نام دستهبندی الزامی است', 'error'); return; }
+ try {
+ await adminApi.createCategory({ ...form, slug: form.name.replace(/\s+/g, '-'), diagram: 'neural' });
+ toast('دستهبندی اضافه شد'); setForm({ name: '', description: '', color: '#0F6B73' }); refresh();
+ } catch {
+ toast('خطا در افزودن دستهبندی', 'error');
+ }
+ };
+ const del = async (id) => {
+ try {
+ await adminApi.deleteCategory(id);
+ toast('دستهبندی حذف شد'); refresh();
+ } catch {
+ toast('خطا در حذف دستهبندی', 'error');
+ }
+ setConfirm(null);
+ };
+
+ return (
+
+ {confirm &&
del(confirm)} onCancel={() => setConfirm(null)} />}
+
+
افزودن دستهبندی جدید
+
+ setForm(f => ({ ...f, name: e.target.value }))} placeholder="نام دستهبندی" style={{ ...inputStyle, maxWidth: 200 }} dir="rtl" />
+ setForm(f => ({ ...f, description: e.target.value }))} placeholder="توضیحات" style={{ ...inputStyle, flex: 1, minWidth: 200 }} dir="rtl" />
+ setForm(f => ({ ...f, color: e.target.value }))} style={{ width: 48, height: 40, border: '1px solid #E4DDD2', borderRadius: 8, cursor: 'pointer', padding: 2 }} />
+
+
+
+
+ {cats.map(c => (
+
+
+
{c.name}
+
{c.articleCount} مقاله
+
+
+
+ ))}
+
+
+
+ );
+}
+
+function TagsManagement() {
+ const toast = useToast();
+ const [tags, setTags] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(false);
+ const [newTag, setNewTag] = useState('');
+ const [confirm, setConfirm] = useState(null);
+ const [search, setSearch] = useState('');
+
+ const refresh = () => {
+ setLoading(true);
+ setError(false);
+ return adminApi.listTags()
+ .then(items => setTags(items ?? []))
+ .catch(() => { setTags([]); setError(true); })
+ .finally(() => setLoading(false));
+ };
+ useEffect(() => { refresh(); }, []);
+ const addTag = async () => {
+ if (!newTag.trim()) return;
+ try {
+ await adminApi.createTag(newTag);
+ toast('برچسب اضافه شد'); setNewTag(''); refresh();
+ } catch {
+ toast('خطا در افزودن برچسب', 'error');
+ }
+ };
+ const del = async (id) => {
+ try {
+ await adminApi.deleteTag(id);
+ toast('برچسب حذف شد'); refresh();
+ } catch {
+ toast('خطا در حذف برچسب', 'error');
+ }
+ setConfirm(null);
+ };
+
+ const filtered = search ? tags.filter(t => t.name.includes(search)) : tags;
+
+ return (
+
+ );
+}
+
+function AuthorsManagement() {
+ const toast = useToast();
+ const [authors, setAuthors] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(false);
+ const [form, setForm] = useState({ name: '', slug: '', specialty: '', bio: '', initials: '', color: '#0F6B73', social: {} });
+ const [editing, setEditing] = useState(null);
+ const [confirm, setConfirm] = useState(null);
+
+ const refresh = () => {
+ setLoading(true);
+ setError(false);
+ return adminApi.listAuthors()
+ .then(items => setAuthors(items ?? []))
+ .catch(() => { setAuthors([]); setError(true); })
+ .finally(() => setLoading(false));
+ };
+ useEffect(() => { refresh(); }, []);
+
+ const save = async () => {
+ if (!form.name.trim() || !form.slug.trim() || !form.specialty.trim()) {
+ toast('نام، نام مستعار و تخصص الزامی است', 'error');
+ return;
+ }
+ try {
+ if (editing) {
+ await adminApi.updateAuthor(editing, form);
+ toast('نویسنده ویرایش شد');
+ } else {
+ await adminApi.createAuthor(form);
+ toast('نویسنده اضافه شد');
+ }
+ setForm({ name: '', slug: '', specialty: '', bio: '', initials: '', color: '#0F6B73', social: {} });
+ setEditing(null);
+ refresh();
+ } catch {
+ toast('خطا در ذخیره نویسنده', 'error');
+ }
+ };
+
+ const del = async (id) => {
+ try {
+ await adminApi.deleteAuthor(id);
+ toast('نویسنده حذف شد');
+ refresh();
+ } catch {
+ toast('خطا در حذف نویسنده', 'error');
+ }
+ setConfirm(null);
+ };
+
+ const edit = (author) => {
+ setForm({
+ name: author.name,
+ slug: author.slug,
+ specialty: author.specialty,
+ bio: author.bio || '',
+ initials: author.initials || '',
+ color: author.color || '#0F6B73',
+ social: author.social || {},
+ });
+ setEditing(author.id);
+ };
+
+ const cancel = () => {
+ setForm({ name: '', slug: '', specialty: '', bio: '', initials: '', color: '#0F6B73', social: {} });
+ setEditing(null);
+ };
+
+ return (
+
+ {confirm &&
del(confirm)} onCancel={() => setConfirm(null)} />}
+
+
+ {authors.map(a => (
+
+
+
+ {a.initials || a.name.slice(0, 2)}
+
+
+
{a.name}
+
{a.specialty}
+
+
+ {a.bio &&
{a.bio}
}
+
+ {a.articleCount || 0} مقاله • slug: {a.slug}
+
+
+
+
+
+
+ ))}
+
+
+
+ );
+}
+
+export { UserManagement, CategoryManagement, TagsManagement, AuthorsManagement };