feat: improve user management and profile features
- Replace email verification status with active/inactive status - Add user archiving (soft delete) instead of hard delete - Add search by name/email in user management - Add status filter (active/inactive) in user management - Simplify actions to show only "Détails" button - Add Google Maps with clickable marker on contact page - Update button labels from "Désactiver" to "Supprimer" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c4ac79ef8b
commit
dce1559a32
|
|
@ -416,11 +416,6 @@ export default function AdminDashboardAdvanced() {
|
||||||
<StatRow label="Clients" value={stats?.users?.clients || 0} color="green" />
|
<StatRow label="Clients" value={stats?.users?.clients || 0} color="green" />
|
||||||
<StatRow label="Employés" value={stats?.users?.employees || 0} color="purple" />
|
<StatRow label="Employés" value={stats?.users?.employees || 0} color="purple" />
|
||||||
<StatRow label="Admins" value={stats?.users?.admins || 0} color="blue" />
|
<StatRow label="Admins" value={stats?.users?.admins || 0} color="blue" />
|
||||||
<StatRow
|
|
||||||
label="Emails vérifiés"
|
|
||||||
value={stats?.users?.verifiedEmails || 0}
|
|
||||||
color="green"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -203,11 +203,6 @@ export default function AdminDashboard() {
|
||||||
<StatRow label="Clients" value={stats?.users?.clients || 0} color="green" />
|
<StatRow label="Clients" value={stats?.users?.clients || 0} color="green" />
|
||||||
<StatRow label="Employés" value={stats?.users?.employees || 0} color="purple" />
|
<StatRow label="Employés" value={stats?.users?.employees || 0} color="purple" />
|
||||||
<StatRow label="Admins" value={stats?.users?.admins || 0} color="blue" />
|
<StatRow label="Admins" value={stats?.users?.admins || 0} color="blue" />
|
||||||
<StatRow
|
|
||||||
label="Emails vérifiés"
|
|
||||||
value={stats?.users?.verifiedEmails || 0}
|
|
||||||
color="green"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -416,11 +416,6 @@ export default function AdminDashboardAdvanced() {
|
||||||
<StatRow label="Clients" value={stats?.users?.clients || 0} color="green" />
|
<StatRow label="Clients" value={stats?.users?.clients || 0} color="green" />
|
||||||
<StatRow label="Employés" value={stats?.users?.employees || 0} color="purple" />
|
<StatRow label="Employés" value={stats?.users?.employees || 0} color="purple" />
|
||||||
<StatRow label="Admins" value={stats?.users?.admins || 0} color="blue" />
|
<StatRow label="Admins" value={stats?.users?.admins || 0} color="blue" />
|
||||||
<StatRow
|
|
||||||
label="Emails vérifiés"
|
|
||||||
value={stats?.users?.verifiedEmails || 0}
|
|
||||||
color="green"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,6 @@ export default function MarketingPage() {
|
||||||
'Code Postal',
|
'Code Postal',
|
||||||
'Genre',
|
'Genre',
|
||||||
'Âge',
|
'Âge',
|
||||||
'Vérifié',
|
|
||||||
'A joué',
|
'A joué',
|
||||||
'A gagné',
|
'A gagné',
|
||||||
'Nombre de tickets',
|
'Nombre de tickets',
|
||||||
|
|
@ -173,7 +172,6 @@ export default function MarketingPage() {
|
||||||
user.postal_code || '',
|
user.postal_code || '',
|
||||||
user.gender || '',
|
user.gender || '',
|
||||||
user.age || '',
|
user.age || '',
|
||||||
user.is_verified ? 'Oui' : 'Non',
|
|
||||||
user.has_played ? 'Oui' : 'Non',
|
user.has_played ? 'Oui' : 'Non',
|
||||||
user.has_won ? 'Oui' : 'Non',
|
user.has_won ? 'Oui' : 'Non',
|
||||||
user.ticket_count || 0,
|
user.ticket_count || 0,
|
||||||
|
|
@ -388,19 +386,7 @@ export default function MarketingPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filtres additionnels */}
|
{/* Filtres additionnels */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
|
||||||
<label className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={filters.verified}
|
|
||||||
onChange={(e) => setFilters({ ...filters, verified: e.target.checked })}
|
|
||||||
className="w-4 h-4"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-700">Emails vérifiés uniquement</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm text-gray-700 mb-1">Ville</label>
|
<label className="block text-sm text-gray-700 mb-1">Ville</label>
|
||||||
<select
|
<select
|
||||||
|
|
|
||||||
|
|
@ -184,18 +184,6 @@ export default function AdminProfilePage() {
|
||||||
<h2 className="text-xl font-bold text-gray-900">Statut du compte</h2>
|
<h2 className="text-xl font-bold text-gray-900">Statut du compte</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-gray-600">
|
|
||||||
Email vérifié
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
{user.isVerified ? (
|
|
||||||
<Badge variant="success">Vérifié ✓</Badge>
|
|
||||||
) : (
|
|
||||||
<Badge variant="warning">Non vérifié</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-600">
|
<label className="text-sm font-medium text-gray-600">
|
||||||
Membre depuis
|
Membre depuis
|
||||||
|
|
|
||||||
|
|
@ -505,9 +505,6 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
|
||||||
<th className="px-6 py-4 text-center text-xs font-bold text-gray-700 uppercase tracking-wider">
|
<th className="px-6 py-4 text-center text-xs font-bold text-gray-700 uppercase tracking-wider">
|
||||||
Lots Gagnés
|
Lots Gagnés
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-4 text-center text-xs font-bold text-gray-700 uppercase tracking-wider">
|
|
||||||
Statut
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
|
@ -537,19 +534,6 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
|
||||||
{participant.prizes_won}
|
{participant.prizes_won}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
|
||||||
{participant.is_verified ? (
|
|
||||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
||||||
<CheckCircle className="w-4 h-4 mr-1" />
|
|
||||||
Vérifié
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
|
|
||||||
<AlertCircle className="w-4 h-4 mr-1" />
|
|
||||||
Non vérifié
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
||||||
|
|
@ -200,23 +200,13 @@ export default function UserDetailsPage() {
|
||||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Statut du compte</h2>
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">Statut du compte</h2>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-600 mb-1">Vérification email</p>
|
<p className="text-sm font-medium text-gray-600 mb-1">Statut</p>
|
||||||
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold ${
|
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold ${
|
||||||
user.isVerified
|
user.isActive !== false
|
||||||
? 'bg-green-100 text-green-800'
|
? 'bg-green-100 text-green-800'
|
||||||
: 'bg-yellow-100 text-yellow-800'
|
: 'bg-red-100 text-red-800'
|
||||||
}`}>
|
}`}>
|
||||||
{user.isVerified ? (
|
{user.isActive !== false ? 'Actif' : 'Inactif'}
|
||||||
<>
|
|
||||||
<CheckCircle className="w-4 h-4 mr-1" />
|
|
||||||
Vérifié
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Clock className="w-4 h-4 mr-1" />
|
|
||||||
Non vérifié
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -245,14 +235,6 @@ export default function UserDetailsPage() {
|
||||||
>
|
>
|
||||||
Modifier l'utilisateur
|
Modifier l'utilisateur
|
||||||
</button>
|
</button>
|
||||||
{user.role === 'CLIENT' && (
|
|
||||||
<button
|
|
||||||
onClick={() => router.push(`/admin/tickets?userId=${user.id}`)}
|
|
||||||
className="w-full px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
|
||||||
>
|
|
||||||
Voir les tickets
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -214,11 +214,16 @@ export default function ContactPage() {
|
||||||
<div className="w-12 h-12 bg-gradient-to-br from-[#d4a574] to-[#c4956a] rounded-full flex items-center justify-center text-xl shadow-md flex-shrink-0">📍</div>
|
<div className="w-12 h-12 bg-gradient-to-br from-[#d4a574] to-[#c4956a] rounded-full flex items-center justify-center text-xl shadow-md flex-shrink-0">📍</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-[#5a5a4e] mb-1">Siège social</h3>
|
<h3 className="font-semibold text-[#5a5a4e] mb-1">Siège social</h3>
|
||||||
<p className="text-[#8a8a7a] text-sm">
|
<a
|
||||||
|
href="https://www.google.com/maps/search/?api=1&query=18+Avenue+Thiers+06000+Nice+France"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-[#8a8a7a] text-sm hover:text-[#d4a574] transition-colors block"
|
||||||
|
>
|
||||||
Thé Tip Top<br />
|
Thé Tip Top<br />
|
||||||
18 Avenue Thiers<br />
|
18 Avenue Thiers<br />
|
||||||
06000 Nice, France
|
06000 Nice, France
|
||||||
</p>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -265,6 +270,44 @@ export default function ContactPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Google Maps */}
|
||||||
|
<div className="bg-white rounded-xl shadow-md p-4 border border-[#e5e4dc]">
|
||||||
|
<h2 className="text-xl font-bold text-[#5a5a4e] mb-4">Nous trouver</h2>
|
||||||
|
<div className="rounded-lg overflow-hidden relative">
|
||||||
|
<iframe
|
||||||
|
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2884.5!2d7.2619!3d43.7102!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x12cdd0106a852d31%3A0x40819a5fd979a70!2s18%20Av.%20Thiers%2C%2006000%20Nice%2C%20France!5e0!3m2!1sfr!2sfr!4v1700000000000!5m2!1sfr!2sfr"
|
||||||
|
width="100%"
|
||||||
|
height="250"
|
||||||
|
style={{ border: 0 }}
|
||||||
|
allowFullScreen
|
||||||
|
loading="lazy"
|
||||||
|
referrerPolicy="no-referrer-when-downgrade"
|
||||||
|
title="Localisation Thé Tip Top - 18 Avenue Thiers, Nice"
|
||||||
|
></iframe>
|
||||||
|
{/* Point rouge cliquable */}
|
||||||
|
<a
|
||||||
|
href="https://www.google.com/maps/search/?api=1&query=18+Avenue+Thiers+06000+Nice+France"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-10 group"
|
||||||
|
>
|
||||||
|
<span className="block w-6 h-6 bg-red-600 rounded-full border-2 border-white shadow-lg cursor-pointer hover:scale-125 transition-transform animate-pulse"></span>
|
||||||
|
<span className="absolute -bottom-1 left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-[6px] border-r-[6px] border-t-[8px] border-l-transparent border-r-transparent border-t-red-600"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="https://www.google.com/maps/search/?api=1&query=18+Avenue+Thiers+06000+Nice+France"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="mt-3 inline-flex items-center gap-2 text-sm text-[#d4a574] hover:text-[#c4956a] transition-colors"
|
||||||
|
>
|
||||||
|
Ouvrir dans Google Maps
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -182,18 +182,6 @@ export default function EmployeProfilePage() {
|
||||||
<h2 className="text-xl font-bold text-gray-900">Statut du compte</h2>
|
<h2 className="text-xl font-bold text-gray-900">Statut du compte</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-gray-600">
|
|
||||||
Email vérifié
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
{user.isVerified ? (
|
|
||||||
<Badge variant="success">Vérifié ✓</Badge>
|
|
||||||
) : (
|
|
||||||
<Badge variant="warning">Non vérifié</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-600">
|
<label className="text-sm font-medium text-gray-600">
|
||||||
Membre depuis
|
Membre depuis
|
||||||
|
|
|
||||||
59
app/not-found.tsx
Normal file
59
app/not-found.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-[#f5f5f0] via-[#faf9f5] to-[#f5f5f0] flex items-center justify-center px-4">
|
||||||
|
<div className="text-center max-w-2xl mx-auto">
|
||||||
|
{/* Logo and 404 */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="flex justify-center mb-8">
|
||||||
|
<img
|
||||||
|
src="/logos/logo.svg"
|
||||||
|
alt="Thé Tip Top Logo"
|
||||||
|
className="w-64 h-auto md:w-80"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-[120px] md:text-[150px] leading-none font-bold text-[#d4a574] opacity-30 select-none">
|
||||||
|
404
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Text content */}
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-[#5a5a4e] mb-4">
|
||||||
|
Oups ! Page introuvable
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-[#8a8a7a] mb-8 max-w-md mx-auto">
|
||||||
|
Il semble que cette page se soit évaporée comme la vapeur d'une bonne tasse de thé...
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<button
|
||||||
|
onClick={() => router.back()}
|
||||||
|
className="inline-flex items-center justify-center gap-2 border-2 border-[#d4a574] text-[#d4a574] hover:bg-[#d4a574] hover:text-white font-bold px-8 py-3 rounded-lg transition-all duration-300"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||||
|
</svg>
|
||||||
|
Retour
|
||||||
|
</button>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-flex items-center justify-center gap-2 bg-gradient-to-r from-[#d4a574] to-[#c4956a] hover:from-[#e5b685] hover:to-[#d4a574] text-white font-bold px-8 py-3 rounded-lg transition-all shadow-lg hover:scale-105 duration-300"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||||
|
</svg>
|
||||||
|
Accueil
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -13,15 +13,18 @@ import toast from "react-hot-toast";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { ROUTES } from "@/utils/constants";
|
import { ROUTES } from "@/utils/constants";
|
||||||
import { formatDate } from "@/utils/helpers";
|
import { formatDate } from "@/utils/helpers";
|
||||||
|
import { Modal } from "@/components/ui/Modal";
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const { user, isAuthenticated, refreshUser } = useAuth();
|
const { user, isAuthenticated, refreshUser, logout } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [isReady, setIsReady] = useState(false);
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
|
|
@ -72,6 +75,21 @@ export default function ProfilePage() {
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteAccount = async () => {
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
await userService.archiveAccount();
|
||||||
|
toast.success("Votre compte a été supprimé avec succès");
|
||||||
|
logout();
|
||||||
|
router.push(ROUTES.HOME);
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.message || "Erreur lors de la suppression du compte");
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getRoleBadgeVariant = (role: string) => {
|
const getRoleBadgeVariant = (role: string) => {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case "admin":
|
case "admin":
|
||||||
|
|
@ -145,13 +163,19 @@ export default function ProfilePage() {
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-4">
|
<div className="pt-4 flex flex-wrap gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsEditing(true)}
|
onClick={() => setIsEditing(true)}
|
||||||
className="bg-gradient-to-r from-[#d4a574] to-[#c4956a] hover:from-[#e5b685] hover:to-[#d4a574] text-white font-bold px-6 py-3 rounded-lg transition-all shadow-lg hover:scale-105 duration-300"
|
className="bg-gradient-to-r from-[#d4a574] to-[#c4956a] hover:from-[#e5b685] hover:to-[#d4a574] text-white font-bold px-6 py-3 rounded-lg transition-all shadow-lg hover:scale-105 duration-300"
|
||||||
>
|
>
|
||||||
Modifier mes informations
|
Modifier mes informations
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDeleteModal(true)}
|
||||||
|
className="bg-red-500 hover:bg-red-600 text-white font-bold px-6 py-3 rounded-lg transition-all shadow-lg hover:scale-105 duration-300"
|
||||||
|
>
|
||||||
|
Supprimer mon compte
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -217,18 +241,6 @@ export default function ProfilePage() {
|
||||||
<h2 className="text-xl font-bold text-[#5a5a4e]">Statut du compte</h2>
|
<h2 className="text-xl font-bold text-[#5a5a4e]">Statut du compte</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
<div>
|
|
||||||
<label className="text-sm font-medium text-[#8a8a7a]">
|
|
||||||
Email vérifié
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
{user.isVerified ? (
|
|
||||||
<Badge variant="success">Vérifié ✓</Badge>
|
|
||||||
) : (
|
|
||||||
<Badge variant="warning">Non vérifié</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-[#8a8a7a]">
|
<label className="text-sm font-medium text-[#8a8a7a]">
|
||||||
Membre depuis
|
Membre depuis
|
||||||
|
|
@ -280,9 +292,45 @@ export default function ProfilePage() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Modal */}
|
||||||
|
<Modal
|
||||||
|
isOpen={showDeleteModal}
|
||||||
|
onClose={() => setShowDeleteModal(false)}
|
||||||
|
title="Confirmer la suppression"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center w-12 h-12 mx-auto mb-4 bg-red-100 rounded-full">
|
||||||
|
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-center text-gray-900 mb-2">
|
||||||
|
Êtes-vous sûr de vouloir supprimer votre compte ?
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-center text-gray-600 mb-6">
|
||||||
|
Votre compte sera supprimé et vous ne pourrez plus vous connecter. Vos données seront conservées et un administrateur pourra réactiver votre compte si nécessaire.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDeleteModal(false)}
|
||||||
|
disabled={isDeleting}
|
||||||
|
className="flex-1 border-2 border-gray-300 hover:bg-gray-100 text-gray-700 font-bold px-6 py-3 rounded-lg transition-all"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDeleteAccount}
|
||||||
|
disabled={isDeleting}
|
||||||
|
className="flex-1 bg-red-500 hover:bg-red-600 disabled:bg-red-300 text-white font-bold px-6 py-3 rounded-lg transition-all"
|
||||||
|
>
|
||||||
|
{isDeleting ? "Suppression..." : "Supprimer"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { adminService } from '@/services/admin.service';
|
import { adminService } from '@/services/admin.service';
|
||||||
import { User, CreateEmployeeData, UpdateUserData, PaginatedResponse } from '@/types';
|
import { User, CreateEmployeeData, UpdateUserData, PaginatedResponse } from '@/types';
|
||||||
|
|
@ -13,6 +13,20 @@ export default function UserManagement() {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [totalPages, setTotalPages] = useState(1);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
const [filterRole, setFilterRole] = useState<string>('');
|
const [filterRole, setFilterRole] = useState<string>('');
|
||||||
|
const [filterStatus, setFilterStatus] = useState<string>('');
|
||||||
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||||
|
|
||||||
|
// Filtrage côté client par nom/email
|
||||||
|
const filteredUsers = useMemo(() => {
|
||||||
|
if (!searchQuery.trim()) return users;
|
||||||
|
const query = searchQuery.toLowerCase().trim();
|
||||||
|
return users.filter(user =>
|
||||||
|
user.firstName?.toLowerCase().includes(query) ||
|
||||||
|
user.lastName?.toLowerCase().includes(query) ||
|
||||||
|
user.email?.toLowerCase().includes(query) ||
|
||||||
|
`${user.firstName} ${user.lastName}`.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}, [users, searchQuery]);
|
||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
const [isCreateEmployeeModalOpen, setIsCreateEmployeeModalOpen] = useState(false);
|
const [isCreateEmployeeModalOpen, setIsCreateEmployeeModalOpen] = useState(false);
|
||||||
|
|
@ -33,10 +47,14 @@ export default function UserManagement() {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
const filters: any = {};
|
||||||
|
if (filterRole) filters.role = filterRole;
|
||||||
|
if (filterStatus) filters.isActive = filterStatus === 'active';
|
||||||
|
|
||||||
const response: PaginatedResponse<User> = await adminService.getAllUsers(
|
const response: PaginatedResponse<User> = await adminService.getAllUsers(
|
||||||
page,
|
page,
|
||||||
20,
|
100, // Charger plus pour le filtrage côté client
|
||||||
filterRole ? { role: filterRole } : undefined
|
Object.keys(filters).length > 0 ? filters : undefined
|
||||||
);
|
);
|
||||||
setUsers(response.data || []);
|
setUsers(response.data || []);
|
||||||
setTotalPages(response.totalPages || 1);
|
setTotalPages(response.totalPages || 1);
|
||||||
|
|
@ -46,7 +64,7 @@ export default function UserManagement() {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [page, filterRole]);
|
}, [page, filterRole, filterStatus]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadUsers();
|
loadUsers();
|
||||||
|
|
@ -68,7 +86,6 @@ export default function UserManagement() {
|
||||||
setEditingUser(user);
|
setEditingUser(user);
|
||||||
setUserFormData({
|
setUserFormData({
|
||||||
role: user.role,
|
role: user.role,
|
||||||
isVerified: user.isVerified,
|
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
});
|
});
|
||||||
|
|
@ -89,14 +106,15 @@ export default function UserManagement() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteUser = async (userId: string) => {
|
const handleToggleUserStatus = async (user: User) => {
|
||||||
if (!confirm('Êtes-vous sûr de vouloir supprimer cet utilisateur ?')) return;
|
const action = user.isActive ? 'désactiver' : 'réactiver';
|
||||||
|
if (!confirm(`Êtes-vous sûr de vouloir ${action} cet utilisateur ?`)) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await adminService.deleteUser(userId);
|
await adminService.updateUser(user.id, { isActive: !user.isActive });
|
||||||
loadUsers();
|
loadUsers();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert(err.message || 'Erreur lors de la suppression');
|
alert(err.message || `Erreur lors de la ${action}ation`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -136,7 +154,16 @@ export default function UserManagement() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Filtres */}
|
{/* Filtres */}
|
||||||
<div className="mb-4">
|
<div className="mb-4 flex flex-wrap gap-4">
|
||||||
|
<div className="flex-1 min-w-[250px]">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Rechercher par nom ou email..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="w-full border rounded px-3 py-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<select
|
<select
|
||||||
value={filterRole}
|
value={filterRole}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
@ -150,6 +177,18 @@ export default function UserManagement() {
|
||||||
<option value="EMPLOYEE">Employés</option>
|
<option value="EMPLOYEE">Employés</option>
|
||||||
<option value="ADMIN">Administrateurs</option>
|
<option value="ADMIN">Administrateurs</option>
|
||||||
</select>
|
</select>
|
||||||
|
<select
|
||||||
|
value={filterStatus}
|
||||||
|
onChange={(e) => {
|
||||||
|
setFilterStatus(e.target.value);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
className="border rounded px-3 py-2"
|
||||||
|
>
|
||||||
|
<option value="">Tous les statuts</option>
|
||||||
|
<option value="active">Actifs</option>
|
||||||
|
<option value="inactive">Inactifs (archivés)</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table des utilisateurs */}
|
{/* Table des utilisateurs */}
|
||||||
|
|
@ -178,14 +217,14 @@ export default function UserManagement() {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
{users.length === 0 ? (
|
{filteredUsers.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} className="px-6 py-8 text-center text-gray-500">
|
<td colSpan={6} className="px-6 py-8 text-center text-gray-500">
|
||||||
Aucun utilisateur trouvé
|
{searchQuery ? 'Aucun résultat pour cette recherche' : 'Aucun utilisateur trouvé'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
users.map((user) => (
|
filteredUsers.map((user) => (
|
||||||
<tr key={user.id} className="hover:bg-gray-50">
|
<tr key={user.id} className="hover:bg-gray-50">
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm font-medium text-gray-900">
|
<div className="text-sm font-medium text-gray-900">
|
||||||
|
|
@ -201,31 +240,19 @@ export default function UserManagement() {
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${user.isVerified ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}`}>
|
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${user.isActive !== false ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||||
{user.isVerified ? 'Vérifié' : 'Non vérifié'}
|
{user.isActive !== false ? 'Actif' : 'Inactif'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
{new Date(user.createdAt).toLocaleDateString('fr-FR')}
|
{new Date(user.createdAt).toLocaleDateString('fr-FR')}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push(`/admin/utilisateurs/${user.id}`)}
|
onClick={() => router.push(`/admin/utilisateurs/${user.id}`)}
|
||||||
className="text-green-600 hover:text-green-900"
|
|
||||||
>
|
|
||||||
Détails
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleEditUser(user)}
|
|
||||||
className="text-blue-600 hover:text-blue-900"
|
className="text-blue-600 hover:text-blue-900"
|
||||||
>
|
>
|
||||||
Modifier
|
Détails
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDeleteUser(user.id)}
|
|
||||||
className="text-red-600 hover:text-red-900"
|
|
||||||
>
|
|
||||||
Supprimer
|
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -340,18 +367,6 @@ export default function UserManagement() {
|
||||||
<option value="ADMIN">Administrateur</option>
|
<option value="ADMIN">Administrateur</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="isVerified"
|
|
||||||
checked={userFormData.isVerified}
|
|
||||||
onChange={(e) => setUserFormData({ ...userFormData, isVerified: e.target.checked })}
|
|
||||||
className="rounded"
|
|
||||||
/>
|
|
||||||
<label htmlFor="isVerified" className="text-sm font-medium">
|
|
||||||
Email vérifié
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 pt-4">
|
<div className="flex gap-2 pt-4">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
||||||
|
|
@ -134,12 +134,16 @@ export const adminService = {
|
||||||
limit = 20,
|
limit = 20,
|
||||||
filters?: {
|
filters?: {
|
||||||
role?: string;
|
role?: string;
|
||||||
|
search?: string;
|
||||||
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
): Promise<PaginatedResponse<User>> => {
|
): Promise<PaginatedResponse<User>> => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
page: page.toString(),
|
page: page.toString(),
|
||||||
limit: limit.toString(),
|
limit: limit.toString(),
|
||||||
...(filters?.role && { role: filters.role }),
|
...(filters?.role && { role: filters.role }),
|
||||||
|
...(filters?.search && { search: filters.search }),
|
||||||
|
...(filters?.isActive !== undefined && { isActive: filters.isActive.toString() }),
|
||||||
});
|
});
|
||||||
const response = await api.get<any>(
|
const response = await api.get<any>(
|
||||||
`${API_ENDPOINTS.USERS}?${params}`
|
`${API_ENDPOINTS.USERS}?${params}`
|
||||||
|
|
@ -161,6 +165,7 @@ export const adminService = {
|
||||||
postalCode: user.postal_code || user.postalCode,
|
postalCode: user.postal_code || user.postalCode,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
isVerified: user.is_verified !== undefined ? user.is_verified : user.isVerified,
|
isVerified: user.is_verified !== undefined ? user.is_verified : user.isVerified,
|
||||||
|
isActive: user.is_active !== undefined ? user.is_active : (user.isActive !== undefined ? user.isActive : true),
|
||||||
createdAt: user.created_at || user.createdAt,
|
createdAt: user.created_at || user.createdAt,
|
||||||
ticketsCount: user.tickets_count || user.ticketsCount || 0,
|
ticketsCount: user.tickets_count || user.ticketsCount || 0,
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -35,4 +35,14 @@ export const userService = {
|
||||||
changePassword: async (data: ChangePasswordData): Promise<void> => {
|
changePassword: async (data: ChangePasswordData): Promise<void> => {
|
||||||
await api.post(API_ENDPOINTS.USER.CHANGE_PASSWORD, data);
|
await api.post(API_ENDPOINTS.USER.CHANGE_PASSWORD, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Archive account (désactive le compte au lieu de le supprimer)
|
||||||
|
archiveAccount: async (): Promise<void> => {
|
||||||
|
await api.put(API_ENDPOINTS.USER.UPDATE_PROFILE, { isActive: false });
|
||||||
|
},
|
||||||
|
|
||||||
|
// Delete account (kept for backwards compatibility)
|
||||||
|
deleteAccount: async (): Promise<void> => {
|
||||||
|
await api.delete(API_ENDPOINTS.USER.DELETE_ACCOUNT);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export interface User {
|
||||||
dateOfBirth?: string;
|
dateOfBirth?: string;
|
||||||
role: 'CLIENT' | 'EMPLOYEE' | 'ADMIN';
|
role: 'CLIENT' | 'EMPLOYEE' | 'ADMIN';
|
||||||
isVerified: boolean;
|
isVerified: boolean;
|
||||||
|
isActive: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
// Ticket statistics (populated in getUserById)
|
// Ticket statistics (populated in getUserById)
|
||||||
|
|
@ -171,6 +172,7 @@ export interface CreateEmployeeData {
|
||||||
export interface UpdateUserData {
|
export interface UpdateUserData {
|
||||||
role?: 'CLIENT' | 'EMPLOYEE' | 'ADMIN';
|
role?: 'CLIENT' | 'EMPLOYEE' | 'ADMIN';
|
||||||
isVerified?: boolean;
|
isVerified?: boolean;
|
||||||
|
isActive?: boolean;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ export const API_ENDPOINTS = {
|
||||||
PROFILE: '/users/profile',
|
PROFILE: '/users/profile',
|
||||||
UPDATE_PROFILE: '/users/profile',
|
UPDATE_PROFILE: '/users/profile',
|
||||||
CHANGE_PASSWORD: '/users/change-password',
|
CHANGE_PASSWORD: '/users/change-password',
|
||||||
|
DELETE_ACCOUNT: '/users/account',
|
||||||
},
|
},
|
||||||
NEWSLETTER: {
|
NEWSLETTER: {
|
||||||
SUBSCRIBE: '/newsletter/subscribe',
|
SUBSCRIBE: '/newsletter/subscribe',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user