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 />
<div className="flex-1 flex flex-col">
{/* Admin Header */}
<header className="bg-white shadow-sm border-b border-gray-200 px-6 py-4">
<div className="flex items-center justify-between">
<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>
<header className="bg-[#faf8f5] shadow-sm border-b border-gray-200 px-6 py-3">
<div className="flex items-center justify-end">
<UserDropdown
user={user}
profilePath="/admin/profil"
onLogout={handleLogout}
accentColor="blue"
accentColor="red"
/>
</div>
</header>

View File

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

View File

@ -227,14 +227,21 @@ export default function MarketingPage() {
return (
<div className="min-h-full bg-gradient-to-br from-gray-50 via-white to-gray-50 p-8">
{/* En-tête */}
<div className="mb-8">
<h1 className="text-4xl font-bold text-[#1e3a5f] mb-2">
<div className="bg-gradient-to-r from-indigo-600 to-purple-600 rounded-2xl p-6 mb-8">
<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
</h1>
<p className="text-gray-600 text-lg">
<p className="text-indigo-200">
Statistiques et export des données pour vos campagnes d'emailing
</p>
</div>
</div>
</div>
{/* Statistiques globales */}
<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 */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 px-6 py-4">
<h2 className="text-xl font-bold text-white flex items-center gap-3">
<div className="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center">
<Download className="w-5 h-5 text-white" />
<div className="bg-[#faf8f5] rounded-2xl border border-gray-200 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-xl font-bold text-[#1e3a5f] flex items-center gap-3">
<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-[#1e3a5f]" />
</div>
Exporter les Données pour Emailing
</h2>
@ -406,7 +413,7 @@ export default function MarketingPage() {
<select
value={selectedSegment}
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="active">
@ -429,7 +436,7 @@ export default function MarketingPage() {
<select
value={filters.city}
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>
{filteredCityData.slice(0, 10).map((city) => (
@ -447,7 +454,7 @@ export default function MarketingPage() {
<select
value={filters.gender}
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>
{filteredGenderData.map((g) => (
@ -467,24 +474,24 @@ export default function MarketingPage() {
onClick={exportSegmentData}
isLoading={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" />
Exporter en CSV
</Button>
<Button variant="outline" onClick={loadMarketingStats}>
<Button variant="outline" onClick={loadMarketingStats} className="bg-white">
<FileText className="w-5 h-5 mr-2" />
Actualiser les données
</Button>
</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="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0">
<Mail className="w-4 h-4 text-blue-600" />
<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-gray-600" />
</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,
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.).

View File

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

View File

@ -71,6 +71,7 @@ export default function TiragesPage() {
const [drawResult, setDrawResult] = useState<DrawResult | null>(null);
const [existingDraw, setExistingDraw] = useState<ExistingDraw | null>(null);
const [hasExistingDraw, setHasExistingDraw] = useState(false);
const [isDrawing, setIsDrawing] = useState(false);
// Critères
const [minTickets, setMinTickets] = useState(1);
@ -136,13 +137,13 @@ export default function TiragesPage() {
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);
setIsDrawing(true);
setDrawResult(null);
// Animation de tirage (3 secondes)
await new Promise(resolve => setTimeout(resolve, 3000));
try {
const token = localStorage.getItem('auth_token') || localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/draw/conduct`, {
@ -172,10 +173,13 @@ export default function TiragesPage() {
setHasExistingDraw(true);
toast.success('🎉 Tirage au sort effectué avec succès!');
await checkExistingDraw();
// Scroll vers le haut pour voir le résultat
window.scrollTo({ top: 0, behavior: 'smooth' });
} catch (error: any) {
toast.error(error.message || 'Erreur lors du tirage');
} finally {
setLoading(false);
setIsDrawing(false);
}
};
@ -317,12 +321,6 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
const deleteDraw = async () => {
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);
try {
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 (
<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 */}
<div className="mb-8">
<h1 className="text-4xl font-bold text-[#1e3a5f] mb-2">
Tirage au Sort - {prizeName}
</h1>
<p className="text-gray-600 text-lg">
Prix à gagner : <span className="font-bold text-purple-600">{prizeValue}</span>
<div className="bg-gradient-to-r from-amber-500 to-orange-600 rounded-2xl p-6 mb-8">
<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>
<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' : ''}
</p>
</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à */}
{hasExistingDraw && existingDraw && (
<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="bg-gradient-to-r from-amber-500 to-yellow-500 px-6 py-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center">
<Trophy className="w-5 h-5 text-white" />
{hasExistingDraw && existingDraw && !drawResult && (
<div className="mb-8 bg-[#faf8f5] rounded-2xl border border-gray-200 overflow-hidden">
{/* Header */}
<div className="bg-gradient-to-r from-amber-500 to-orange-600 px-6 py-4">
<div className="flex items-center justify-between">
<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>
<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 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 className="flex gap-2 flex-wrap">
<Button onClick={downloadReport} size="sm" variant="outline" className="rounded-xl">
<Download className="w-4 h-4 mr-1" />
{/* Content */}
<div className="p-6">
<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
</Button>
{existingDraw.status === 'COMPLETED' && (
<Button onClick={markAsNotified} size="sm" variant="outline" className="rounded-xl">
<Mail className="w-4 h-4 mr-1" />
<Button
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é
</Button>
)}
{existingDraw.status === 'NOTIFIED' && (
<Button onClick={markAsClaimed} size="sm" variant="outline" className="rounded-xl">
<CheckCircle className="w-4 h-4 mr-1" />
<Button
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é
</Button>
)}
@ -442,10 +540,9 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
<Button
onClick={deleteDraw}
size="sm"
variant="outline"
className="border-red-300 text-red-700 hover:bg-red-50 hover:border-red-400 rounded-xl"
className="!bg-white !text-red-600 border border-red-200 hover:!bg-red-50 rounded-xl"
>
<Trash2 className="w-4 h-4 mr-1" />
<Trash2 className="w-4 h-4 mr-2" />
Annuler ce tirage
</Button>
</div>
@ -454,16 +551,16 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
)}
{/* Liste des participants éligibles */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 mb-6 overflow-hidden">
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 px-6 py-4">
<div className="bg-[#faf8f5] rounded-2xl border border-gray-200 mb-6 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center">
<Users className="w-5 h-5 text-white" />
<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-[#1e3a5f]" />
</div>
<div>
<h2 className="text-xl font-bold text-white">Participants Éligibles</h2>
<p className="text-blue-100 text-sm">
<h2 className="text-xl font-bold text-[#1e3a5f]">Participants Éligibles</h2>
<p className="text-gray-500 text-sm">
{loading
? 'Chargement en cours...'
: participants.length > 0
@ -477,7 +574,7 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
isLoading={loading}
disabled={loading}
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' : ''}`} />
Actualiser
@ -535,7 +632,7 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
</td>
<td className="px-6 py-4 whitespace-nowrap">
<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)}
</div>
<div>
@ -595,95 +692,6 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
</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>
);
}

View File

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

View File

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

View File

@ -3,7 +3,7 @@
import { useState, useEffect, useMemo } from 'react';
import { adminService } from '@/services/admin.service';
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() {
const [prizes, setPrizes] = useState<Prize[]>([]);
@ -126,7 +126,7 @@ export default function PrizeManagement() {
return (
<div className="p-6">
{/* 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="flex items-center gap-3">
<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 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>
{error && (

View File

@ -9,14 +9,17 @@ import {
BarChart3,
Gift,
Menu,
X
X,
Trophy,
} from "lucide-react";
import { useState } from "react";
import Logo from "@/components/Logo";
interface NavItem {
label: string;
href: string;
icon: React.ReactNode;
color: string;
}
export default function Sidebar() {
@ -28,31 +31,37 @@ export default function Sidebar() {
label: "Dashboard",
href: "/admin/dashboard",
icon: <LayoutDashboard className="w-5 h-5" />,
color: "darkblue",
},
{
label: "Utilisateurs",
href: "/admin/utilisateurs",
icon: <Users className="w-5 h-5" />,
color: "blue",
},
{
label: "Tickets",
href: "/admin/tickets",
icon: <Ticket className="w-5 h-5" />,
color: "emerald",
},
{
label: "Lots & Prix",
href: "/admin/lots",
icon: <Gift className="w-5 h-5" />,
color: "purple",
},
{
label: "Données Marketing",
href: "/admin/marketing-data",
icon: <BarChart3 className="w-5 h-5" />,
color: "indigo",
},
{
label: "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;
};
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 (
<>
{/* Mobile menu button */}
<button
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 ? (
<X className="w-6 h-6 text-gray-600" />
@ -77,7 +98,7 @@ export default function Sidebar() {
{/* Overlay for mobile */}
{isOpen && (
<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)}
/>
)}
@ -85,32 +106,38 @@ export default function Sidebar() {
{/* Sidebar */}
<aside
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"}
lg:translate-x-0 lg:static
w-64
w-72
`}
>
<div className="flex flex-col h-full">
{/* Logo/Header */}
<div className="p-6 border-b border-gray-200">
<h2 className="text-xl font-bold text-gray-900">Admin Panel</h2>
<p className="text-sm text-gray-500 mt-1">Thé Tip Top</p>
<div className="flex items-center gap-3">
<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>
{/* Navigation */}
<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) => (
<Link
key={item.href}
href={item.href}
onClick={() => setIsOpen(false)}
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)
? "bg-blue-600 text-white"
: "text-gray-700 hover:bg-gray-100"
? getActiveStyles(item.color)
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900"
}
`}
>

View File

@ -1,6 +1,6 @@
'use client';
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { adminService } from '@/services/admin.service';
import { Ticket } from '@/types';
import { StatusBadge, Pagination } from '@/components/ui';
@ -16,6 +16,7 @@ export default function TicketManagement() {
const [filterStatus, setFilterStatus] = useState<string>('');
const [filterPrizeType, setFilterPrizeType] = useState<string>('');
const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null);
const [ticketStats, setTicketStats] = useState({ pending: 0, claimed: 0, rejected: 0 });
const loadTickets = useCallback(async () => {
try {
@ -46,6 +47,10 @@ export default function TicketManagement() {
ticketsData = response.data;
total = response.total || response.data.length;
totalPagesCount = response.totalPages || 1;
// Récupérer les stats du backend
if (response.stats) {
setTicketStats(response.stats);
}
}
setTickets(ticketsData);
@ -76,16 +81,6 @@ export default function TicketManagement() {
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
const getInitials = (firstName?: string, lastName?: string) => {
@ -107,8 +102,8 @@ export default function TicketManagement() {
return (
<div className="p-6">
{/* Section filtres avec gradient */}
<div className="bg-gradient-to-r from-emerald-600 to-teal-600 rounded-2xl p-6 mb-6">
{/* Section filtres */}
<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 gap-3 flex-wrap">
<select
@ -117,14 +112,14 @@ export default function TicketManagement() {
setFilterPrizeType(e.target.value);
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="INFUSEUR" className="text-gray-900">Infuseur à thé</option>
<option value="THE_GRATUIT" className="text-gray-900">Thé détox/infusion 100g</option>
<option value="THE_SIGNATURE" className="text-gray-900">Thé signature 100g</option>
<option value="COFFRET_DECOUVERTE" className="text-gray-900">Coffret découverte 39</option>
<option value="COFFRET_PRESTIGE" className="text-gray-900">Coffret prestige 69</option>
<option value="">Tous les lots</option>
<option value="INFUSEUR">Infuseur à thé</option>
<option value="THE_GRATUIT">Thé détox/infusion 100g</option>
<option value="THE_SIGNATURE">Thé signature 100g</option>
<option value="COFFRET_DECOUVERTE">Coffret découverte 39</option>
<option value="COFFRET_PRESTIGE">Coffret prestige 69</option>
</select>
<select
@ -133,12 +128,12 @@ export default function TicketManagement() {
setFilterStatus(e.target.value);
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="PENDING" className="text-gray-900">En attente</option>
<option value="REJECTED" className="text-gray-900">Rejeté</option>
<option value="CLAIMED" className="text-gray-900">Réclamé</option>
<option value="">Tous les statuts</option>
<option value="PENDING">En attente</option>
<option value="REJECTED">Rejeté</option>
<option value="CLAIMED">Réclamé</option>
</select>
{(filterStatus || filterPrizeType) && (
@ -148,7 +143,7 @@ export default function TicketManagement() {
setFilterPrizeType('');
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" />
Réinitialiser
@ -158,7 +153,7 @@ export default function TicketManagement() {
<button
onClick={loadTickets}
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' : ''}`} />
{loading ? 'Chargement...' : 'Actualiser'}

View File

@ -178,18 +178,18 @@ export default function UserManagement() {
return (
<div className="p-6">
{/* Section recherche avec gradient */}
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 rounded-2xl p-6 mb-6">
{/* Section recherche */}
<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-1 w-full">
<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
type="text"
placeholder="Rechercher par nom ou email..."
value={searchQuery}
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>
@ -200,12 +200,12 @@ export default function UserManagement() {
setFilterRole(e.target.value);
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="CLIENT" className="text-gray-900">Clients</option>
<option value="EMPLOYEE" className="text-gray-900">Employés</option>
<option value="ADMIN" className="text-gray-900">Administrateurs</option>
<option value="">Tous les rôles</option>
<option value="CLIENT">Clients</option>
<option value="EMPLOYEE">Employés</option>
<option value="ADMIN">Administrateurs</option>
</select>
<select
value={filterStatus}
@ -213,15 +213,15 @@ export default function UserManagement() {
setFilterStatus(e.target.value);
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="active" className="text-gray-900">Actifs</option>
<option value="inactive" className="text-gray-900">Inactifs</option>
<option value="">Tous les statuts</option>
<option value="active">Actifs</option>
<option value="inactive">Inactifs</option>
</select>
<button
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" />
<span className="hidden sm:inline">Nouvel employé</span>

View File

@ -243,9 +243,6 @@ export const adminService = {
}
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);
// 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
if (response.data && response.data.data) {
return {
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)) {
// Backend renvoie: { success, data, total, page, limit, totalPages, stats }
if (response.data && Array.isArray(response.data)) {
return {
data: response.data.map(transformTicket),
total: response.total || response.data.length,
page: response.page || page,
limit: response.limit || limit,
totalPages: response.totalPages || 1,
stats: response.stats,
};
} else if (Array.isArray(response)) {
return {

View File

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