590 lines
25 KiB
TypeScript
590 lines
25 KiB
TypeScript
/**
|
|
* User Management Page
|
|
*
|
|
* Manage organization users, roles, and invitations
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { listUsers, updateUser, deleteUser, canInviteUser } from '@/lib/api';
|
|
import { createInvitation } from '@/lib/api/invitations';
|
|
import { useAuth } from '@/lib/context/auth-context';
|
|
import Link from 'next/link';
|
|
|
|
export default function UsersManagementPage() {
|
|
const router = useRouter();
|
|
const queryClient = useQueryClient();
|
|
const { user: currentUser } = useAuth();
|
|
const [showInviteModal, setShowInviteModal] = useState(false);
|
|
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
|
|
const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null);
|
|
const [inviteForm, setInviteForm] = useState({
|
|
email: '',
|
|
firstName: '',
|
|
lastName: '',
|
|
role: 'USER' as 'MANAGER' | 'USER' | 'VIEWER',
|
|
});
|
|
const [error, setError] = useState('');
|
|
const [success, setSuccess] = useState('');
|
|
|
|
const { data: users, isLoading } = useQuery({
|
|
queryKey: ['users'],
|
|
queryFn: () => listUsers(),
|
|
});
|
|
|
|
// Check license availability
|
|
const { data: licenseStatus } = useQuery({
|
|
queryKey: ['canInvite'],
|
|
queryFn: () => canInviteUser(),
|
|
});
|
|
|
|
const inviteMutation = useMutation({
|
|
mutationFn: (data: typeof inviteForm) => {
|
|
return createInvitation({
|
|
email: data.email,
|
|
firstName: data.firstName,
|
|
lastName: data.lastName,
|
|
role: data.role,
|
|
});
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['users'] });
|
|
queryClient.invalidateQueries({ queryKey: ['canInvite'] });
|
|
setSuccess('Invitation sent successfully! The user will receive an email with a registration link.');
|
|
setShowInviteModal(false);
|
|
setInviteForm({
|
|
email: '',
|
|
firstName: '',
|
|
lastName: '',
|
|
role: 'USER',
|
|
});
|
|
setTimeout(() => setSuccess(''), 5000);
|
|
},
|
|
onError: (err: any) => {
|
|
setError(err.response?.data?.message || 'Failed to send invitation');
|
|
setTimeout(() => setError(''), 5000);
|
|
},
|
|
});
|
|
|
|
const changeRoleMutation = useMutation({
|
|
mutationFn: ({ id, role }: { id: string; role: 'ADMIN' | 'MANAGER' | 'USER' | 'VIEWER' }) => {
|
|
return updateUser(id, { role });
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['users'] });
|
|
setSuccess('Role updated successfully');
|
|
setTimeout(() => setSuccess(''), 3000);
|
|
},
|
|
onError: (err: any) => {
|
|
setError(err.response?.data?.message || 'Failed to update role');
|
|
setTimeout(() => setError(''), 5000);
|
|
},
|
|
});
|
|
|
|
const toggleActiveMutation = useMutation({
|
|
mutationFn: ({ id, isActive }: { id: string; isActive: boolean }) => {
|
|
return updateUser(id, { isActive: !isActive });
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['users'] });
|
|
queryClient.invalidateQueries({ queryKey: ['canInvite'] });
|
|
setSuccess('User status updated successfully');
|
|
setTimeout(() => setSuccess(''), 3000);
|
|
},
|
|
onError: (err: any) => {
|
|
setError(err.response?.data?.message || 'Failed to update user status');
|
|
setTimeout(() => setError(''), 5000);
|
|
},
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (id: string) => deleteUser(id),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['users'] });
|
|
queryClient.invalidateQueries({ queryKey: ['canInvite'] });
|
|
setSuccess('User deleted successfully');
|
|
setTimeout(() => setSuccess(''), 3000);
|
|
},
|
|
onError: (err: any) => {
|
|
setError(err.response?.data?.message || 'Failed to delete user');
|
|
setTimeout(() => setError(''), 5000);
|
|
},
|
|
});
|
|
|
|
// Restrict access to ADMIN and MANAGER only
|
|
useEffect(() => {
|
|
if (currentUser && currentUser.role !== 'ADMIN' && currentUser.role !== 'MANAGER') {
|
|
router.push('/dashboard');
|
|
}
|
|
}, [currentUser, router]);
|
|
|
|
// Don't render until we've checked permissions
|
|
if (!currentUser || (currentUser.role !== 'ADMIN' && currentUser.role !== 'MANAGER')) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const handleInvite = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
|
|
inviteMutation.mutate(inviteForm);
|
|
};
|
|
|
|
const handleRoleChange = (userId: string, newRole: string) => {
|
|
changeRoleMutation.mutate({ id: userId, role: newRole as 'ADMIN' | 'MANAGER' | 'USER' | 'VIEWER' });
|
|
};
|
|
|
|
const handleToggleActive = (userId: string, isActive: boolean) => {
|
|
if (
|
|
window.confirm(`Are you sure you want to ${isActive ? 'deactivate' : 'activate'} this user?`)
|
|
) {
|
|
toggleActiveMutation.mutate({ id: userId, isActive });
|
|
}
|
|
};
|
|
|
|
const handleDelete = (userId: string) => {
|
|
if (
|
|
window.confirm('Are you sure you want to delete this user? This action cannot be undone.')
|
|
) {
|
|
deleteMutation.mutate(userId);
|
|
}
|
|
};
|
|
|
|
const getRoleBadgeColor = (role: string) => {
|
|
const colors: Record<string, string> = {
|
|
ADMIN: 'bg-red-100 text-red-800',
|
|
MANAGER: 'bg-blue-100 text-blue-800',
|
|
USER: 'bg-green-100 text-green-800',
|
|
VIEWER: 'bg-gray-100 text-gray-800',
|
|
};
|
|
return colors[role] || 'bg-gray-100 text-gray-800';
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* License Warning */}
|
|
{licenseStatus && !licenseStatus.canInvite && (
|
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
|
|
<div className="flex items-start">
|
|
<div className="flex-shrink-0">
|
|
<svg className="h-5 w-5 text-amber-400" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
<div className="ml-3 flex-1">
|
|
<h3 className="text-sm font-medium text-amber-800">License limit reached</h3>
|
|
<p className="mt-1 text-sm text-amber-700">
|
|
Your organization has used all available licenses ({licenseStatus.usedLicenses}/{licenseStatus.maxLicenses}).
|
|
Upgrade your subscription to invite more users.
|
|
</p>
|
|
<div className="mt-3">
|
|
<Link
|
|
href="/dashboard/settings/subscription"
|
|
className="text-sm font-medium text-amber-800 hover:text-amber-900 underline"
|
|
>
|
|
Upgrade Subscription
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* License Usage Info */}
|
|
{licenseStatus && licenseStatus.canInvite && licenseStatus.availableLicenses <= 2 && licenseStatus.maxLicenses !== -1 && (
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center">
|
|
<svg className="h-5 w-5 text-blue-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
|
</svg>
|
|
<span className="text-sm text-blue-800">
|
|
{licenseStatus.availableLicenses} license{licenseStatus.availableLicenses !== 1 ? 's' : ''} remaining ({licenseStatus.usedLicenses}/{licenseStatus.maxLicenses} used)
|
|
</span>
|
|
</div>
|
|
<Link
|
|
href="/dashboard/settings/subscription"
|
|
className="text-sm font-medium text-blue-600 hover:text-blue-800"
|
|
>
|
|
Manage Subscription
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">User Management</h1>
|
|
<p className="text-sm text-gray-500 mt-1">Manage team members and their permissions</p>
|
|
</div>
|
|
{licenseStatus?.canInvite ? (
|
|
<button
|
|
onClick={() => setShowInviteModal(true)}
|
|
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
|
|
>
|
|
<span className="mr-2">+</span>
|
|
Invite User
|
|
</button>
|
|
) : (
|
|
<Link
|
|
href="/dashboard/settings/subscription"
|
|
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-amber-600 hover:bg-amber-700"
|
|
>
|
|
<span className="mr-2">+</span>
|
|
Upgrade to Invite
|
|
</Link>
|
|
)}
|
|
</div>
|
|
|
|
{success && (
|
|
<div className="rounded-md bg-green-50 p-4">
|
|
<div className="text-sm text-green-800">{success}</div>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="rounded-md bg-red-50 p-4">
|
|
<div className="text-sm text-red-800">{error}</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Users Table */}
|
|
<div className="bg-white rounded-lg shadow">
|
|
{isLoading ? (
|
|
<div className="px-6 py-12 text-center text-gray-500">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
Loading users...
|
|
</div>
|
|
) : users?.users && users.users.length > 0 ? (
|
|
<div className="overflow-x-auto overflow-y-visible">
|
|
<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">
|
|
Status
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Last Login
|
|
</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.users.map(user => (
|
|
<tr key={user.id} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="flex items-center">
|
|
<div className="flex-shrink-0 h-10 w-10 bg-blue-600 rounded-full flex items-center justify-center text-white font-semibold">
|
|
{user.firstName[0]}
|
|
{user.lastName[0]}
|
|
</div>
|
|
<div className="ml-4">
|
|
<div className="text-sm font-medium text-gray-900">
|
|
{user.firstName} {user.lastName}
|
|
</div>
|
|
<div className="text-sm text-gray-500">
|
|
{user.email}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm text-gray-900">{user.email}</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<select
|
|
value={user.role}
|
|
onChange={e => handleRoleChange(user.id, e.target.value)}
|
|
className={`text-xs font-semibold rounded-full px-3 py-1 ${getRoleBadgeColor(
|
|
user.role
|
|
)}`}
|
|
disabled={
|
|
changeRoleMutation.isPending ||
|
|
(user.role === 'ADMIN' && currentUser?.role !== 'ADMIN') ||
|
|
user.id === currentUser?.id
|
|
}
|
|
title={user.id === currentUser?.id ? 'You cannot change your own role' : ''}
|
|
>
|
|
{currentUser?.role === 'ADMIN' && <option value="ADMIN">Admin</option>}
|
|
<option value="MANAGER">Manager</option>
|
|
<option value="USER">User</option>
|
|
<option value="VIEWER">Viewer</option>
|
|
</select>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span
|
|
className={`px-3 py-1 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-sm text-gray-500">
|
|
{new Date(user.createdAt).toLocaleDateString()}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
<button
|
|
onClick={(e) => {
|
|
if (openMenuId === user.id) {
|
|
setOpenMenuId(null);
|
|
setMenuPosition(null);
|
|
} else {
|
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
setMenuPosition({
|
|
top: rect.bottom + 5,
|
|
left: rect.left - 180
|
|
});
|
|
setOpenMenuId(user.id);
|
|
}
|
|
}}
|
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
>
|
|
<svg className="w-5 h-5 text-gray-600" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
|
|
</svg>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
) : (
|
|
<div className="px-6 py-12 text-center">
|
|
<svg
|
|
className="mx-auto h-12 w-12 text-gray-400"
|
|
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 7a4 4 0 11-8 0 4 4 0 018 0z"
|
|
/>
|
|
</svg>
|
|
<h3 className="mt-2 text-sm font-medium text-gray-900">No users</h3>
|
|
<p className="mt-1 text-sm text-gray-500">Get started by inviting a team member</p>
|
|
<div className="mt-6">
|
|
{licenseStatus?.canInvite ? (
|
|
<button
|
|
onClick={() => setShowInviteModal(true)}
|
|
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
|
|
>
|
|
<span className="mr-2">+</span>
|
|
Invite User
|
|
</button>
|
|
) : (
|
|
<Link
|
|
href="/dashboard/settings/subscription"
|
|
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-amber-600 hover:bg-amber-700"
|
|
>
|
|
<span className="mr-2">+</span>
|
|
Upgrade to Invite
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Actions Menu Modal */}
|
|
{openMenuId && menuPosition && (
|
|
<>
|
|
<div
|
|
className="fixed inset-0 z-[998]"
|
|
onClick={() => {
|
|
setOpenMenuId(null);
|
|
setMenuPosition(null);
|
|
}}
|
|
/>
|
|
<div
|
|
className="fixed w-56 bg-white border-2 border-gray-300 rounded-lg shadow-2xl z-[999]"
|
|
style={{
|
|
top: `${menuPosition.top}px`,
|
|
left: `${menuPosition.left}px`,
|
|
}}
|
|
>
|
|
<div className="py-2">
|
|
<button
|
|
onClick={() => {
|
|
const user = users?.users.find(u => u.id === openMenuId);
|
|
if (user) {
|
|
handleToggleActive(user.id, user.isActive);
|
|
}
|
|
setOpenMenuId(null);
|
|
setMenuPosition(null);
|
|
}}
|
|
disabled={toggleActiveMutation.isPending}
|
|
className="w-full px-4 py-3 text-left hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-3 border-b border-gray-200"
|
|
>
|
|
{users?.users.find(u => u.id === openMenuId)?.isActive ? (
|
|
<>
|
|
<svg className="w-5 h-5 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
|
</svg>
|
|
<span className="text-sm font-medium text-gray-700">Désactiver</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<span className="text-sm font-medium text-gray-700">Activer</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
if (openMenuId) {
|
|
handleDelete(openMenuId);
|
|
}
|
|
setOpenMenuId(null);
|
|
setMenuPosition(null);
|
|
}}
|
|
disabled={deleteMutation.isPending}
|
|
className="w-full px-4 py-3 text-left hover:bg-red-50 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-3"
|
|
>
|
|
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
<span className="text-sm font-medium text-red-600">Supprimer</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Invite Modal */}
|
|
{showInviteModal && (
|
|
<div className="fixed inset-0 z-50 overflow-y-auto">
|
|
<div className="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
|
<div
|
|
className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75"
|
|
onClick={() => setShowInviteModal(false)}
|
|
/>
|
|
|
|
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
|
|
<div>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-medium text-gray-900">Invite User</h3>
|
|
<button
|
|
onClick={() => setShowInviteModal(false)}
|
|
className="text-gray-400 hover:text-gray-500"
|
|
>
|
|
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M6 18L18 6M6 6l12 12"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleInvite} className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">
|
|
First Name *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={inviteForm.firstName}
|
|
onChange={e => setInviteForm({ ...inviteForm, firstName: e.target.value })}
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">Last Name *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={inviteForm.lastName}
|
|
onChange={e => setInviteForm({ ...inviteForm, lastName: e.target.value })}
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">Email *</label>
|
|
<input
|
|
type="email"
|
|
required
|
|
value={inviteForm.email}
|
|
onChange={e => setInviteForm({ ...inviteForm, email: e.target.value })}
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700">Role *</label>
|
|
<select
|
|
value={inviteForm.role}
|
|
onChange={e => setInviteForm({ ...inviteForm, role: e.target.value as any })}
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm px-3 py-2 border"
|
|
>
|
|
<option value="USER">User</option>
|
|
<option value="MANAGER">Manager</option>
|
|
{currentUser?.role === 'ADMIN' && <option value="ADMIN">Admin</option>}
|
|
<option value="VIEWER">Viewer</option>
|
|
</select>
|
|
{currentUser?.role !== 'ADMIN' && (
|
|
<p className="mt-1 text-xs text-gray-500">
|
|
Only platform administrators can assign the ADMIN role
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-5 sm:mt-6 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense">
|
|
<button
|
|
type="submit"
|
|
disabled={inviteMutation.isPending}
|
|
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:col-start-2 sm:text-sm disabled:bg-gray-400"
|
|
>
|
|
{inviteMutation.isPending ? 'Inviting...' : 'Send Invitation'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowInviteModal(false)}
|
|
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:col-start-1 sm:text-sm"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|