File size: 8,890 Bytes
6282d86
b92ea37
6282d86
 
 
b8629ec
6282d86
 
 
 
 
 
 
 
 
 
b92ea37
6282d86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b92ea37
6282d86
 
 
 
 
 
 
 
 
 
 
 
 
 
b92ea37
6282d86
b92ea37
6282d86
 
b8629ec
b92ea37
 
b8629ec
 
b92ea37
 
b8629ec
 
6282d86
 
 
b92ea37
 
6282d86
 
 
 
 
 
 
 
b92ea37
6282d86
 
 
b92ea37
6282d86
 
 
 
b92ea37
6282d86
b92ea37
6282d86
 
 
 
 
b92ea37
 
 
 
6282d86
 
 
 
 
 
 
 
 
 
 
 
 
 
b92ea37
6282d86
b8629ec
 
 
 
 
 
 
 
 
 
b92ea37
b8629ec
 
 
 
 
6282d86
 
 
 
 
 
 
 
 
 
 
b92ea37
6282d86
b92ea37
 
6282d86
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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>
    );
}