edtech / apps /admin /src /pages /super-admin /UsersManager.tsx
CognxSafeTrack
feat(i18n): complete super-admin i18n — all 11 pages fully translated
b92ea37
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useAuth } from '@/lib/auth';
import { api } from '@/lib/api';
import { useToast } from '@/hooks/useToast';
import { Search, KeyRound } from 'lucide-react';
const ROLE_COLORS: Record<string, string> = {
STUDENT: 'bg-slate-700 text-slate-300',
ORG_MEMBER: 'bg-blue-900/60 text-blue-300',
ORG_ADMIN: 'bg-violet-900/60 text-violet-300',
ADMIN: 'bg-amber-900/60 text-amber-300',
SUPER_ADMIN: 'bg-red-900/60 text-red-300',
};
export default function UsersManager() {
const { t } = useTranslation();
const { token } = useAuth();
const toast = useToast();
const [users, setUsers] = useState<any[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const [loading, setLoading] = useState(true);
const LIMIT = 20;
async function load() {
if (!token) return;
setLoading(true);
try {
const params = new URLSearchParams({ page: String(page), limit: String(LIMIT) });
if (search) params.set('search', search);
const data = await api.get(`/v1/super-admin/users?${params}`, token);
setUsers(data.data ?? []);
setTotal(data.total ?? 0);
} catch { toast.error(t('super_admin.err_load_users')); }
finally { setLoading(false); }
}
useEffect(() => { load(); }, [page, token]);
async function handleSearch(e: React.FormEvent) {
e.preventDefault();
setPage(1);
load();
}
async function handleRoleChange(userId: string, role: string) {
try {
await api.patch(`/v1/super-admin/users/${userId}/role`, { role }, token);
toast.success(t('super_admin.role_updated'));
load();
} catch { toast.error(t('super_admin.err_role_change')); }
}
async function handleResetPassword(user: any) {
if (!user.email) { toast.error(t('super_admin.reset_no_email')); return; }
if (!confirm(t('super_admin.reset_confirm', { email: user.email }))) return;
try {
await api.post(`/v1/super-admin/users/${user.id}/reset-password`, {}, token);
toast.success(t('super_admin.reset_sent', { email: user.email }));
} catch { toast.error(t('super_admin.err_reset_password')); }
}
return (
<div className="space-y-4">
<div>
<h1 className="text-xl font-bold text-white">{t('super_admin.users_title')}</h1>
<p className="text-sm text-slate-400 mt-0.5">{t('super_admin.users_total', { count: total })}</p>
</div>
<form onSubmit={handleSearch} className="flex gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder={t('super_admin.user_search_placeholder')}
className="w-full pl-9 pr-4 py-2 bg-slate-900 border border-slate-700 rounded-lg text-sm text-white placeholder-slate-500 focus:outline-none focus:border-violet-500"
/>
</div>
<button type="submit" className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-white text-sm rounded-lg transition-colors">{t('super_admin.org_search_btn')}</button>
</form>
<div className="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
{loading ? (
<div className="p-8 text-center text-slate-500 animate-pulse">{t('super_admin.org_loading')}</div>
) : users.length === 0 ? (
<div className="p-8 text-center text-slate-500">{t('super_admin.user_empty')}</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-800">
<th className="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{t('super_admin.col_user')}</th>
<th className="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{t('super_admin.col_organization')}</th>
<th className="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{t('super_admin.col_role')}</th>
<th className="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{t('super_admin.col_created_at')}</th>
<th className="px-4 py-3" />
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{users.map(user => (
<tr key={user.id} className="hover:bg-slate-800/50 transition-colors">
<td className="px-4 py-3">
<div className="font-medium text-white">{user.name || '—'}</div>
<div className="text-xs text-slate-500">{user.email || user.phone || '—'}</div>
</td>
<td className="px-4 py-3 text-slate-300 text-xs">{user.organization?.name || '—'}</td>
<td className="px-4 py-3">
<span className={`text-xs px-2 py-0.5 rounded-full ${ROLE_COLORS[user.role] ?? 'bg-slate-700 text-slate-300'}`}>{user.role}</span>
</td>
<td className="px-4 py-3 text-xs text-slate-400">{new Date(user.createdAt).toLocaleDateString()}</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<select
defaultValue={user.role}
onChange={e => handleRoleChange(user.id, e.target.value)}
className="text-xs bg-slate-800 border border-slate-700 rounded px-2 py-1 text-slate-300 focus:outline-none focus:border-violet-500"
>
{['STUDENT', 'ORG_MEMBER', 'ORG_ADMIN', 'ADMIN', 'SUPER_ADMIN'].map(r => <option key={r} value={r}>{r}</option>)}
</select>
<button
onClick={() => handleResetPassword(user)}
title={t('super_admin.btn_reset_password')}
className="p-1.5 text-slate-400 hover:text-violet-300 hover:bg-slate-700 rounded transition-colors"
>
<KeyRound className="w-3.5 h-3.5" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{total > LIMIT && (
<div className="flex items-center justify-between text-sm text-slate-400">
<span>{t('super_admin.pagination_info', { from: ((page - 1) * LIMIT) + 1, to: Math.min(page * LIMIT, total), total })}</span>
<div className="flex gap-2">
<button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1} className="px-3 py-1.5 bg-slate-900 border border-slate-700 rounded-lg disabled:opacity-40 hover:bg-slate-800 transition-colors">{t('super_admin.prev')}</button>
<button onClick={() => setPage(p => p + 1)} disabled={page * LIMIT >= total} className="px-3 py-1.5 bg-slate-900 border border-slate-700 rounded-lg disabled:opacity-40 hover:bg-slate-800 transition-colors">{t('super_admin.next')}</button>
</div>
</div>
)}
</div>
);
}