All checks were successful
Dev CI / Backend — Lint (push) Successful in 10m23s
Dev CI / Backend — Unit Tests (push) Successful in 10m17s
Dev CI / Frontend — Lint & Type-check (push) Successful in 11m3s
Dev CI / Frontend — Unit Tests (push) Successful in 10m33s
Dev CI / Notify Failure (push) Has been skipped
471 lines
18 KiB
TypeScript
471 lines
18 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useTranslations } from 'next-intl';
|
|
import { getAllUsers, updateAdminUser, deleteAdminUser } from '@/lib/api/admin';
|
|
import { createUser } from '@/lib/api/users';
|
|
import { getAllOrganizations } from '@/lib/api/admin';
|
|
import type { UserRole } from '@/types/api';
|
|
import { PageHeader } from '@/components/ui/PageHeader';
|
|
|
|
interface User {
|
|
id: string;
|
|
email: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
role: UserRole;
|
|
organizationId: string;
|
|
organizationName?: string;
|
|
isActive: boolean;
|
|
createdAt: string;
|
|
}
|
|
|
|
interface Organization {
|
|
id: string;
|
|
name: string;
|
|
}
|
|
|
|
export default function AdminUsersPage() {
|
|
const t = useTranslations('dashboard.admin.users');
|
|
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
const [showEditModal, setShowEditModal] = useState(false);
|
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
|
|
// Form state
|
|
const [formData, setFormData] = useState<{
|
|
email: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
role: UserRole;
|
|
organizationId: string;
|
|
password: string;
|
|
}>({
|
|
email: '',
|
|
firstName: '',
|
|
lastName: '',
|
|
role: 'USER',
|
|
organizationId: '',
|
|
password: '',
|
|
});
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, []);
|
|
|
|
const fetchData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const [usersResponse, orgsResponse] = await Promise.all([
|
|
getAllUsers(),
|
|
getAllOrganizations(),
|
|
]);
|
|
|
|
setUsers(usersResponse.users || []);
|
|
setOrganizations(orgsResponse.organizations || []);
|
|
setError(null);
|
|
} catch (err: any) {
|
|
setError(err.message || t('loadError'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCreate = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
try {
|
|
await createUser(formData);
|
|
await fetchData();
|
|
setShowCreateModal(false);
|
|
resetForm();
|
|
} catch (err: any) {
|
|
alert(err.message || t('createError'));
|
|
}
|
|
};
|
|
|
|
const handleUpdate = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!selectedUser) return;
|
|
|
|
try {
|
|
await updateAdminUser(selectedUser.id, {
|
|
firstName: formData.firstName,
|
|
lastName: formData.lastName,
|
|
role: formData.role,
|
|
isActive: selectedUser.isActive,
|
|
});
|
|
await fetchData();
|
|
setShowEditModal(false);
|
|
setSelectedUser(null);
|
|
resetForm();
|
|
} catch (err: any) {
|
|
alert(err.message || t('updateError'));
|
|
}
|
|
};
|
|
|
|
const handleDelete = async () => {
|
|
if (!selectedUser) return;
|
|
|
|
try {
|
|
await deleteAdminUser(selectedUser.id);
|
|
await fetchData();
|
|
setShowDeleteConfirm(false);
|
|
setSelectedUser(null);
|
|
} catch (err: any) {
|
|
alert(err.message || t('deleteError'));
|
|
}
|
|
};
|
|
|
|
const resetForm = () => {
|
|
setFormData({
|
|
email: '',
|
|
firstName: '',
|
|
lastName: '',
|
|
role: 'USER',
|
|
organizationId: '',
|
|
password: '',
|
|
});
|
|
};
|
|
|
|
const openEditModal = (user: User) => {
|
|
setSelectedUser(user);
|
|
setFormData({
|
|
email: user.email,
|
|
firstName: user.firstName,
|
|
lastName: user.lastName,
|
|
role: user.role,
|
|
organizationId: user.organizationId,
|
|
password: '',
|
|
});
|
|
setShowEditModal(true);
|
|
};
|
|
|
|
const openDeleteConfirm = (user: User) => {
|
|
setSelectedUser(user);
|
|
setShowDeleteConfirm(true);
|
|
};
|
|
|
|
const getRoleLabel = (role: string) => {
|
|
const allowed = ['USER', 'MANAGER', 'ADMIN', 'VIEWER'];
|
|
if (allowed.includes(role)) {
|
|
return t(`roles.${role}` as any);
|
|
}
|
|
return role;
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-96">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
|
<p className="mt-4 text-gray-600">{t('loading')}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<PageHeader
|
|
title={t('title')}
|
|
description={t('subtitle')}
|
|
actions={
|
|
<button
|
|
onClick={() => setShowCreateModal(true)}
|
|
className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors"
|
|
>
|
|
{t('create')}
|
|
</button>
|
|
}
|
|
/>
|
|
|
|
{/* Error Message */}
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Users Table */}
|
|
<div className="bg-white rounded-lg shadow overflow-hidden">
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
{t('table.user')}
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
{t('table.email')}
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
{t('table.role')}
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
{t('table.organization')}
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
{t('table.status')}
|
|
</th>
|
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
{t('table.actions')}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{users.map(user => (
|
|
<tr key={user.id} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm font-medium text-gray-900">
|
|
{user.firstName} {user.lastName}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm text-gray-500">{user.email}</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
|
|
user.role === 'ADMIN' ? 'bg-purple-100 text-purple-800' :
|
|
user.role === 'MANAGER' ? 'bg-blue-100 text-blue-800' :
|
|
'bg-gray-100 text-gray-800'
|
|
}`}>
|
|
{getRoleLabel(user.role)}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{user.organizationName || user.organizationId}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
|
|
user.isActive ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
|
}`}>
|
|
{user.isActive ? t('active') : t('inactive')}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
|
|
<button
|
|
onClick={() => openEditModal(user)}
|
|
className="text-blue-600 hover:text-blue-900"
|
|
>
|
|
{t('edit')}
|
|
</button>
|
|
<button
|
|
onClick={() => openDeleteConfirm(user)}
|
|
className="text-red-600 hover:text-red-900"
|
|
>
|
|
{t('delete')}
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Create Modal */}
|
|
{showCreateModal && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div className="bg-white rounded-lg p-6 max-w-md w-full">
|
|
<h2 className="text-xl font-bold mb-4">{t('modal.createTitle')}</h2>
|
|
<form onSubmit={handleCreate} className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">{t('modal.email')}</label>
|
|
<input
|
|
type="email"
|
|
required
|
|
value={formData.email}
|
|
onChange={e => setFormData({ ...formData, email: e.target.value })}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">{t('modal.firstName')}</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.firstName}
|
|
onChange={e => setFormData({ ...formData, firstName: e.target.value })}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">{t('modal.lastName')}</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.lastName}
|
|
onChange={e => setFormData({ ...formData, lastName: e.target.value })}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">{t('modal.role')}</label>
|
|
<select
|
|
value={formData.role}
|
|
onChange={e => setFormData({ ...formData, role: e.target.value as UserRole })}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
>
|
|
<option value="USER">{t('roles.USER')}</option>
|
|
<option value="MANAGER">{t('roles.MANAGER')}</option>
|
|
<option value="ADMIN">{t('roles.ADMIN')}</option>
|
|
<option value="VIEWER">{t('roles.VIEWER')}</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">{t('modal.organization')}</label>
|
|
<select
|
|
required
|
|
value={formData.organizationId}
|
|
onChange={e => setFormData({ ...formData, organizationId: e.target.value })}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
>
|
|
<option value="">{t('modal.selectOrganization')}</option>
|
|
{organizations.map(org => (
|
|
<option key={org.id} value={org.id}>
|
|
{org.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">
|
|
{t('modal.password')}
|
|
</label>
|
|
<input
|
|
type="password"
|
|
value={formData.password}
|
|
onChange={e => setFormData({ ...formData, password: e.target.value })}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div className="flex justify-end space-x-2 pt-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setShowCreateModal(false);
|
|
resetForm();
|
|
}}
|
|
className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
|
|
>
|
|
{t('modal.cancel')}
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|
>
|
|
{t('modal.create')}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Edit Modal */}
|
|
{showEditModal && selectedUser && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div className="bg-white rounded-lg p-6 max-w-md w-full">
|
|
<h2 className="text-xl font-bold mb-4">{t('modal.editTitle')}</h2>
|
|
<form onSubmit={handleUpdate} className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">{t('modal.emailReadOnly')}</label>
|
|
<input
|
|
type="email"
|
|
disabled
|
|
value={formData.email}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 bg-gray-100 rounded-md shadow-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">{t('modal.firstName')}</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.firstName}
|
|
onChange={e => setFormData({ ...formData, firstName: e.target.value })}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">{t('modal.lastName')}</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={formData.lastName}
|
|
onChange={e => setFormData({ ...formData, lastName: e.target.value })}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">{t('modal.role')}</label>
|
|
<select
|
|
value={formData.role}
|
|
onChange={e => setFormData({ ...formData, role: e.target.value as UserRole })}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
|
>
|
|
<option value="USER">{t('roles.USER')}</option>
|
|
<option value="MANAGER">{t('roles.MANAGER')}</option>
|
|
<option value="ADMIN">{t('roles.ADMIN')}</option>
|
|
<option value="VIEWER">{t('roles.VIEWER')}</option>
|
|
</select>
|
|
</div>
|
|
<div className="flex justify-end space-x-2 pt-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setShowEditModal(false);
|
|
setSelectedUser(null);
|
|
resetForm();
|
|
}}
|
|
className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
|
|
>
|
|
{t('modal.cancel')}
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|
>
|
|
{t('modal.update')}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Delete Confirmation */}
|
|
{showDeleteConfirm && selectedUser && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div className="bg-white rounded-lg p-6 max-w-md w-full">
|
|
<h2 className="text-xl font-bold mb-4 text-red-600">{t('deleteConfirm.title')}</h2>
|
|
<p className="text-gray-700 mb-6">
|
|
{t('deleteConfirm.message', { firstName: selectedUser.firstName, lastName: selectedUser.lastName })}
|
|
</p>
|
|
<div className="flex justify-end space-x-2">
|
|
<button
|
|
onClick={() => {
|
|
setShowDeleteConfirm(false);
|
|
setSelectedUser(null);
|
|
}}
|
|
className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50"
|
|
>
|
|
{t('deleteConfirm.cancel')}
|
|
</button>
|
|
<button
|
|
onClick={handleDelete}
|
|
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700"
|
|
>
|
|
{t('deleteConfirm.confirm')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|