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 />
|
<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>
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -227,13 +227,20 @@ 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">
|
||||||
Données Marketing
|
<div className="w-14 h-14 bg-white/20 rounded-xl flex items-center justify-center">
|
||||||
</h1>
|
<Mail className="w-7 h-7 text-white" />
|
||||||
<p className="text-gray-600 text-lg">
|
</div>
|
||||||
Statistiques et export des données pour vos campagnes d'emailing
|
<div>
|
||||||
</p>
|
<h1 className="text-3xl font-bold text-white">
|
||||||
|
Données Marketing
|
||||||
|
</h1>
|
||||||
|
<p className="text-indigo-200">
|
||||||
|
Statistiques et export des données pour vos campagnes d'emailing
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Statistiques globales */}
|
{/* Statistiques globales */}
|
||||||
|
|
@ -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.).
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
{/* En-tête avec titre du prix */}
|
{/* Animation de tirage en cours */}
|
||||||
<div className="mb-8">
|
{isDrawing && (
|
||||||
<h1 className="text-4xl font-bold text-[#1e3a5f] mb-2">
|
<div className="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center">
|
||||||
Tirage au Sort - {prizeName}
|
<div className="bg-white rounded-3xl p-12 text-center max-w-md mx-4 animate-pulse">
|
||||||
</h1>
|
<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">
|
||||||
<p className="text-gray-600 text-lg">
|
<Trophy className="w-12 h-12 text-white" />
|
||||||
Prix à gagner : <span className="font-bold text-purple-600">{prizeValue}€</span> •
|
</div>
|
||||||
Participants ayant joué au moins {minTickets} ticket{minTickets > 1 ? 's' : ''}
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">Tirage en cours...</h2>
|
||||||
</p>
|
<p className="text-gray-500">Sélection aléatoire du gagnant parmi {participants.length} participants</p>
|
||||||
</div>
|
<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>
|
||||||
{/* Alerte si un tirage existe déjà */}
|
<div className="w-3 h-3 bg-orange-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
|
||||||
{hasExistingDraw && existingDraw && (
|
<div className="w-3 h-3 bg-red-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
|
||||||
<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" />
|
|
||||||
</div>
|
|
||||||
<h3 className="font-bold text-white text-lg">Tirage déjà effectué</h3>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6">
|
</div>
|
||||||
<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>
|
{/* En-tête avec titre du prix */}
|
||||||
<p className="font-bold text-gray-900">{new Date(existingDraw.draw_date).toLocaleString('fr-FR')}</p>
|
<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>
|
</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>
|
{/* Prix */}
|
||||||
<p className="font-bold text-gray-900">{existingDraw.winner_name}</p>
|
<div className="bg-white rounded-2xl p-6 shadow-lg border border-purple-100">
|
||||||
<p className="text-xs text-gray-500">{existingDraw.winner_email}</p>
|
<div className="flex items-center gap-4 mb-4">
|
||||||
</div>
|
<div className="w-16 h-16 bg-gradient-to-br from-purple-500 to-pink-600 rounded-full flex items-center justify-center">
|
||||||
<div className="bg-white p-4 rounded-xl border border-amber-100">
|
<Gift className="w-8 h-8 text-white" />
|
||||||
<p className="text-xs text-gray-500 uppercase font-semibold mb-1">Prix</p>
|
</div>
|
||||||
<p className="font-bold text-gray-900">{existingDraw.prize_name}</p>
|
<div>
|
||||||
<p className="text-sm text-purple-600 font-semibold">{existingDraw.prize_value}€</p>
|
<p className="text-xs text-purple-600 font-semibold uppercase">Prix gagné</p>
|
||||||
</div>
|
<h3 className="text-xl font-bold text-gray-900">
|
||||||
<div className="bg-white p-4 rounded-xl border border-amber-100">
|
{drawResult.prize.name}
|
||||||
<p className="text-xs text-gray-500 uppercase font-semibold mb-1">Statut</p>
|
</h3>
|
||||||
<span
|
</div>
|
||||||
className={`inline-flex px-3 py-1 rounded-full text-sm font-semibold ${
|
</div>
|
||||||
existingDraw.status === 'CLAIMED'
|
<p className="text-3xl font-bold text-purple-600">
|
||||||
? 'bg-green-100 text-green-700'
|
{drawResult.prize.value}€
|
||||||
: existingDraw.status === 'NOTIFIED'
|
</p>
|
||||||
? '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">
|
<div className="flex justify-center">
|
||||||
<Button onClick={downloadReport} size="sm" variant="outline" className="rounded-xl">
|
<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-1" />
|
<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 && !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>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* 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
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 />
|
||||||
|
|
|
||||||
|
|
@ -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> = ({
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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'}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user