xpeditis2.0/apps/frontend/app/dashboard/admin/users/page.tsx
David a1e255e816
Some checks failed
CI/CD Pipeline / Discord Notification (Failure) (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Deployment Summary (push) Blocked by required conditions
CI/CD Pipeline / Deploy to Portainer (push) Blocked by required conditions
CI/CD Pipeline / Discord Notification (Success) (push) Blocked by required conditions
CI/CD Pipeline / Backend - Build, Test & Push (push) Failing after 1m20s
CI/CD Pipeline / Frontend - Build, Test & Push (push) Has been cancelled
fix v1.0.0
2025-12-23 11:49:57 +01:00

464 lines
18 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
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';
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 [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: '',
});
// Fetch users and organizations
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 || 'Failed to load data');
} 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 || 'Failed to create user');
}
};
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 || 'Failed to update user');
}
};
const handleDelete = async () => {
if (!selectedUser) return;
try {
await deleteAdminUser(selectedUser.id);
await fetchData();
setShowDeleteConfirm(false);
setSelectedUser(null);
} catch (err: any) {
alert(err.message || 'Failed to delete user');
}
};
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);
};
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">Loading users...</p>
</div>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">User Management</h1>
<p className="mt-1 text-sm text-gray-500">
Manage all users in the system
</p>
</div>
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
+ Create User
</button>
</div>
{/* 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">
User
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Email
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Role
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Organization
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
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'
}`}>
{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 ? 'Active' : '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"
>
Edit
</button>
<button
onClick={() => openDeleteConfirm(user)}
className="text-red-600 hover:text-red-900"
>
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">Create New User</h2>
<form onSubmit={handleCreate} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">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">First Name</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">Last Name</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">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">User</option>
<option value="MANAGER">Manager</option>
<option value="ADMIN">Admin</option>
<option value="VIEWER">Viewer</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">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="">Select Organization</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">
Password (leave empty for auto-generated)
</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"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
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">Edit User</h2>
<form onSubmit={handleUpdate} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">Email (read-only)</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">First Name</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">Last Name</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">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">User</option>
<option value="MANAGER">Manager</option>
<option value="ADMIN">Admin</option>
<option value="VIEWER">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"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
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">Confirm Delete</h2>
<p className="text-gray-700 mb-6">
Are you sure you want to delete user <strong>{selectedUser.firstName} {selectedUser.lastName}</strong>?
This action cannot be undone.
</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"
>
Cancel
</button>
<button
onClick={handleDelete}
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700"
>
Delete
</button>
</div>
</div>
</div>
)}
</div>
);
}