feat: redesign admin panel with blanc cassé theme

- Update sidebar and header with off-white (#faf8f5) background
- Add ticket stats endpoint integration for global counts
- Redesign tirages page with animation and improved layout
- Add red accent color for admin avatar
- Update various button styles and remove unnecessary elements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
soufiane 2025-12-03 19:43:14 +01:00
parent aa1d8b1d66
commit f20cf40fff
13 changed files with 324 additions and 297 deletions

View File

@ -52,20 +52,13 @@ export default function AdminLayout({
<Sidebar /> <Sidebar />
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
{/* Admin Header */} {/* Admin Header */}
<header className="bg-white shadow-sm border-b border-gray-200 px-6 py-4"> <header className="bg-[#faf8f5] shadow-sm border-b border-gray-200 px-6 py-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-end">
<div className="flex items-center gap-4">
<Logo size="md" showText={false} />
<div>
<h1 className="text-2xl font-bold text-gray-900">Thé Tip Top - Administration</h1>
</div>
</div>
<UserDropdown <UserDropdown
user={user} user={user}
profilePath="/admin/profil" profilePath="/admin/profil"
onLogout={handleLogout} onLogout={handleLogout}
accentColor="blue" accentColor="red"
/> />
</div> </div>
</header> </header>

View File

@ -1,17 +1,21 @@
'use client'; 'use client';
import PrizeManagement from '@/components/admin/PrizeManagement'; import PrizeManagement from '@/components/admin/PrizeManagement';
import { Gift } from 'lucide-react';
export default function LotsPage() { export default function LotsPage() {
return ( return (
<div className="min-h-full bg-gradient-to-br from-gray-50 via-white to-gray-50 p-8"> <div className="min-h-full bg-gradient-to-br from-gray-50 via-white to-gray-50 p-8">
<div className="mb-8"> <div className="bg-gradient-to-r from-purple-600 to-pink-600 rounded-2xl p-6 mb-8">
<h1 className="text-4xl font-bold text-[#1e3a5f] mb-2"> <div className="flex items-center gap-4">
Gestion des Lots & Prix <div className="w-14 h-14 bg-white/20 rounded-xl flex items-center justify-center">
</h1> <Gift className="w-7 h-7 text-white" />
<p className="text-gray-600 text-lg"> </div>
Gérez les lots et prix du jeu-concours <div>
</p> <h1 className="text-3xl font-bold text-white">Gestion des Lots & Prix</h1>
<p className="text-purple-200">Gérez les lots et prix du jeu-concours</p>
</div>
</div>
</div> </div>
<div className="bg-white rounded-2xl shadow-md border border-gray-100"> <div className="bg-white rounded-2xl shadow-md border border-gray-100">
<PrizeManagement /> <PrizeManagement />

View File

@ -227,14 +227,21 @@ export default function MarketingPage() {
return ( return (
<div className="min-h-full bg-gradient-to-br from-gray-50 via-white to-gray-50 p-8"> <div className="min-h-full bg-gradient-to-br from-gray-50 via-white to-gray-50 p-8">
{/* En-tête */} {/* En-tête */}
<div className="mb-8"> <div className="bg-gradient-to-r from-indigo-600 to-purple-600 rounded-2xl p-6 mb-8">
<h1 className="text-4xl font-bold text-[#1e3a5f] mb-2"> <div className="flex items-center gap-4">
<div className="w-14 h-14 bg-white/20 rounded-xl flex items-center justify-center">
<Mail className="w-7 h-7 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold text-white">
Données Marketing Données Marketing
</h1> </h1>
<p className="text-gray-600 text-lg"> <p className="text-indigo-200">
Statistiques et export des données pour vos campagnes d'emailing Statistiques et export des données pour vos campagnes d'emailing
</p> </p>
</div> </div>
</div>
</div>
{/* Statistiques globales */} {/* Statistiques globales */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
@ -387,11 +394,11 @@ export default function MarketingPage() {
)} )}
{/* Section Export */} {/* Section Export */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden"> <div className="bg-[#faf8f5] rounded-2xl border border-gray-200 overflow-hidden">
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 px-6 py-4"> <div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-xl font-bold text-white flex items-center gap-3"> <h2 className="text-xl font-bold text-[#1e3a5f] flex items-center gap-3">
<div className="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center"> <div className="w-10 h-10 bg-white rounded-xl flex items-center justify-center border border-gray-200">
<Download className="w-5 h-5 text-white" /> <Download className="w-5 h-5 text-[#1e3a5f]" />
</div> </div>
Exporter les Données pour Emailing Exporter les Données pour Emailing
</h2> </h2>
@ -406,7 +413,7 @@ export default function MarketingPage() {
<select <select
value={selectedSegment} value={selectedSegment}
onChange={(e) => setSelectedSegment(e.target.value)} onChange={(e) => setSelectedSegment(e.target.value)}
className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-4 py-3 bg-white border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent"
> >
<option value="all">Tous les clients ({stats.totalClients})</option> <option value="all">Tous les clients ({stats.totalClients})</option>
<option value="active"> <option value="active">
@ -429,7 +436,7 @@ export default function MarketingPage() {
<select <select
value={filters.city} value={filters.city}
onChange={(e) => setFilters({ ...filters, city: e.target.value })} onChange={(e) => setFilters({ ...filters, city: e.target.value })}
className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-4 py-3 bg-white border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent"
> >
<option value="">Toutes les villes</option> <option value="">Toutes les villes</option>
{filteredCityData.slice(0, 10).map((city) => ( {filteredCityData.slice(0, 10).map((city) => (
@ -447,7 +454,7 @@ export default function MarketingPage() {
<select <select
value={filters.gender} value={filters.gender}
onChange={(e) => setFilters({ ...filters, gender: e.target.value })} onChange={(e) => setFilters({ ...filters, gender: e.target.value })}
className="w-full px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-4 py-3 bg-white border border-gray-200 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent"
> >
<option value="">Tous les genres</option> <option value="">Tous les genres</option>
{filteredGenderData.map((g) => ( {filteredGenderData.map((g) => (
@ -467,24 +474,24 @@ export default function MarketingPage() {
onClick={exportSegmentData} onClick={exportSegmentData}
isLoading={exportLoading} isLoading={exportLoading}
disabled={exportLoading} disabled={exportLoading}
className="bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700" className="!bg-white !text-black border border-gray-200 hover:!bg-gray-100"
> >
<Download className="w-5 h-5 mr-2" /> <Download className="w-5 h-5 mr-2" />
Exporter en CSV Exporter en CSV
</Button> </Button>
<Button variant="outline" onClick={loadMarketingStats}> <Button variant="outline" onClick={loadMarketingStats} className="bg-white">
<FileText className="w-5 h-5 mr-2" /> <FileText className="w-5 h-5 mr-2" />
Actualiser les données Actualiser les données
</Button> </Button>
</div> </div>
<div className="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100 rounded-xl"> <div className="p-4 bg-white border border-gray-200 rounded-xl">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0"> <div className="w-8 h-8 bg-gray-100 rounded-lg flex items-center justify-center flex-shrink-0">
<Mail className="w-4 h-4 text-blue-600" /> <Mail className="w-4 h-4 text-gray-600" />
</div> </div>
<p className="text-sm text-blue-800"> <p className="text-sm text-gray-600">
<strong>Note:</strong> L'export générera un fichier CSV avec les emails, noms, <strong>Note:</strong> L'export générera un fichier CSV avec les emails, noms,
prénoms et autres données de contact. Utilisez ce fichier pour vos campagnes prénoms et autres données de contact. Utilisez ce fichier pour vos campagnes
d'emailing avec votre outil préféré (Mailchimp, SendGrid, etc.). d'emailing avec votre outil préféré (Mailchimp, SendGrid, etc.).

View File

@ -1,17 +1,21 @@
"use client"; "use client";
import TicketManagement from "@/components/admin/TicketManagement"; import TicketManagement from "@/components/admin/TicketManagement";
import { Ticket } from "lucide-react";
export default function AdminTicketsPage() { export default function AdminTicketsPage() {
return ( return (
<div className="min-h-full bg-gradient-to-br from-gray-50 via-white to-gray-50 p-8"> <div className="min-h-full bg-gradient-to-br from-gray-50 via-white to-gray-50 p-8">
<div className="mb-8"> <div className="bg-gradient-to-r from-emerald-600 to-teal-600 rounded-2xl p-6 mb-8">
<h1 className="text-4xl font-bold text-[#1e3a5f] mb-2"> <div className="flex items-center gap-4">
Gestion des tickets <div className="w-14 h-14 bg-white/20 rounded-xl flex items-center justify-center">
</h1> <Ticket className="w-7 h-7 text-white" />
<p className="text-gray-600 text-lg"> </div>
Consultez et gérez tous les tickets du jeu-concours <div>
</p> <h1 className="text-3xl font-bold text-white">Gestion des Tickets</h1>
<p className="text-emerald-200">Consultez et gérez tous les tickets du jeu-concours</p>
</div>
</div>
</div> </div>
<div className="bg-white rounded-2xl shadow-md border border-gray-100"> <div className="bg-white rounded-2xl shadow-md border border-gray-100">
<TicketManagement /> <TicketManagement />

View File

@ -71,6 +71,7 @@ export default function TiragesPage() {
const [drawResult, setDrawResult] = useState<DrawResult | null>(null); const [drawResult, setDrawResult] = useState<DrawResult | null>(null);
const [existingDraw, setExistingDraw] = useState<ExistingDraw | null>(null); const [existingDraw, setExistingDraw] = useState<ExistingDraw | null>(null);
const [hasExistingDraw, setHasExistingDraw] = useState(false); const [hasExistingDraw, setHasExistingDraw] = useState(false);
const [isDrawing, setIsDrawing] = useState(false);
// Critères // Critères
const [minTickets, setMinTickets] = useState(1); const [minTickets, setMinTickets] = useState(1);
@ -136,13 +137,13 @@ export default function TiragesPage() {
return; return;
} }
const confirmMessage = hasExistingDraw
? `⚠️ ATTENTION: Un tirage a déjà été effectué!\n\nÊtes-vous ABSOLUMENT SÛR de vouloir effectuer un nouveau tirage parmi ${participants.length} participants éligibles?\n\nCeci remplacera le tirage précédent!`
: `Êtes-vous sûr de vouloir lancer le tirage au sort parmi ${participants.length} participants éligibles?\n\nCette action est irréversible!`;
if (!confirm(confirmMessage)) return;
setLoading(true); setLoading(true);
setIsDrawing(true);
setDrawResult(null);
// Animation de tirage (3 secondes)
await new Promise(resolve => setTimeout(resolve, 3000));
try { try {
const token = localStorage.getItem('auth_token') || localStorage.getItem('token'); const token = localStorage.getItem('auth_token') || localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/draw/conduct`, { const response = await fetch(`${API_BASE_URL}/draw/conduct`, {
@ -172,10 +173,13 @@ export default function TiragesPage() {
setHasExistingDraw(true); setHasExistingDraw(true);
toast.success('🎉 Tirage au sort effectué avec succès!'); toast.success('🎉 Tirage au sort effectué avec succès!');
await checkExistingDraw(); await checkExistingDraw();
// Scroll vers le haut pour voir le résultat
window.scrollTo({ top: 0, behavior: 'smooth' });
} catch (error: any) { } catch (error: any) {
toast.error(error.message || 'Erreur lors du tirage'); toast.error(error.message || 'Erreur lors du tirage');
} finally { } finally {
setLoading(false); setLoading(false);
setIsDrawing(false);
} }
}; };
@ -317,12 +321,6 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
const deleteDraw = async () => { const deleteDraw = async () => {
if (!existingDraw) return; if (!existingDraw) return;
const confirmMessage = `⚠️ ATTENTION: Cette action est IRRÉVERSIBLE!\n\nVoulez-vous vraiment annuler ce tirage au sort?\n\nGagnant: ${existingDraw.winner_name}\nEmail: ${existingDraw.winner_email}\nPrix: ${existingDraw.prize_name}`;
if (!confirm(confirmMessage)) {
return;
}
setLoading(true); setLoading(true);
try { try {
const token = localStorage.getItem('auth_token') || localStorage.getItem('token'); const token = localStorage.getItem('auth_token') || localStorage.getItem('token');
@ -365,76 +363,176 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
return ( return (
<div className="min-h-full bg-gradient-to-br from-gray-50 via-white to-gray-50 p-8"> <div className="min-h-full bg-gradient-to-br from-gray-50 via-white to-gray-50 p-8">
{/* Animation de tirage en cours */}
{isDrawing && (
<div className="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center">
<div className="bg-white rounded-3xl p-12 text-center max-w-md mx-4 animate-pulse">
<div className="w-24 h-24 bg-gradient-to-br from-amber-400 to-orange-500 rounded-full flex items-center justify-center mx-auto mb-6 animate-spin">
<Trophy className="w-12 h-12 text-white" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">Tirage en cours...</h2>
<p className="text-gray-500">Sélection aléatoire du gagnant parmi {participants.length} participants</p>
<div className="mt-6 flex justify-center gap-2">
<div className="w-3 h-3 bg-amber-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
<div className="w-3 h-3 bg-orange-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
<div className="w-3 h-3 bg-red-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
</div>
</div>
</div>
)}
{/* En-tête avec titre du prix */} {/* En-tête avec titre du prix */}
<div className="mb-8"> <div className="bg-gradient-to-r from-amber-500 to-orange-600 rounded-2xl p-6 mb-8">
<h1 className="text-4xl font-bold text-[#1e3a5f] mb-2"> <div className="flex items-center gap-4">
Tirage au Sort - {prizeName} <div className="w-14 h-14 bg-white/20 rounded-xl flex items-center justify-center">
</h1> <Trophy className="w-7 h-7 text-white" />
<p className="text-gray-600 text-lg"> </div>
Prix à gagner : <span className="font-bold text-purple-600">{prizeValue}</span> <div>
<h1 className="text-3xl font-bold text-white">Tirage au Sort - {prizeName}</h1>
<p className="text-amber-100">
Prix à gagner : <span className="font-semibold text-white">{prizeValue}</span>
Participants ayant joué au moins {minTickets} ticket{minTickets > 1 ? 's' : ''} Participants ayant joué au moins {minTickets} ticket{minTickets > 1 ? 's' : ''}
</p> </p>
</div> </div>
</div>
</div>
{/* Résultat du tirage - Affiché en haut */}
{drawResult && (
<div className="mb-8 bg-gradient-to-br from-amber-50 via-yellow-50 to-orange-50 rounded-2xl border-2 border-amber-300 overflow-hidden animate-fadeIn">
<div className="bg-gradient-to-r from-amber-500 to-orange-500 px-6 py-4 text-center">
<Trophy className="w-12 h-12 text-white mx-auto mb-2" />
<h2 className="text-2xl font-bold text-white">
🎉 Félicitations au gagnant !
</h2>
</div>
<div className="p-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
{/* Gagnant */}
<div className="bg-white rounded-2xl p-6 shadow-lg border border-blue-100">
<div className="flex items-center gap-4 mb-4">
<div className="w-16 h-16 bg-gradient-to-br from-emerald-500 to-teal-600 rounded-full flex items-center justify-center text-white font-bold text-xl">
{drawResult.winner.name.split(' ').map(n => n[0]).join('')}
</div>
<div>
<p className="text-xs text-emerald-600 font-semibold uppercase">Gagnant</p>
<h3 className="text-xl font-bold text-gray-900">
{drawResult.winner.name}
</h3>
</div>
</div>
<p className="text-sm text-gray-600">{drawResult.winner.email}</p>
</div>
{/* Prix */}
<div className="bg-white rounded-2xl p-6 shadow-lg border border-purple-100">
<div className="flex items-center gap-4 mb-4">
<div className="w-16 h-16 bg-gradient-to-br from-purple-500 to-pink-600 rounded-full flex items-center justify-center">
<Gift className="w-8 h-8 text-white" />
</div>
<div>
<p className="text-xs text-purple-600 font-semibold uppercase">Prix gagné</p>
<h3 className="text-xl font-bold text-gray-900">
{drawResult.prize.name}
</h3>
</div>
</div>
<p className="text-3xl font-bold text-purple-600">
{drawResult.prize.value}
</p>
</div>
</div>
<div className="flex justify-center">
<Button onClick={downloadReport} className="!bg-gradient-to-r !from-amber-500 !to-orange-600 !text-white hover:!from-amber-600 hover:!to-orange-700 rounded-xl">
<Download className="w-4 h-4 mr-2" />
Télécharger le rapport
</Button>
</div>
</div>
</div>
)}
{/* Alerte si un tirage existe déjà */} {/* Alerte si un tirage existe déjà */}
{hasExistingDraw && existingDraw && ( {hasExistingDraw && existingDraw && !drawResult && (
<div className="mb-6 bg-gradient-to-r from-amber-50 to-yellow-50 rounded-2xl border-2 border-amber-200 overflow-hidden"> <div className="mb-8 bg-[#faf8f5] rounded-2xl border border-gray-200 overflow-hidden">
<div className="bg-gradient-to-r from-amber-500 to-yellow-500 px-6 py-3"> {/* Header */}
<div className="flex items-center gap-3"> <div className="bg-gradient-to-r from-amber-500 to-orange-600 px-6 py-4">
<div className="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center"> <div className="flex items-center justify-between">
<Trophy className="w-5 h-5 text-white" /> <div className="flex items-center gap-4">
<div className="w-14 h-14 bg-white/20 rounded-xl flex items-center justify-center">
<Trophy className="w-7 h-7 text-white" />
</div> </div>
<h3 className="font-bold text-white text-lg">Tirage déjà effectué</h3> <div>
<h3 className="font-bold text-white text-xl">Tirage effectué</h3>
<p className="text-amber-100 text-sm">{new Date(existingDraw.draw_date).toLocaleString('fr-FR')}</p>
</div> </div>
</div> </div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<div className="bg-white p-4 rounded-xl border border-amber-100">
<p className="text-xs text-gray-500 uppercase font-semibold mb-1">Date</p>
<p className="font-bold text-gray-900">{new Date(existingDraw.draw_date).toLocaleString('fr-FR')}</p>
</div>
<div className="bg-white p-4 rounded-xl border border-amber-100">
<p className="text-xs text-gray-500 uppercase font-semibold mb-1">Gagnant</p>
<p className="font-bold text-gray-900">{existingDraw.winner_name}</p>
<p className="text-xs text-gray-500">{existingDraw.winner_email}</p>
</div>
<div className="bg-white p-4 rounded-xl border border-amber-100">
<p className="text-xs text-gray-500 uppercase font-semibold mb-1">Prix</p>
<p className="font-bold text-gray-900">{existingDraw.prize_name}</p>
<p className="text-sm text-purple-600 font-semibold">{existingDraw.prize_value}</p>
</div>
<div className="bg-white p-4 rounded-xl border border-amber-100">
<p className="text-xs text-gray-500 uppercase font-semibold mb-1">Statut</p>
<span
className={`inline-flex px-3 py-1 rounded-full text-sm font-semibold ${
existingDraw.status === 'CLAIMED'
? 'bg-green-100 text-green-700'
: existingDraw.status === 'NOTIFIED'
? 'bg-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-700'
}`}
>
{existingDraw.status === 'CLAIMED' ? 'Récupéré' : existingDraw.status === 'NOTIFIED' ? 'Notifié' : existingDraw.status}
</span>
</div> </div>
</div> </div>
<div className="flex gap-2 flex-wrap"> {/* Content */}
<Button onClick={downloadReport} size="sm" variant="outline" className="rounded-xl"> <div className="p-6">
<Download className="w-4 h-4 mr-1" /> <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
{/* Gagnant */}
<div className="bg-white rounded-2xl p-5 shadow-sm border border-gray-100">
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-gradient-to-br from-emerald-500 to-teal-600 rounded-full flex items-center justify-center text-white font-bold text-lg">
{existingDraw.winner_name.split(' ').map(n => n[0]).join('')}
</div>
<div>
<p className="text-xs text-emerald-600 font-semibold uppercase mb-1">Gagnant</p>
<p className="font-bold text-gray-900 text-lg">{existingDraw.winner_name}</p>
<p className="text-sm text-gray-500">{existingDraw.winner_email}</p>
</div>
</div>
</div>
{/* Prix */}
<div className="bg-white rounded-2xl p-5 shadow-sm border border-gray-100">
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-gradient-to-br from-purple-500 to-pink-600 rounded-full flex items-center justify-center">
<Gift className="w-7 h-7 text-white" />
</div>
<div>
<p className="text-xs text-purple-600 font-semibold uppercase mb-1">Prix gagné</p>
<p className="font-bold text-gray-900 text-lg">{existingDraw.prize_name}</p>
<p className="text-2xl font-bold text-purple-600">{existingDraw.prize_value}</p>
</div>
</div>
</div>
</div>
{/* Actions */}
<div className="flex gap-3 flex-wrap">
<Button
onClick={downloadReport}
size="sm"
className="!bg-gradient-to-r !from-amber-500 !to-orange-600 !text-white hover:!from-amber-600 hover:!to-orange-700 rounded-xl"
>
<Download className="w-4 h-4 mr-2" />
Télécharger rapport Télécharger rapport
</Button> </Button>
{existingDraw.status === 'COMPLETED' && ( {existingDraw.status === 'COMPLETED' && (
<Button onClick={markAsNotified} size="sm" variant="outline" className="rounded-xl"> <Button
<Mail className="w-4 h-4 mr-1" /> onClick={markAsNotified}
size="sm"
className="!bg-white !text-black border border-gray-200 hover:!bg-gray-100 rounded-xl"
>
<Mail className="w-4 h-4 mr-2" />
Marquer comme notifié Marquer comme notifié
</Button> </Button>
)} )}
{existingDraw.status === 'NOTIFIED' && ( {existingDraw.status === 'NOTIFIED' && (
<Button onClick={markAsClaimed} size="sm" variant="outline" className="rounded-xl"> <Button
<CheckCircle className="w-4 h-4 mr-1" /> onClick={markAsClaimed}
size="sm"
className="!bg-white !text-black border border-gray-200 hover:!bg-gray-100 rounded-xl"
>
<CheckCircle className="w-4 h-4 mr-2" />
Marquer comme récupéré Marquer comme récupéré
</Button> </Button>
)} )}
@ -442,10 +540,9 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
<Button <Button
onClick={deleteDraw} onClick={deleteDraw}
size="sm" size="sm"
variant="outline" className="!bg-white !text-red-600 border border-red-200 hover:!bg-red-50 rounded-xl"
className="border-red-300 text-red-700 hover:bg-red-50 hover:border-red-400 rounded-xl"
> >
<Trash2 className="w-4 h-4 mr-1" /> <Trash2 className="w-4 h-4 mr-2" />
Annuler ce tirage Annuler ce tirage
</Button> </Button>
</div> </div>
@ -454,16 +551,16 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
)} )}
{/* Liste des participants éligibles */} {/* Liste des participants éligibles */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 mb-6 overflow-hidden"> <div className="bg-[#faf8f5] rounded-2xl border border-gray-200 mb-6 overflow-hidden">
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 px-6 py-4"> <div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center"> <div className="w-10 h-10 bg-white rounded-xl flex items-center justify-center border border-gray-200">
<Users className="w-5 h-5 text-white" /> <Users className="w-5 h-5 text-[#1e3a5f]" />
</div> </div>
<div> <div>
<h2 className="text-xl font-bold text-white">Participants Éligibles</h2> <h2 className="text-xl font-bold text-[#1e3a5f]">Participants Éligibles</h2>
<p className="text-blue-100 text-sm"> <p className="text-gray-500 text-sm">
{loading {loading
? 'Chargement en cours...' ? 'Chargement en cours...'
: participants.length > 0 : participants.length > 0
@ -477,7 +574,7 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
isLoading={loading} isLoading={loading}
disabled={loading} disabled={loading}
size="sm" size="sm"
className="bg-blue-600 text-white hover:bg-blue-700 rounded-xl" className="!bg-white !text-black hover:!bg-gray-100 rounded-xl border border-gray-200"
> >
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} /> <RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
Actualiser Actualiser
@ -535,7 +632,7 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-indigo-500 rounded-full flex items-center justify-center text-white font-bold text-sm"> <div className="w-10 h-10 bg-gradient-to-br from-emerald-500 to-teal-500 rounded-full flex items-center justify-center text-white font-bold text-sm">
{participant.first_name?.charAt(0)}{participant.last_name?.charAt(0)} {participant.first_name?.charAt(0)}{participant.last_name?.charAt(0)}
</div> </div>
<div> <div>
@ -595,95 +692,6 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
</div> </div>
</div> </div>
{/* Résultat du tirage */}
{drawResult && (
<div className="bg-gradient-to-br from-amber-50 via-yellow-50 to-orange-50 rounded-2xl border-2 border-amber-300 overflow-hidden">
<div className="bg-gradient-to-r from-amber-500 to-orange-500 px-6 py-4 text-center">
<Trophy className="w-12 h-12 text-white mx-auto mb-2" />
<h2 className="text-2xl font-bold text-white">
Félicitations au gagnant !
</h2>
</div>
<div className="p-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
{/* Gagnant */}
<div className="bg-white rounded-2xl p-6 shadow-lg border border-blue-100">
<div className="flex items-center gap-4 mb-4">
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-full flex items-center justify-center text-white font-bold text-xl">
{drawResult.winner.name.split(' ').map(n => n[0]).join('')}
</div>
<div>
<p className="text-xs text-blue-600 font-semibold uppercase">Gagnant</p>
<h3 className="text-xl font-bold text-gray-900">
{drawResult.winner.name}
</h3>
</div>
</div>
<div className="space-y-2 text-sm">
<p className="text-gray-600">{drawResult.winner.email}</p>
<p className="text-gray-500">
{drawResult.winner.ticketsPlayed} ticket(s) joué(s)
</p>
</div>
</div>
{/* Prix */}
<div className="bg-white rounded-2xl p-6 shadow-lg border border-purple-100">
<div className="flex items-center gap-4 mb-4">
<div className="w-16 h-16 bg-gradient-to-br from-purple-500 to-pink-600 rounded-full flex items-center justify-center">
<Gift className="w-8 h-8 text-white" />
</div>
<div>
<p className="text-xs text-purple-600 font-semibold uppercase">Prix gagné</p>
<h3 className="text-xl font-bold text-gray-900">
{drawResult.prize.name}
</h3>
</div>
</div>
<p className="text-3xl font-bold text-purple-600">
{drawResult.prize.value}
</p>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-white rounded-xl p-4 text-center border border-gray-100">
<p className="text-3xl font-bold text-gray-900">
{drawResult.statistics.totalParticipants}
</p>
<p className="text-sm text-gray-500">Total participants</p>
</div>
<div className="bg-white rounded-xl p-4 text-center border border-blue-100">
<p className="text-3xl font-bold text-blue-600">
{drawResult.statistics.eligibleParticipants}
</p>
<p className="text-sm text-gray-500">Éligibles</p>
</div>
<div className="bg-white rounded-xl p-4 text-center border border-green-100">
<p className="text-3xl font-bold text-green-600">1</p>
<p className="text-sm text-gray-500">Gagnant</p>
</div>
</div>
<div className="flex gap-3 justify-center">
<Button onClick={downloadReport} className="rounded-xl">
<Download className="w-4 h-4 mr-2" />
Télécharger le rapport
</Button>
<Button variant="outline" className="rounded-xl" onClick={() => {
if (typeof window !== 'undefined') {
window.location.reload();
}
}}>
<RefreshCw className="w-4 h-4 mr-2" />
Rafraîchir
</Button>
</div>
</div>
</div>
)}
</div> </div>
); );
} }

View File

@ -6,13 +6,16 @@ import { Users } from "lucide-react";
export default function AdminUtilisateursPage() { export default function AdminUtilisateursPage() {
return ( return (
<div className="min-h-full bg-gradient-to-br from-gray-50 via-white to-gray-50 p-8"> <div className="min-h-full bg-gradient-to-br from-gray-50 via-white to-gray-50 p-8">
<div className="mb-8"> <div className="bg-gradient-to-r from-blue-600 to-indigo-600 rounded-2xl p-6 mb-8">
<h1 className="text-4xl font-bold text-[#1e3a5f] mb-2"> <div className="flex items-center gap-4">
Gestion des utilisateurs <div className="w-14 h-14 bg-white/20 rounded-xl flex items-center justify-center">
</h1> <Users className="w-7 h-7 text-white" />
<p className="text-gray-600 text-lg"> </div>
Gérez tous les comptes utilisateurs de la plateforme <div>
</p> <h1 className="text-3xl font-bold text-white">Gestion des Utilisateurs</h1>
<p className="text-blue-200">Gérez tous les comptes utilisateurs de la plateforme</p>
</div>
</div>
</div> </div>
<div className="bg-white rounded-2xl shadow-md border border-gray-100"> <div className="bg-white rounded-2xl shadow-md border border-gray-100">
<UserManagement /> <UserManagement />

View File

@ -14,13 +14,14 @@ interface UserDropdownProps {
} | null; } | null;
profilePath: string; profilePath: string;
onLogout: () => void; onLogout: () => void;
accentColor?: 'blue' | 'green'; accentColor?: 'blue' | 'green' | 'red';
className?: string; className?: string;
} }
const accentColors = { const accentColors = {
blue: 'bg-blue-600', blue: 'bg-blue-600',
green: 'bg-green-600', green: 'bg-green-600',
red: 'bg-gradient-to-br from-red-500 to-red-600',
}; };
export const UserDropdown: React.FC<UserDropdownProps> = ({ export const UserDropdown: React.FC<UserDropdownProps> = ({

View File

@ -3,7 +3,7 @@
import { useState, useEffect, useMemo } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { adminService } from '@/services/admin.service'; import { adminService } from '@/services/admin.service';
import { Prize, CreatePrizeData, UpdatePrizeData } from '@/types'; import { Prize, CreatePrizeData, UpdatePrizeData } from '@/types';
import { Gift, Package, Trophy, Percent, Archive, RefreshCw, X } from 'lucide-react'; import { Gift, Package, Trophy, Archive, RefreshCw, X } from 'lucide-react';
export default function PrizeManagement() { export default function PrizeManagement() {
const [prizes, setPrizes] = useState<Prize[]>([]); const [prizes, setPrizes] = useState<Prize[]>([]);
@ -126,7 +126,7 @@ export default function PrizeManagement() {
return ( return (
<div className="p-6"> <div className="p-6">
{/* Stats rapides */} {/* Stats rapides */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6"> <div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-gradient-to-br from-purple-50 to-purple-100 rounded-xl p-4 border border-purple-200"> <div className="bg-gradient-to-br from-purple-50 to-purple-100 rounded-xl p-4 border border-purple-200">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-purple-200 rounded-lg flex items-center justify-center"> <div className="w-10 h-10 bg-purple-200 rounded-lg flex items-center justify-center">
@ -160,17 +160,6 @@ export default function PrizeManagement() {
</div> </div>
</div> </div>
</div> </div>
<div className="bg-gradient-to-br from-amber-50 to-amber-100 rounded-xl p-4 border border-amber-200">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-amber-200 rounded-lg flex items-center justify-center">
<Percent className="w-5 h-5 text-amber-600" />
</div>
<div>
<p className="text-2xl font-bold text-amber-700">{prizeStats.totalStock > 0 ? ((prizeStats.totalUsed / prizeStats.totalStock) * 100).toFixed(1) : '0.0'}%</p>
<p className="text-xs text-amber-600">Taux distrib.</p>
</div>
</div>
</div>
</div> </div>
{error && ( {error && (

View File

@ -9,14 +9,17 @@ import {
BarChart3, BarChart3,
Gift, Gift,
Menu, Menu,
X X,
Trophy,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import Logo from "@/components/Logo";
interface NavItem { interface NavItem {
label: string; label: string;
href: string; href: string;
icon: React.ReactNode; icon: React.ReactNode;
color: string;
} }
export default function Sidebar() { export default function Sidebar() {
@ -28,31 +31,37 @@ export default function Sidebar() {
label: "Dashboard", label: "Dashboard",
href: "/admin/dashboard", href: "/admin/dashboard",
icon: <LayoutDashboard className="w-5 h-5" />, icon: <LayoutDashboard className="w-5 h-5" />,
color: "darkblue",
}, },
{ {
label: "Utilisateurs", label: "Utilisateurs",
href: "/admin/utilisateurs", href: "/admin/utilisateurs",
icon: <Users className="w-5 h-5" />, icon: <Users className="w-5 h-5" />,
color: "blue",
}, },
{ {
label: "Tickets", label: "Tickets",
href: "/admin/tickets", href: "/admin/tickets",
icon: <Ticket className="w-5 h-5" />, icon: <Ticket className="w-5 h-5" />,
color: "emerald",
}, },
{ {
label: "Lots & Prix", label: "Lots & Prix",
href: "/admin/lots", href: "/admin/lots",
icon: <Gift className="w-5 h-5" />, icon: <Gift className="w-5 h-5" />,
color: "purple",
}, },
{ {
label: "Données Marketing", label: "Données Marketing",
href: "/admin/marketing-data", href: "/admin/marketing-data",
icon: <BarChart3 className="w-5 h-5" />, icon: <BarChart3 className="w-5 h-5" />,
color: "indigo",
}, },
{ {
label: "Tirages", label: "Tirages",
href: "/admin/tirages", href: "/admin/tirages",
icon: <Gift className="w-5 h-5" />, icon: <Trophy className="w-5 h-5" />,
color: "amber",
}, },
]; ];
@ -60,12 +69,24 @@ export default function Sidebar() {
return pathname === href; return pathname === href;
}; };
const getActiveStyles = (color: string) => {
const styles: Record<string, string> = {
darkblue: "bg-gradient-to-r from-[#1e3a5f] to-[#2d5a8f] text-white shadow-lg shadow-blue-900/30",
blue: "bg-gradient-to-r from-blue-600 to-indigo-600 text-white shadow-lg shadow-blue-500/30",
emerald: "bg-gradient-to-r from-emerald-600 to-teal-600 text-white shadow-lg shadow-emerald-500/30",
purple: "bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg shadow-purple-500/30",
indigo: "bg-gradient-to-r from-indigo-600 to-purple-600 text-white shadow-lg shadow-indigo-500/30",
amber: "bg-gradient-to-r from-amber-500 to-orange-600 text-white shadow-lg shadow-amber-500/30",
};
return styles[color] || styles.blue;
};
return ( return (
<> <>
{/* Mobile menu button */} {/* Mobile menu button */}
<button <button
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
className="lg:hidden fixed top-4 left-4 z-50 p-2 rounded-md bg-white shadow-md hover:bg-gray-50" className="lg:hidden fixed top-4 left-4 z-50 p-2 rounded-xl bg-white shadow-lg hover:bg-gray-50 border border-gray-100"
> >
{isOpen ? ( {isOpen ? (
<X className="w-6 h-6 text-gray-600" /> <X className="w-6 h-6 text-gray-600" />
@ -77,7 +98,7 @@ export default function Sidebar() {
{/* Overlay for mobile */} {/* Overlay for mobile */}
{isOpen && ( {isOpen && (
<div <div
className="lg:hidden fixed inset-0 bg-black bg-opacity-50 z-30" className="lg:hidden fixed inset-0 bg-black/50 backdrop-blur-sm z-30"
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
/> />
)} )}
@ -85,32 +106,38 @@ export default function Sidebar() {
{/* Sidebar */} {/* Sidebar */}
<aside <aside
className={` className={`
fixed top-0 left-0 h-full bg-white shadow-lg z-40 transition-transform duration-300 ease-in-out fixed top-0 left-0 h-full bg-[#faf8f5] shadow-xl z-40 transition-transform duration-300 ease-in-out border-r border-gray-200
${isOpen ? "translate-x-0" : "-translate-x-full"} ${isOpen ? "translate-x-0" : "-translate-x-full"}
lg:translate-x-0 lg:static lg:translate-x-0 lg:static
w-64 w-72
`} `}
> >
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
{/* Logo/Header */} {/* Logo/Header */}
<div className="p-6 border-b border-gray-200"> <div className="p-6 border-b border-gray-200">
<h2 className="text-xl font-bold text-gray-900">Admin Panel</h2> <div className="flex items-center gap-3">
<p className="text-sm text-gray-500 mt-1">Thé Tip Top</p> <Logo size="md" showText={false} />
<div>
<h2 className="text-xl font-bold text-[#1e3a5f]">Thé Tip Top</h2>
<p className="text-sm text-gray-500">Administration</p>
</div>
</div>
</div> </div>
{/* Navigation */} {/* Navigation */}
<nav className="flex-1 p-4 space-y-2"> <nav className="flex-1 p-4 space-y-2">
<p className="text-xs font-semibold text-gray-400 uppercase tracking-wider px-4 mb-3">Menu Principal</p>
{navItems.map((item) => ( {navItems.map((item) => (
<Link <Link
key={item.href} key={item.href}
href={item.href} href={item.href}
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
className={` className={`
flex items-center gap-3 px-4 py-3 rounded-lg transition-colors flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200
${ ${
isActive(item.href) isActive(item.href)
? "bg-blue-600 text-white" ? getActiveStyles(item.color)
: "text-gray-700 hover:bg-gray-100" : "text-gray-600 hover:bg-gray-100 hover:text-gray-900"
} }
`} `}
> >

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect, useCallback, useMemo } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { adminService } from '@/services/admin.service'; import { adminService } from '@/services/admin.service';
import { Ticket } from '@/types'; import { Ticket } from '@/types';
import { StatusBadge, Pagination } from '@/components/ui'; import { StatusBadge, Pagination } from '@/components/ui';
@ -16,6 +16,7 @@ export default function TicketManagement() {
const [filterStatus, setFilterStatus] = useState<string>(''); const [filterStatus, setFilterStatus] = useState<string>('');
const [filterPrizeType, setFilterPrizeType] = useState<string>(''); const [filterPrizeType, setFilterPrizeType] = useState<string>('');
const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null); const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null);
const [ticketStats, setTicketStats] = useState({ pending: 0, claimed: 0, rejected: 0 });
const loadTickets = useCallback(async () => { const loadTickets = useCallback(async () => {
try { try {
@ -46,6 +47,10 @@ export default function TicketManagement() {
ticketsData = response.data; ticketsData = response.data;
total = response.total || response.data.length; total = response.total || response.data.length;
totalPagesCount = response.totalPages || 1; totalPagesCount = response.totalPages || 1;
// Récupérer les stats du backend
if (response.stats) {
setTicketStats(response.stats);
}
} }
setTickets(ticketsData); setTickets(ticketsData);
@ -76,16 +81,6 @@ export default function TicketManagement() {
loadTickets(); loadTickets();
}, [loadTickets]); }, [loadTickets]);
// Calculer les stats des tickets
const ticketStats = useMemo(() => {
const stats = { pending: 0, claimed: 0, rejected: 0 };
tickets.forEach(ticket => {
if (ticket.status === 'PENDING') stats.pending++;
else if (ticket.status === 'CLAIMED') stats.claimed++;
else if (ticket.status === 'REJECTED') stats.rejected++;
});
return stats;
}, [tickets]);
// Fonction pour obtenir les initiales // Fonction pour obtenir les initiales
const getInitials = (firstName?: string, lastName?: string) => { const getInitials = (firstName?: string, lastName?: string) => {
@ -107,8 +102,8 @@ export default function TicketManagement() {
return ( return (
<div className="p-6"> <div className="p-6">
{/* Section filtres avec gradient */} {/* Section filtres */}
<div className="bg-gradient-to-r from-emerald-600 to-teal-600 rounded-2xl p-6 mb-6"> <div className="bg-[#faf8f5] rounded-2xl p-6 mb-6 border border-gray-200">
<div className="flex flex-col md:flex-row gap-4 items-center justify-between"> <div className="flex flex-col md:flex-row gap-4 items-center justify-between">
<div className="flex gap-3 flex-wrap"> <div className="flex gap-3 flex-wrap">
<select <select
@ -117,14 +112,14 @@ export default function TicketManagement() {
setFilterPrizeType(e.target.value); setFilterPrizeType(e.target.value);
setPage(1); setPage(1);
}} }}
className="px-4 py-3 rounded-xl bg-white/10 backdrop-blur-sm text-white border border-white/20 focus:outline-none focus:ring-2 focus:ring-white/40" className="px-4 py-3 rounded-xl bg-white text-gray-700 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-emerald-500"
> >
<option value="" className="text-gray-900">Tous les lots</option> <option value="">Tous les lots</option>
<option value="INFUSEUR" className="text-gray-900">Infuseur à thé</option> <option value="INFUSEUR">Infuseur à thé</option>
<option value="THE_GRATUIT" className="text-gray-900">Thé détox/infusion 100g</option> <option value="THE_GRATUIT">Thé détox/infusion 100g</option>
<option value="THE_SIGNATURE" className="text-gray-900">Thé signature 100g</option> <option value="THE_SIGNATURE">Thé signature 100g</option>
<option value="COFFRET_DECOUVERTE" className="text-gray-900">Coffret découverte 39</option> <option value="COFFRET_DECOUVERTE">Coffret découverte 39</option>
<option value="COFFRET_PRESTIGE" className="text-gray-900">Coffret prestige 69</option> <option value="COFFRET_PRESTIGE">Coffret prestige 69</option>
</select> </select>
<select <select
@ -133,12 +128,12 @@ export default function TicketManagement() {
setFilterStatus(e.target.value); setFilterStatus(e.target.value);
setPage(1); setPage(1);
}} }}
className="px-4 py-3 rounded-xl bg-white/10 backdrop-blur-sm text-white border border-white/20 focus:outline-none focus:ring-2 focus:ring-white/40" className="px-4 py-3 rounded-xl bg-white text-gray-700 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-emerald-500"
> >
<option value="" className="text-gray-900">Tous les statuts</option> <option value="">Tous les statuts</option>
<option value="PENDING" className="text-gray-900">En attente</option> <option value="PENDING">En attente</option>
<option value="REJECTED" className="text-gray-900">Rejeté</option> <option value="REJECTED">Rejeté</option>
<option value="CLAIMED" className="text-gray-900">Réclamé</option> <option value="CLAIMED">Réclamé</option>
</select> </select>
{(filterStatus || filterPrizeType) && ( {(filterStatus || filterPrizeType) && (
@ -148,7 +143,7 @@ export default function TicketManagement() {
setFilterPrizeType(''); setFilterPrizeType('');
setPage(1); setPage(1);
}} }}
className="px-4 py-3 rounded-xl bg-white/20 text-white border border-white/20 hover:bg-white/30 transition-colors flex items-center gap-2" className="px-4 py-3 rounded-xl bg-white text-gray-600 border border-gray-200 hover:bg-gray-100 transition-colors flex items-center gap-2"
> >
<X className="w-4 h-4" /> <X className="w-4 h-4" />
Réinitialiser Réinitialiser
@ -158,7 +153,7 @@ export default function TicketManagement() {
<button <button
onClick={loadTickets} onClick={loadTickets}
disabled={loading} disabled={loading}
className="flex items-center gap-2 px-5 py-3 bg-white text-emerald-600 rounded-xl font-semibold hover:bg-emerald-50 transition-colors shadow-lg disabled:opacity-50" className="flex items-center gap-2 px-5 py-3 bg-white text-[#1e3a5f] rounded-xl font-semibold hover:bg-gray-100 transition-colors shadow-sm border border-gray-200 disabled:opacity-50"
> >
<RefreshCw className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`} /> <RefreshCw className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`} />
{loading ? 'Chargement...' : 'Actualiser'} {loading ? 'Chargement...' : 'Actualiser'}

View File

@ -178,18 +178,18 @@ export default function UserManagement() {
return ( return (
<div className="p-6"> <div className="p-6">
{/* Section recherche avec gradient */} {/* Section recherche */}
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 rounded-2xl p-6 mb-6"> <div className="bg-[#faf8f5] rounded-2xl p-6 mb-6 border border-gray-200">
<div className="flex flex-col md:flex-row gap-4 items-center justify-between"> <div className="flex flex-col md:flex-row gap-4 items-center justify-between">
<div className="flex-1 w-full"> <div className="flex-1 w-full">
<div className="relative"> <div className="relative">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-blue-200" /> <Search className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input <input
type="text" type="text"
placeholder="Rechercher par nom ou email..." placeholder="Rechercher par nom ou email..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-12 pr-4 py-3 rounded-xl bg-white/10 backdrop-blur-sm text-white placeholder-blue-200 border border-white/20 focus:outline-none focus:ring-2 focus:ring-white/40" className="w-full pl-12 pr-4 py-3 rounded-xl bg-white text-gray-900 placeholder-gray-400 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/> />
</div> </div>
</div> </div>
@ -200,12 +200,12 @@ export default function UserManagement() {
setFilterRole(e.target.value); setFilterRole(e.target.value);
setPage(1); setPage(1);
}} }}
className="px-4 py-3 rounded-xl bg-white/10 backdrop-blur-sm text-white border border-white/20 focus:outline-none focus:ring-2 focus:ring-white/40" className="px-4 py-3 rounded-xl bg-white text-gray-700 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
<option value="" className="text-gray-900">Tous les rôles</option> <option value="">Tous les rôles</option>
<option value="CLIENT" className="text-gray-900">Clients</option> <option value="CLIENT">Clients</option>
<option value="EMPLOYEE" className="text-gray-900">Employés</option> <option value="EMPLOYEE">Employés</option>
<option value="ADMIN" className="text-gray-900">Administrateurs</option> <option value="ADMIN">Administrateurs</option>
</select> </select>
<select <select
value={filterStatus} value={filterStatus}
@ -213,15 +213,15 @@ export default function UserManagement() {
setFilterStatus(e.target.value); setFilterStatus(e.target.value);
setPage(1); setPage(1);
}} }}
className="px-4 py-3 rounded-xl bg-white/10 backdrop-blur-sm text-white border border-white/20 focus:outline-none focus:ring-2 focus:ring-white/40" className="px-4 py-3 rounded-xl bg-white text-gray-700 border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
<option value="" className="text-gray-900">Tous les statuts</option> <option value="">Tous les statuts</option>
<option value="active" className="text-gray-900">Actifs</option> <option value="active">Actifs</option>
<option value="inactive" className="text-gray-900">Inactifs</option> <option value="inactive">Inactifs</option>
</select> </select>
<button <button
onClick={() => setIsCreateEmployeeModalOpen(true)} onClick={() => setIsCreateEmployeeModalOpen(true)}
className="flex items-center gap-2 px-5 py-3 bg-white text-blue-600 rounded-xl font-semibold hover:bg-blue-50 transition-colors shadow-lg" className="flex items-center gap-2 px-5 py-3 bg-white text-[#1e3a5f] rounded-xl font-semibold hover:bg-gray-100 transition-colors shadow-sm border border-gray-200"
> >
<UserPlus className="w-5 h-5" /> <UserPlus className="w-5 h-5" />
<span className="hidden sm:inline">Nouvel employé</span> <span className="hidden sm:inline">Nouvel employé</span>

View File

@ -243,9 +243,6 @@ export const adminService = {
} }
const url = `${API_ENDPOINTS.TICKETS}?${params}`; const url = `${API_ENDPOINTS.TICKETS}?${params}`;
console.log('🔍 Frontend - URL appelée:', url);
console.log('🔍 Frontend - Filters:', filters);
const response = await api.get<any>(url); const response = await api.get<any>(url);
// Transformer les données du backend (format plat) en format attendu par le frontend // Transformer les données du backend (format plat) en format attendu par le frontend
@ -272,21 +269,15 @@ export const adminService = {
}); });
// Gérer différents formats de réponse de l'API // Gérer différents formats de réponse de l'API
if (response.data && response.data.data) { // Backend renvoie: { success, data, total, page, limit, totalPages, stats }
return { if (response.data && Array.isArray(response.data)) {
data: response.data.data.map(transformTicket),
total: response.data.total,
page: response.data.page,
limit: response.data.limit,
totalPages: response.data.totalPages,
};
} else if (response.data && Array.isArray(response.data)) {
return { return {
data: response.data.map(transformTicket), data: response.data.map(transformTicket),
total: response.total || response.data.length, total: response.total || response.data.length,
page: response.page || page, page: response.page || page,
limit: response.limit || limit, limit: response.limit || limit,
totalPages: response.totalPages || 1, totalPages: response.totalPages || 1,
stats: response.stats,
}; };
} else if (Array.isArray(response)) { } else if (Array.isArray(response)) {
return { return {

View File

@ -146,6 +146,11 @@ export interface PaginatedResponse<T> {
page: number; page: number;
limit: number; limit: number;
totalPages: number; totalPages: number;
stats?: {
pending: number;
claimed: number;
rejected: number;
};
} }
// Form Validation Types // Form Validation Types