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:
parent
aa1d8b1d66
commit
f20cf40fff
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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.).
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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> = ({
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -146,6 +146,11 @@ export interface PaginatedResponse<T> {
|
|||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
stats?: {
|
||||
pending: number;
|
||||
claimed: number;
|
||||
rejected: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Form Validation Types
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user