feat: add user details page and getUserById service method
Created comprehensive user details page at /admin/utilisateurs/[id] that displays contact information, personal data, account status, and ticket statistics. Added getUserById method to admin service to fetch detailed user information from the backend API. Fixes the "Détails" button that was previously navigating to a non-existent page. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9e55042d1f
commit
3456657ae5
262
app/admin/utilisateurs/[id]/page.tsx
Normal file
262
app/admin/utilisateurs/[id]/page.tsx
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { adminService } from '@/services/admin.service';
|
||||
import { User } from '@/types';
|
||||
import { ArrowLeft, Mail, Phone, MapPin, Calendar, Award, Ticket, CheckCircle, Clock } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export default function UserDetailsPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const userId = params.id as string;
|
||||
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadUserDetails();
|
||||
}, [userId]);
|
||||
|
||||
const loadUserDetails = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const userData = await adminService.getUserById(userId);
|
||||
setUser(userData);
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Erreur lors du chargement des détails');
|
||||
toast.error('Erreur lors du chargement des détails de l\'utilisateur');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getRoleBadgeColor = (role: string) => {
|
||||
switch (role) {
|
||||
case 'ADMIN':
|
||||
return 'bg-red-100 text-red-800';
|
||||
case 'EMPLOYEE':
|
||||
return 'bg-blue-100 text-blue-800';
|
||||
case 'CLIENT':
|
||||
return 'bg-green-100 text-green-800';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-8 bg-gray-200 rounded w-1/4"></div>
|
||||
<div className="h-64 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !user) {
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||
{error || 'Utilisateur non trouvé'}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="mt-4 flex items-center gap-2 text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Retour
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-8 bg-gray-50 min-h-screen">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="flex items-center gap-2 text-gray-600 hover:text-gray-800 mb-4"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Retour à la liste
|
||||
</button>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
{user.firstName} {user.lastName}
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-1">Détails de l'utilisateur</p>
|
||||
</div>
|
||||
<span className={`px-4 py-2 rounded-full text-sm font-semibold ${getRoleBadgeColor(user.role)}`}>
|
||||
{user.role}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Informations principales */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Informations de contact */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Informations de contact</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<Mail className="w-5 h-5 text-gray-400 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Email</p>
|
||||
<p className="text-base text-gray-900">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
{user.phone && (
|
||||
<div className="flex items-start gap-3">
|
||||
<Phone className="w-5 h-5 text-gray-400 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Téléphone</p>
|
||||
<p className="text-base text-gray-900">{user.phone}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(user.address || user.city || user.postalCode) && (
|
||||
<div className="flex items-start gap-3">
|
||||
<MapPin className="w-5 h-5 text-gray-400 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Adresse</p>
|
||||
<p className="text-base text-gray-900">
|
||||
{user.address && <>{user.address}<br /></>}
|
||||
{user.postalCode && user.city && `${user.postalCode} ${user.city}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Informations personnelles */}
|
||||
{(user.dateOfBirth || user.gender) && (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Informations personnelles</h2>
|
||||
<div className="space-y-4">
|
||||
{user.dateOfBirth && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Date de naissance</p>
|
||||
<p className="text-base text-gray-900">
|
||||
{new Date(user.dateOfBirth).toLocaleDateString('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{user.gender && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Genre</p>
|
||||
<p className="text-base text-gray-900">
|
||||
{user.gender === 'MALE' ? 'Homme' :
|
||||
user.gender === 'FEMALE' ? 'Femme' :
|
||||
user.gender === 'OTHER' ? 'Autre' : 'Non spécifié'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Statistiques de tickets */}
|
||||
{user.role === 'CLIENT' && (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Activité des tickets</h2>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="text-center p-4 bg-blue-50 rounded-lg">
|
||||
<Ticket className="w-8 h-8 text-blue-600 mx-auto mb-2" />
|
||||
<p className="text-2xl font-bold text-blue-600">{user.ticketsCount || 0}</p>
|
||||
<p className="text-sm text-gray-600">Total tickets</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-yellow-50 rounded-lg">
|
||||
<Clock className="w-8 h-8 text-yellow-600 mx-auto mb-2" />
|
||||
<p className="text-2xl font-bold text-yellow-600">{user.pendingTickets || 0}</p>
|
||||
<p className="text-sm text-gray-600">En attente</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-green-50 rounded-lg">
|
||||
<CheckCircle className="w-8 h-8 text-green-600 mx-auto mb-2" />
|
||||
<p className="text-2xl font-bold text-green-600">{user.claimedTickets || 0}</p>
|
||||
<p className="text-sm text-gray-600">Réclamés</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Statut du compte */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Statut du compte</h2>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600 mb-1">Vérification email</p>
|
||||
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold ${
|
||||
user.isVerified
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-yellow-100 text-yellow-800'
|
||||
}`}>
|
||||
{user.isVerified ? (
|
||||
<>
|
||||
<CheckCircle className="w-4 h-4 mr-1" />
|
||||
Vérifié
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Clock className="w-4 h-4 mr-1" />
|
||||
Non vérifié
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600 mb-1">Membre depuis</p>
|
||||
<div className="flex items-center gap-2 text-gray-900">
|
||||
<Calendar className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-sm">
|
||||
{new Date(user.createdAt).toLocaleDateString('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions rapides */}
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Actions</h2>
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={() => router.push(`/admin/utilisateurs?edit=${user.id}`)}
|
||||
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Modifier l'utilisateur
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
@ -96,6 +96,36 @@ export const adminService = {
|
|||
|
||||
// ==================== GESTION DES UTILISATEURS ====================
|
||||
|
||||
/**
|
||||
* Récupérer un utilisateur par ID
|
||||
*/
|
||||
getUserById: async (userId: string): Promise<User> => {
|
||||
const response = await api.get<ApiResponse<any>>(
|
||||
`${API_ENDPOINTS.USERS}/${userId}`
|
||||
);
|
||||
|
||||
// Convert snake_case to camelCase
|
||||
const user = response.data;
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.first_name || user.firstName,
|
||||
lastName: user.last_name || user.lastName,
|
||||
phone: user.phone,
|
||||
address: user.address,
|
||||
city: user.city,
|
||||
postalCode: user.postal_code || user.postalCode,
|
||||
role: user.role,
|
||||
isVerified: user.is_verified !== undefined ? user.is_verified : user.isVerified,
|
||||
createdAt: user.created_at || user.createdAt,
|
||||
ticketsCount: user.tickets_count || user.ticketsCount || 0,
|
||||
pendingTickets: user.pending_tickets || user.pendingTickets || 0,
|
||||
claimedTickets: user.claimed_tickets || user.claimedTickets || 0,
|
||||
dateOfBirth: user.date_of_birth || user.dateOfBirth,
|
||||
gender: user.gender,
|
||||
} as User;
|
||||
},
|
||||
|
||||
/**
|
||||
* Récupérer tous les utilisateurs (paginé)
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user