420 lines
15 KiB
JavaScript
420 lines
15 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { userApi } from '../api/users';
|
|
import LoadingCard from '@/components/LoadingCard';
|
|
import MainCard from '@/components/MainCard';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow
|
|
} from '@/components/ui/table';
|
|
import { Input } from '@/components/ui/input';
|
|
import { CardContent } from '@/components/ui/card';
|
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import ConfirmDialog from '@/components/ConfirmDialog';
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
import { Label } from '@/components/ui/label';
|
|
import { useAuth } from '../context/useAuth';
|
|
import { Users} from 'lucide-react';
|
|
const AdminUsersPage = () => {
|
|
const { user: currentUser } = useAuth();
|
|
const [users, setUsers] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
const [inviteLink, setInviteLink] = useState('');
|
|
const [isCopied, setIsCopied] = useState(false);
|
|
const [editingUser, setEditingUser] = useState(null);
|
|
const [editForm, setEditForm] = useState({
|
|
first_name: '',
|
|
last_name: '',
|
|
email: '',
|
|
color: '#0000ff',
|
|
is_active: true,
|
|
is_staff: false,
|
|
is_superuser: false,
|
|
});
|
|
|
|
const generateInviteLink = async () => {
|
|
try {
|
|
setError('');
|
|
const invitation = await userApi.createInvitation();
|
|
const newInviteLink = `${window.location.origin}/register?invite=${invitation.token}`;
|
|
setInviteLink(newInviteLink);
|
|
setIsCopied(false);
|
|
} catch (error) {
|
|
setError('Erreur lors de la génération du lien d\'invitation');
|
|
console.error(error);
|
|
}
|
|
};
|
|
const copyToClipboard = () => {
|
|
if (inviteLink) {
|
|
navigator.clipboard.writeText(inviteLink).then(() => {
|
|
setIsCopied(true);
|
|
setTimeout(() => setIsCopied(false), 2000);
|
|
}).catch((error) => {
|
|
setError('Impossible de copier le lien.');
|
|
console.error(error);
|
|
});
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const loadUsers = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const users = await userApi.getUsers();
|
|
setUsers(users);
|
|
} catch (requestError) {
|
|
setError('Erreur lors du chargement des utilisateurs');
|
|
console.error(requestError);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadUsers();
|
|
}, []);
|
|
|
|
const handleDeleteUser = async (userId) => {
|
|
try {
|
|
await userApi.deleteUser(userId);
|
|
|
|
setUsers(users.filter(user => String(user.id) !== String(userId)));
|
|
} catch (err) {
|
|
setError(err.message);
|
|
console.error(err);
|
|
}
|
|
};
|
|
|
|
const handleMakeAdmin = async (userId) => {
|
|
try {
|
|
await userApi.promoteUser(userId);
|
|
window.location.reload();
|
|
} catch (err) {
|
|
setError(err.message);
|
|
console.error(err);
|
|
}
|
|
};
|
|
|
|
const handleEditUser = async (userId) => {
|
|
if (!/^#[0-9A-Fa-f]{6}$/.test(editForm.color)) {
|
|
setError('La couleur doit être au format hexadécimal, par exemple #2563eb.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const payload = { ...editForm };
|
|
if (!currentUser.is_superuser) delete payload.is_superuser;
|
|
await userApi.updateUser(userId, payload);
|
|
window.location.reload();
|
|
} catch (err) {
|
|
setError(err.message);
|
|
console.error(err);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<LoadingCard
|
|
icon={Users}
|
|
title="Gestion des utilisateurs"
|
|
message="Chargement des utilisateurs..."
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<MainCard title="Gestion des utilisateurs" icon={Users}>
|
|
<CardContent className="overflow-y-auto">
|
|
{error && (
|
|
<Alert variant="destructive" className="mb-4">
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<div className="flex h-9 w-full min-w-0 items-center gap-2">
|
|
<Button
|
|
onClick={generateInviteLink}
|
|
size="sm"
|
|
className="shrink-0"
|
|
>
|
|
Inviter un utilisateur
|
|
</Button>
|
|
|
|
{inviteLink && (
|
|
<>
|
|
<div className="min-w-0 flex-1">
|
|
<Input
|
|
type="text"
|
|
value={inviteLink}
|
|
readOnly
|
|
className="h-9 w-full"
|
|
/>
|
|
</div>
|
|
|
|
<Button
|
|
onClick={copyToClipboard}
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-9 shrink-0"
|
|
>
|
|
{isCopied ? 'Copié !' : 'Copier'}
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div className="overflow-x-auto mb-6">
|
|
<Table className="w-full">
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="text-center">Nom</TableHead>
|
|
<TableHead className="text-center">Couleur</TableHead>
|
|
<TableHead className="text-center">Rôle</TableHead>
|
|
<TableHead className="text-center">Inscrit le</TableHead>
|
|
<TableHead className="text-center">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
|
|
<TableBody>
|
|
{users.map((user) => (
|
|
<TableRow key={user.id}>
|
|
<TableCell className="text-center">
|
|
{user.first_name} {user.last_name}
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
<div className="flex items-center justify-center gap-2">
|
|
<div
|
|
className="h-4 w-4 rounded-full"
|
|
style={{
|
|
backgroundColor: user.color || '#0000ff'
|
|
}}
|
|
/>
|
|
<span>{user.color || 'Bleu'}</span>
|
|
</div>
|
|
</TableCell>
|
|
|
|
<TableCell className="text-center">
|
|
<Badge variant={user.is_admin ? 'default' : 'secondary'}>
|
|
{user.is_admin ? 'Administrateur' : 'Utilisateur'}
|
|
</Badge>
|
|
</TableCell>
|
|
|
|
<TableCell className="text-center">
|
|
{new Date(user.date_joined).toLocaleDateString('fr-FR')}
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
<div className="flex justify-center gap-2">
|
|
<Dialog
|
|
open={editingUser === user.id}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setEditingUser(null);
|
|
} else {
|
|
setEditingUser(user.id);
|
|
setEditForm({
|
|
first_name: user.first_name || '',
|
|
last_name: user.last_name || '',
|
|
email: user.email || '',
|
|
color: user.color || '#0000ff',
|
|
is_active: user.is_active,
|
|
is_staff: user.is_staff || user.is_admin,
|
|
is_superuser: user.is_superuser,
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={user.is_superuser && !currentUser.is_superuser}
|
|
onClick={() => {
|
|
setEditingUser(user.id);
|
|
setEditForm({
|
|
first_name: user.first_name || '',
|
|
last_name: user.last_name || '',
|
|
email: user.email || '',
|
|
color: user.color || '#0000ff',
|
|
is_active: user.is_active,
|
|
is_staff: user.is_staff || user.is_admin,
|
|
is_superuser: user.is_superuser,
|
|
});
|
|
}}
|
|
>
|
|
Éditer
|
|
</Button>
|
|
|
|
<DialogContent>
|
|
<DialogHeader className="border-b">
|
|
<DialogTitle>Modifier l'utilisateur</DialogTitle>
|
|
</DialogHeader>
|
|
{error && <Alert variant="destructive"><AlertDescription>{error}</AlertDescription></Alert>}
|
|
|
|
<div className="grid gap-4 py-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="grid gap-2">
|
|
<Label htmlFor={`first-name-${user.id}`}>
|
|
Prénom
|
|
</Label>
|
|
<Input
|
|
id={`first-name-${user.id}`}
|
|
value={editForm.first_name}
|
|
onChange={(event) =>
|
|
setEditForm({
|
|
...editForm,
|
|
first_name: event.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Label htmlFor={`last-name-${user.id}`}>
|
|
Nom
|
|
</Label>
|
|
<Input
|
|
id={`last-name-${user.id}`}
|
|
value={editForm.last_name}
|
|
onChange={(event) =>
|
|
setEditForm({
|
|
...editForm,
|
|
last_name: event.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Label htmlFor={`email-${user.id}`}>E-mail</Label>
|
|
<Input
|
|
id={`email-${user.id}`}
|
|
type="email"
|
|
value={editForm.email}
|
|
onChange={(event) =>
|
|
setEditForm({
|
|
...editForm,
|
|
email: event.target.value,
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Label htmlFor={`color-${user.id}`}>Couleur</Label>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<Input
|
|
id={`color-${user.id}`}
|
|
type="color"
|
|
value={
|
|
/^#[0-9A-Fa-f]{6}$/.test(editForm.color)
|
|
? editForm.color
|
|
: '#000000'
|
|
}
|
|
onChange={(event) =>
|
|
setEditForm({
|
|
...editForm,
|
|
color: event.target.value,
|
|
})
|
|
}
|
|
className="h-10 w-14 cursor-pointer p-1"
|
|
/>
|
|
|
|
<Input
|
|
aria-label="Valeur hexadécimale de la couleur"
|
|
value={editForm.color}
|
|
pattern="^#[0-9A-Fa-f]{6}$"
|
|
onChange={(event) =>
|
|
setEditForm({
|
|
...editForm,
|
|
color: event.target.value,
|
|
})
|
|
}
|
|
className="max-w-36 font-mono uppercase"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<label className="flex items-center gap-3 text-sm">
|
|
<Checkbox
|
|
checked={editForm.is_active}
|
|
onCheckedChange={(checked) =>
|
|
setEditForm({
|
|
...editForm,
|
|
is_active: checked,
|
|
})
|
|
}
|
|
/>
|
|
Compte actif
|
|
</label>
|
|
|
|
<label className="flex items-center gap-3 text-sm">
|
|
<Checkbox
|
|
checked={editForm.is_staff}
|
|
onCheckedChange={(checked) =>
|
|
setEditForm({
|
|
...editForm,
|
|
is_staff: checked,
|
|
is_superuser: checked
|
|
? editForm.is_superuser
|
|
: false,
|
|
})
|
|
}
|
|
/>
|
|
Administrateur
|
|
</label>
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
Le mot de passe ne peut pas être modifié ici.
|
|
</p>
|
|
</div>
|
|
|
|
<Button onClick={() => handleEditUser(user.id)}>
|
|
Sauvegarder
|
|
</Button>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{!user.is_admin && (
|
|
<ConfirmDialog
|
|
title="Promouvoir l'utilisateur"
|
|
description="Êtes-vous sûr de vouloir promouvoir cet utilisateur ?"
|
|
onConfirm={() => handleMakeAdmin(user.id)}
|
|
>
|
|
<Button variant="outline" size="sm">
|
|
Promouvoir
|
|
</Button>
|
|
</ConfirmDialog>
|
|
)}
|
|
|
|
<ConfirmDialog
|
|
title="Supprimer l'utilisateur"
|
|
description="Êtes-vous sûr de vouloir supprimer cet utilisateur ?"
|
|
onConfirm={() => handleDeleteUser(user.id)}
|
|
>
|
|
<Button variant="destructive" size="sm" disabled={String(user.id) === String(currentUser.id) || (user.is_superuser && !currentUser.is_superuser)}>
|
|
Supprimer
|
|
</Button>
|
|
</ConfirmDialog>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</CardContent>
|
|
</MainCard>
|
|
);
|
|
};
|
|
|
|
export default AdminUsersPage;
|