356 lines
14 KiB
JavaScript
356 lines
14 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { userApi } from '../api/users';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow
|
|
} from '@/components/ui/table';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Card, CardContent, CardHeader, CardTitle } 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, DialogTrigger } from '@/components/ui/dialog';
|
|
import { Label } from '@/components/ui/label';
|
|
import { useAuth } from '../context/useAuth';
|
|
|
|
const AdminUsersPage = () => {
|
|
const { currentUser, logout } = 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 => user.id !== userId));
|
|
if(userId===currentUser.id){
|
|
logout();
|
|
}
|
|
} catch (err) {
|
|
setError('Erreur lors de la suppression de l\'utilisateur');
|
|
console.error(err);
|
|
}
|
|
};
|
|
|
|
const handleMakeAdmin = async (userId) => {
|
|
// Remplacer window.confirm par le composant ConfirmDialog
|
|
const confirmed = await new Promise((resolve) => {
|
|
resolve(true); // On résout directement pour l'exemple
|
|
});
|
|
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await userApi.promoteUser(userId);
|
|
setUsers(users.map(user =>
|
|
user.id === userId ? { ...user, is_admin: true } : user
|
|
));
|
|
} catch (err) {
|
|
setError('Erreur lors de la promotion de l\'utilisateur');
|
|
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 updatedUser = await userApi.updateUser(userId, editForm);
|
|
const updatedUsers = users.map(user =>
|
|
user.id === userId ? updatedUser : user
|
|
);
|
|
setUsers(updatedUsers);
|
|
setEditingUser(null);
|
|
setEditForm({
|
|
first_name: '',
|
|
last_name: '',
|
|
email: '',
|
|
color: '#0000ff',
|
|
is_active: true,
|
|
is_staff: false,
|
|
is_superuser: false,
|
|
});
|
|
} catch (err) {
|
|
setError('Erreur lors de la mise à jour de l\'utilisateur');
|
|
console.error(err);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<Card className="page-container page-layout admin-page">
|
|
<CardHeader className="border-b">
|
|
<CardTitle>Gestion des comptes</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p>Chargement des utilisateurs...</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card className="page-container page-layout admin-page flex h-full w-full">
|
|
<CardHeader className="border-b">
|
|
<CardTitle>Gestion des comptes</CardTitle>
|
|
</CardHeader>
|
|
|
|
<CardContent>
|
|
{error && (
|
|
<Alert variant="destructive" className="mb-4">
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<div className="overflow-x-auto mb-6">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Nom</TableHead>
|
|
<TableHead>Couleur</TableHead>
|
|
<TableHead>Rôle</TableHead>
|
|
<TableHead>Inscrit le</TableHead>
|
|
<TableHead>Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{users.map((user) => (
|
|
<TableRow key={user.id}>
|
|
<TableCell>{user.first_name} {user.last_name}</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: user.color || '#0000ff' }}></div>
|
|
<span>{user.color || 'Bleu'}</span>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant={user.is_admin ? "default" : "secondary"}>
|
|
{user.is_admin ? 'Administrateur' : 'Utilisateur'}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>{new Date(user.date_joined).toLocaleDateString('fr-FR')}</TableCell>
|
|
<TableCell>
|
|
<div className="flex 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,
|
|
});
|
|
}
|
|
}}>
|
|
<DialogTrigger asChild>
|
|
<Button variant="outline" size="sm" onClick={() => setEditingUser(user.id)}>
|
|
Éditer
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader className="border-b">
|
|
<DialogTitle>Modifier l'utilisateur</DialogTitle>
|
|
</DialogHeader>
|
|
<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">
|
|
Supprimer
|
|
</Button>
|
|
</ConfirmDialog>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-4">
|
|
{inviteLink ? (
|
|
<div className="flex flex-col gap-2">
|
|
<Button onClick={generateInviteLink}>
|
|
Inviter un utilisateur
|
|
</Button>
|
|
<div className="flex gap-2">
|
|
<Input
|
|
type="text"
|
|
value={inviteLink}
|
|
readOnly
|
|
className="flex-1"
|
|
/>
|
|
<Button
|
|
onClick={copyToClipboard}
|
|
variant="outline"
|
|
>
|
|
{isCopied ? 'Copié!' : 'Copier'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<Button onClick={generateInviteLink}>
|
|
Inviter un utilisateur
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export default AdminUsersPage; |