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]) => ( +
+
{l}
+
{v}
+
+ ))} +
+ +
+
+ )} + +
+ setSearch(e.target.value)} placeholder="جستجوی کاربر..." style={{ ...inputStyle, maxWidth: 240 }} dir="rtl" /> + +
+ +
+
+ + + + {['نام', 'ایمیل', 'نقش', 'وضعیت', 'مقاله‌ها', 'اقدام'].map(h => ( + + ))} + + + + {filtered.map((u, i) => ( + + + + + + + + + ))} + +
{h}
+
+ + {u.name} +
+
{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 ( +
+ {confirm && del(confirm)} onCancel={() => setConfirm(null)} />} +
+ setNewTag(e.target.value)} onKeyDown={e => e.key === 'Enter' && addTag()} placeholder="برچسب جدید..." style={{ ...inputStyle, maxWidth: 220 }} dir="rtl" /> + + setSearch(e.target.value)} placeholder="جستجوی برچسب..." style={{ ...inputStyle, maxWidth: 200 }} dir="rtl" /> +
+
+ {filtered.map(t => ( +
+ {t.name} + ({t.count}) + +
+ ))} +
+ + {!loading && !error && tags.length > 0 && filtered.length === 0 && } +
+ ); +} + +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)} />} +
+

+ {editing ? 'ویرایش نویسنده' : 'افزودن نویسنده جدید'} +

+
+ setForm(f => ({ ...f, name: e.target.value }))} placeholder="نام نویسنده *" style={inputStyle} dir="rtl" /> + setForm(f => ({ ...f, slug: e.target.value }))} placeholder="نام مستعار (slug) *" style={inputStyle} dir="rtl" /> +
+
+ setForm(f => ({ ...f, specialty: e.target.value }))} placeholder="تخصص *" style={inputStyle} dir="rtl" /> +
+ setForm(f => ({ ...f, initials: e.target.value }))} placeholder="حروف اول" style={{ ...inputStyle, flex: 1 }} dir="rtl" maxLength={3} /> + setForm(f => ({ ...f, color: e.target.value }))} style={{ width: 48, height: 40, border: '1px solid #E4DDD2', borderRadius: 8, cursor: 'pointer', padding: 2 }} /> +
+
+