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:
soufiane 2025-11-19 15:09:00 +01:00
parent 9e55042d1f
commit 3456657ae5
2 changed files with 292 additions and 0 deletions

View 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>
);
}

View File

@ -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é)
*/