247 lines
10 KiB
TypeScript
247 lines
10 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useAuth } from '../../contexts/AuthContext';
|
|
import { useTenant } from '../../contexts/TenantContext';
|
|
import toast from 'react-hot-toast';
|
|
import { PlusIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
|
import UserModal from '../../components/modals/UserModal';
|
|
import logger from '../../utils/logger';
|
|
|
|
|
|
interface User {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
role: 'superuser' | 'tenant_admin' | 'evaluator';
|
|
isActive: boolean;
|
|
}
|
|
|
|
const API_URL = globalThis.__APP_ENV__?.VITE_API_BASE_URL || 'https://duxiter.azurianlab.com/api';
|
|
|
|
const TenantUsers = () => {
|
|
const { tenant } = useTenant();
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [search, setSearch] = useState('');
|
|
const [showAddModal, setShowAddModal] = useState(false);
|
|
const [showEditModal, setShowEditModal] = useState(false);
|
|
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
|
const [roleFilter, setRoleFilter] = useState<string>('');
|
|
const [statusFilter, setStatusFilter] = useState<string>('');
|
|
|
|
// Fetch users
|
|
const fetchUsers = async () => {
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
const queryParams = new URLSearchParams();
|
|
if (search) queryParams.append('search', search);
|
|
if (roleFilter) queryParams.append('role', roleFilter);
|
|
if (statusFilter) queryParams.append('status', statusFilter);
|
|
|
|
const response = await fetch(`${API_URL}/users?${queryParams.toString()}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch users');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (!data.success) {
|
|
throw new Error(data.message || 'Failed to fetch users');
|
|
}
|
|
|
|
setUsers(data.users || []);
|
|
} catch (error) {
|
|
logger.error('Error fetching users:', error);
|
|
toast.error(error instanceof Error ? error.message : 'Failed to load users');
|
|
setUsers([]); // Ensure users is an array to prevent .map error
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchUsers();
|
|
}, [search, roleFilter, statusFilter]);
|
|
|
|
// Toggle user status
|
|
const toggleUserStatus = async (userId: string) => {
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
const response = await fetch(`${API_URL}/users/${userId}/toggle-status`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to update user status');
|
|
}
|
|
|
|
const data = await response.json();
|
|
setUsers(users.map(user =>
|
|
user.id === userId ? { ...user, isActive: data.isActive } : user
|
|
));
|
|
toast.success('User status updated successfully');
|
|
} catch (error) {
|
|
logger.error('Error updating user status:', error);
|
|
toast.error('Failed to update user status');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="px-4 sm:px-6 lg:px-8 text-gray-600 dark:text-gray-400">
|
|
<div className="sm:flex sm:items-center">
|
|
<div className="sm:flex-auto">
|
|
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white">Users</h1>
|
|
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
Manage users in your organization
|
|
</p>
|
|
</div>
|
|
<div className="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowAddModal(true)}
|
|
className="inline-flex items-center justify-center rounded-md border border-transparent bg-primary-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 sm:w-auto"
|
|
>
|
|
<PlusIcon className="-ml-1 mr-2 h-5 w-5" />
|
|
Add User
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="mt-8 flex flex-col sm:flex-row gap-4">
|
|
<div className="flex-1">
|
|
<div className="relative rounded-md shadow-sm">
|
|
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
|
<MagnifyingGlassIcon className="h-5 w-5 text-gray-500" aria-hidden="true" />
|
|
</div>
|
|
<input
|
|
type="text"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
placeholder="Search users..."
|
|
className="block w-full rounded-md border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 pl-10 focus:border-primary-500 focus:ring-primary-500 sm:text-sm text-gray-900 dark:text-white"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="sm:w-48">
|
|
<select
|
|
value={roleFilter}
|
|
onChange={(e) => setRoleFilter(e.target.value)}
|
|
className="block w-full rounded-md border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 focus:border-primary-500 focus:ring-primary-500 sm:text-sm text-gray-900 dark:text-white"
|
|
>
|
|
<option value="">All Roles</option>
|
|
<option value="tenant_admin">Admin</option>
|
|
<option value="evaluator">Evaluator</option>
|
|
</select>
|
|
</div>
|
|
<div className="sm:w-48">
|
|
<select
|
|
value={statusFilter}
|
|
onChange={(e) => setStatusFilter(e.target.value)}
|
|
className="block w-full rounded-md border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 focus:border-primary-500 focus:ring-primary-500 sm:text-sm text-gray-900 dark:text-white"
|
|
>
|
|
<option value="">All Status</option>
|
|
<option value="active">Active</option>
|
|
<option value="inactive">Inactive</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Users Table */}
|
|
<div className="mt-8 flex flex-col">
|
|
<div className="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
|
<div className="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
|
|
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 dark:ring-white dark:ring-opacity-5 md:rounded-lg">
|
|
<table className="min-w-full divide-y divide-gray-300 dark:divide-gray-700">
|
|
<thead className="bg-gray-100 dark:bg-gray-800">
|
|
<tr>
|
|
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-white sm:pl-6">
|
|
Name
|
|
</th>
|
|
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">
|
|
Email
|
|
</th>
|
|
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">
|
|
Role
|
|
</th>
|
|
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">
|
|
Status
|
|
</th>
|
|
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
|
<span className="sr-only">Actions</span>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-900">
|
|
{users.map((user) => (
|
|
<tr key={user.id}>
|
|
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 dark:text-white sm:pl-6">
|
|
{user.name}
|
|
</td>
|
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-600 dark:text-gray-400">{user.email}</td>
|
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-600 dark:text-gray-400">
|
|
{user.role === 'tenant_admin' ? 'Admin' : 'Evaluator'}
|
|
</td>
|
|
<td className="whitespace-nowrap px-3 py-4 text-sm">
|
|
<span className={`inline-flex rounded-full px-2 text-xs font-semibold leading-5 ${
|
|
user.isActive
|
|
? 'bg-green-900 text-green-200'
|
|
: 'bg-red-900 text-red-200'
|
|
}`}>
|
|
{user.isActive ? 'Active' : 'Inactive'}
|
|
</span>
|
|
</td>
|
|
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
|
<button
|
|
onClick={() => {
|
|
setSelectedUser(user);
|
|
setShowEditModal(true);
|
|
}}
|
|
className="text-primary-500 hover:text-primary-400 mr-4"
|
|
>
|
|
Edit
|
|
</button>
|
|
<button
|
|
onClick={() => toggleUserStatus(user.id)}
|
|
className={`${
|
|
user.isActive
|
|
? 'text-red-500 hover:text-red-400'
|
|
: 'text-green-500 hover:text-green-400'
|
|
}`}
|
|
>
|
|
{user.isActive ? 'Deactivate' : 'Activate'}
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Add/Edit User Modal */}
|
|
<UserModal
|
|
isOpen={showAddModal || showEditModal}
|
|
onClose={() => {
|
|
setShowAddModal(false);
|
|
setShowEditModal(false);
|
|
setSelectedUser(null);
|
|
}}
|
|
user={selectedUser || undefined}
|
|
onSuccess={fetchUsers}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TenantUsers;
|