191 lines
8.7 KiB
TypeScript
191 lines
8.7 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import frappeAPI from '../api/frappeClient';
|
|
import type { FrappeDocType } from '../api/frappeClient';
|
|
|
|
const UsersList: React.FC = () => {
|
|
const [users, setUsers] = useState<FrappeDocType[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const navigate = useNavigate();
|
|
|
|
useEffect(() => {
|
|
loadUsers();
|
|
}, []);
|
|
|
|
const loadUsers = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await frappeAPI.getDocTypeRecords(
|
|
'User',
|
|
{},
|
|
['name', 'full_name', 'email', 'enabled', 'creation', 'modified']
|
|
);
|
|
setUsers(response.message || []);
|
|
} catch (err: any) {
|
|
setError(err.response?.data?.message || 'Failed to load users');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const filteredUsers = users.filter(user =>
|
|
user.full_name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
user.email?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
user.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
const handleUserClick = (user: FrappeDocType) => {
|
|
navigate(`/users/${user.name}`);
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
|
|
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-indigo-600"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
|
{/* Header */}
|
|
<header className="bg-white dark:bg-gray-800 shadow">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex justify-between items-center py-6">
|
|
<div className="flex items-center">
|
|
<button
|
|
onClick={() => navigate('/dashboard')}
|
|
className="mr-4 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
|
|
>
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
</button>
|
|
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white break-words">Users</h1>
|
|
</div>
|
|
<div className="flex items-center space-x-4">
|
|
<button
|
|
onClick={() => navigate('/dashboard')}
|
|
className="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-medium"
|
|
>
|
|
Back to Dashboard
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Content */}
|
|
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
|
{error && (
|
|
<div className="mb-6 rounded-md bg-red-50 dark:bg-red-900/20 p-4">
|
|
<div className="text-sm text-red-700 dark:text-red-400">{error}</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Search and Actions */}
|
|
<div className="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<svg className="h-5 w-5 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
</svg>
|
|
</div>
|
|
<input
|
|
type="text"
|
|
placeholder="Search users..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md leading-5 bg-white dark:bg-gray-700 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="mt-4 sm:mt-0 sm:ml-4">
|
|
<button
|
|
onClick={loadUsers}
|
|
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
>
|
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
</svg>
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Users Table */}
|
|
<div className="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-md">
|
|
<div className="px-4 py-5 sm:px-6">
|
|
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
|
Users ({filteredUsers.length})
|
|
</h3>
|
|
<p className="mt-1 max-w-2xl text-sm text-gray-500 dark:text-gray-400">
|
|
Manage user accounts and permissions
|
|
</p>
|
|
</div>
|
|
|
|
{filteredUsers.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<svg className="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
|
|
</svg>
|
|
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">No users found</h3>
|
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
{searchTerm ? 'Try adjusting your search terms.' : 'No users available.'}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
|
{filteredUsers.map((user) => (
|
|
<li key={user.name}>
|
|
<div
|
|
className="px-4 py-4 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer"
|
|
onClick={() => handleUserClick(user)}
|
|
>
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0 h-10 w-10">
|
|
<div className="h-10 w-10 rounded-full bg-indigo-100 dark:bg-indigo-900 flex items-center justify-center">
|
|
<span className="text-sm font-medium text-indigo-600 dark:text-indigo-300">
|
|
{user.full_name?.charAt(0) || user.name.charAt(0)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="ml-4">
|
|
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
|
{user.full_name || user.name}
|
|
</div>
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
{user.email || 'No email'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center space-x-4">
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
<div className="flex items-center">
|
|
<div className={`w-2 h-2 rounded-full mr-2 ${user.enabled ? 'bg-green-400' : 'bg-red-400'}`}></div>
|
|
{user.enabled ? 'Active' : 'Inactive'}
|
|
</div>
|
|
</div>
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
Created: {new Date(user.creation).toLocaleDateString()}
|
|
</div>
|
|
<svg className="w-5 h-5 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default UsersList;
|