- Add gradient header with dark blue theme - Improve stat cards with gradient backgrounds and decorative circles - Update period filter with modern button styling - Improve city cards with gradient rank badges - Better hover effects and transitions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
690 lines
27 KiB
TypeScript
690 lines
27 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { Card } from '@/components/ui/Card';
|
|
import Button from '@/components/Button';
|
|
import toast from 'react-hot-toast';
|
|
import { API_BASE_URL } from '@/utils/constants';
|
|
import {
|
|
Trophy,
|
|
Users,
|
|
CheckCircle,
|
|
AlertCircle,
|
|
Download,
|
|
Mail,
|
|
Gift,
|
|
User,
|
|
RefreshCw,
|
|
Trash2,
|
|
} from 'lucide-react';
|
|
|
|
interface Participant {
|
|
id: string;
|
|
email: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
is_verified: boolean;
|
|
tickets_played: number;
|
|
prizes_won: number;
|
|
}
|
|
|
|
interface DrawResult {
|
|
draw: {
|
|
id: string;
|
|
drawDate: string;
|
|
status: string;
|
|
};
|
|
winner: {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
ticketsPlayed: number;
|
|
};
|
|
statistics: {
|
|
totalParticipants: number;
|
|
eligibleParticipants: number;
|
|
criteria: any;
|
|
};
|
|
prize: {
|
|
name: string;
|
|
value: string;
|
|
};
|
|
}
|
|
|
|
interface ExistingDraw {
|
|
id: string;
|
|
draw_date: string;
|
|
winner_email: string;
|
|
winner_name: string;
|
|
prize_name: string;
|
|
prize_value: string;
|
|
total_participants: number;
|
|
eligible_participants: number;
|
|
status: string;
|
|
notified_at: string | null;
|
|
claimed_at: string | null;
|
|
}
|
|
|
|
export default function TiragesPage() {
|
|
const [participants, setParticipants] = useState<Participant[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [drawResult, setDrawResult] = useState<DrawResult | null>(null);
|
|
const [existingDraw, setExistingDraw] = useState<ExistingDraw | null>(null);
|
|
const [hasExistingDraw, setHasExistingDraw] = useState(false);
|
|
|
|
// Critères
|
|
const [minTickets, setMinTickets] = useState(1);
|
|
const [verifiedOnly, setVerifiedOnly] = useState(false);
|
|
const [prizeName, setPrizeName] = useState('An de thé');
|
|
const [prizeValue, setPrizeValue] = useState('360');
|
|
const [allowRedraw, setAllowRedraw] = useState(false);
|
|
|
|
const checkExistingDraw = useCallback(async () => {
|
|
try {
|
|
const token = localStorage.getItem('auth_token') || localStorage.getItem('token');
|
|
const response = await fetch(`${API_BASE_URL}/draw/check-existing`, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setHasExistingDraw(data.data.hasExistingDraw);
|
|
setExistingDraw(data.data.lastDraw);
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur:', error);
|
|
}
|
|
}, []);
|
|
|
|
const loadParticipants = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const token = localStorage.getItem('auth_token') || localStorage.getItem('token');
|
|
const response = await fetch(
|
|
`${API_BASE_URL}/draw/eligible-participants?minTickets=${minTickets}&verified=${verifiedOnly}`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!response.ok) throw new Error('Erreur lors du chargement');
|
|
|
|
const data = await response.json();
|
|
setParticipants(data.data.participants);
|
|
toast.success(`${data.data.total} participants éligibles trouvés`);
|
|
} catch (error: any) {
|
|
toast.error(error.message || 'Erreur lors du chargement');
|
|
setParticipants([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [minTickets, verifiedOnly]);
|
|
|
|
useEffect(() => {
|
|
checkExistingDraw();
|
|
// Charger automatiquement les participants au chargement de la page
|
|
loadParticipants();
|
|
}, [checkExistingDraw, loadParticipants]);
|
|
|
|
const conductDraw = async () => {
|
|
if (participants.length === 0) {
|
|
toast.error('Veuillez d\'abord charger les participants éligibles');
|
|
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);
|
|
try {
|
|
const token = localStorage.getItem('auth_token') || localStorage.getItem('token');
|
|
const response = await fetch(`${API_BASE_URL}/draw/conduct`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
criteria: {
|
|
minTickets,
|
|
verified: verifiedOnly,
|
|
},
|
|
prizeName,
|
|
prizeValue,
|
|
allowRedraw: hasExistingDraw,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.message || 'Erreur lors du tirage');
|
|
}
|
|
|
|
const data = await response.json();
|
|
setDrawResult(data.data);
|
|
setHasExistingDraw(true);
|
|
toast.success('🎉 Tirage au sort effectué avec succès!');
|
|
await checkExistingDraw();
|
|
} catch (error: any) {
|
|
toast.error(error.message || 'Erreur lors du tirage');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const downloadReport = async () => {
|
|
if (!existingDraw) return;
|
|
|
|
try {
|
|
const token = localStorage.getItem('auth_token') || localStorage.getItem('token');
|
|
const response = await fetch(
|
|
`${API_BASE_URL}/draw/${existingDraw.id}/report`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!response.ok) throw new Error('Erreur lors de la génération du rapport');
|
|
|
|
const data = await response.json();
|
|
const report = data.data;
|
|
|
|
// Créer un rapport texte
|
|
const reportText = `
|
|
=================================================
|
|
RAPPORT DE TIRAGE AU SORT - THÉ TIP TOP
|
|
=================================================
|
|
|
|
📅 Date du tirage: ${new Date(report.draw.date).toLocaleString('fr-FR')}
|
|
👤 Effectué par: ${report.draw.conductedBy.name} (${report.draw.conductedBy.email})
|
|
📊 Statut: ${report.draw.status}
|
|
|
|
-------------------------------------------------
|
|
GAGNANT
|
|
-------------------------------------------------
|
|
🏆 Nom: ${report.winner.firstName} ${report.winner.lastName}
|
|
📧 Email: ${report.winner.email}
|
|
📱 Téléphone: ${report.winner.phone || 'Non renseigné'}
|
|
📍 Ville: ${report.winner.city || 'Non renseignée'}
|
|
🎫 Nombre de tickets joués: ${report.winner.totalTickets}
|
|
|
|
-------------------------------------------------
|
|
PRIX
|
|
-------------------------------------------------
|
|
🎁 Nom: ${report.prize.name}
|
|
💰 Valeur: ${report.prize.value}€
|
|
|
|
-------------------------------------------------
|
|
STATISTIQUES
|
|
-------------------------------------------------
|
|
👥 Total de participants: ${report.statistics.totalParticipants}
|
|
✅ Participants éligibles: ${report.statistics.eligibleParticipants}
|
|
📋 Critères:
|
|
- Tickets minimum: ${report.statistics.criteria.minTickets}
|
|
- Email vérifié: ${report.statistics.criteria.verified ? 'Oui' : 'Non'}
|
|
|
|
-------------------------------------------------
|
|
TICKETS DU GAGNANT
|
|
-------------------------------------------------
|
|
${report.winner.tickets.map((t: any, i: number) =>
|
|
`${i + 1}. Code: ${t.code} | Lot: ${t.prize_name} | Statut: ${t.status} | Joué le: ${new Date(t.played_at).toLocaleDateString('fr-FR')}`
|
|
).join('\n')}
|
|
|
|
-------------------------------------------------
|
|
${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.notifiedAt).toLocaleString('fr-FR')}\n` : ''}${report.draw.claimedAt ? `✅ Lot récupéré le: ${new Date(report.draw.claimedAt).toLocaleString('fr-FR')}\n` : ''}-------------------------------------------------
|
|
|
|
📝 Notes: ${report.draw.notes || 'Aucune note'}
|
|
|
|
=================================================
|
|
Généré le ${new Date().toLocaleString('fr-FR')}
|
|
=================================================
|
|
`.trim();
|
|
|
|
// Télécharger le rapport
|
|
const blob = new Blob([reportText], { type: 'text/plain;charset=utf-8' });
|
|
const link = document.createElement('a');
|
|
link.href = URL.createObjectURL(blob);
|
|
link.download = `rapport-tirage-${existingDraw.id}.txt`;
|
|
link.click();
|
|
|
|
toast.success('Rapport téléchargé!');
|
|
} catch (error: any) {
|
|
toast.error(error.message || 'Erreur lors du téléchargement');
|
|
}
|
|
};
|
|
|
|
const markAsNotified = async () => {
|
|
if (!existingDraw) return;
|
|
|
|
try {
|
|
const token = localStorage.getItem('auth_token') || localStorage.getItem('token');
|
|
const response = await fetch(
|
|
`${API_BASE_URL}/draw/${existingDraw.id}/notify`,
|
|
{
|
|
method: 'PUT',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!response.ok) throw new Error('Erreur');
|
|
|
|
toast.success('Gagnant marqué comme notifié');
|
|
await checkExistingDraw();
|
|
} catch (error: any) {
|
|
toast.error(error.message || 'Erreur');
|
|
}
|
|
};
|
|
|
|
const markAsClaimed = async () => {
|
|
if (!existingDraw) return;
|
|
|
|
const notes = prompt('Notes (optionnel):');
|
|
|
|
try {
|
|
const token = localStorage.getItem('auth_token') || localStorage.getItem('token');
|
|
const response = await fetch(
|
|
`${API_BASE_URL}/draw/${existingDraw.id}/claim`,
|
|
{
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({ notes }),
|
|
}
|
|
);
|
|
|
|
if (!response.ok) throw new Error('Erreur');
|
|
|
|
toast.success('Lot marqué comme récupéré');
|
|
await checkExistingDraw();
|
|
} catch (error: any) {
|
|
toast.error(error.message || 'Erreur');
|
|
}
|
|
};
|
|
|
|
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');
|
|
const response = await fetch(
|
|
`${API_BASE_URL}/draw/${existingDraw.id}`,
|
|
{
|
|
method: 'DELETE',
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
let errorMessage = 'Erreur lors de l\'annulation';
|
|
try {
|
|
const error = await response.json();
|
|
errorMessage = error.message || errorMessage;
|
|
} catch (e) {
|
|
// Si le parsing JSON échoue, utiliser le message par défaut
|
|
errorMessage = `Erreur ${response.status}: ${response.statusText}`;
|
|
}
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
toast.success('🗑️ Tirage au sort annulé avec succès!');
|
|
setHasExistingDraw(false);
|
|
setExistingDraw(null);
|
|
setDrawResult(null);
|
|
setAllowRedraw(false);
|
|
await checkExistingDraw();
|
|
await loadParticipants();
|
|
} catch (error: any) {
|
|
console.error('Erreur lors de l\'annulation du tirage:', error);
|
|
toast.error(error.message || 'Erreur lors de l\'annulation');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<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 */}
|
|
<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> •
|
|
Participants ayant joué au moins {minTickets} ticket{minTickets > 1 ? 's' : ''}
|
|
</p>
|
|
</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" />
|
|
</div>
|
|
<h3 className="font-bold text-white text-lg">Tirage déjà effectué</h3>
|
|
</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" />
|
|
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" />
|
|
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" />
|
|
Marquer comme récupéré
|
|
</Button>
|
|
)}
|
|
|
|
<Button
|
|
onClick={deleteDraw}
|
|
size="sm"
|
|
variant="outline"
|
|
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" />
|
|
Annuler ce tirage
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 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="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>
|
|
<div>
|
|
<h2 className="text-xl font-bold text-white">Participants Éligibles</h2>
|
|
<p className="text-blue-100 text-sm">
|
|
{loading
|
|
? 'Chargement en cours...'
|
|
: participants.length > 0
|
|
? `${participants.length} participant${participants.length > 1 ? 's' : ''} éligible${participants.length > 1 ? 's' : ''}`
|
|
: 'Aucun participant'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
onClick={loadParticipants}
|
|
isLoading={loading}
|
|
disabled={loading}
|
|
size="sm"
|
|
className="bg-blue-600 text-white hover:bg-blue-700 rounded-xl"
|
|
>
|
|
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
|
Actualiser
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-6">
|
|
{loading && (
|
|
<div className="text-center py-12">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
<p className="text-gray-600">Chargement des participants éligibles...</p>
|
|
</div>
|
|
)}
|
|
|
|
{!loading && participants.length === 0 && (
|
|
<div className="text-center py-12 bg-gray-50 rounded-xl">
|
|
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<Users className="w-8 h-8 text-gray-400" />
|
|
</div>
|
|
<p className="text-gray-900 text-lg font-medium">Aucun participant éligible</p>
|
|
<p className="text-gray-500 text-sm mt-2">
|
|
Vérifiez que des participants ont joué au moins {minTickets} ticket{minTickets > 1 ? 's' : ''}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{!loading && participants.length > 0 && (
|
|
<>
|
|
<div className="overflow-x-auto rounded-xl border border-gray-100">
|
|
<table className="w-full">
|
|
<thead className="bg-gradient-to-r from-gray-50 to-gray-100">
|
|
<tr>
|
|
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
#
|
|
</th>
|
|
<th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
Participant
|
|
</th>
|
|
<th className="px-6 py-4 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
Tickets Joués
|
|
</th>
|
|
<th className="px-6 py-4 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
Lots Gagnés
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-100">
|
|
{participants.map((participant, index) => (
|
|
<tr key={participant.id} className="hover:bg-blue-50 transition-colors">
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className="inline-flex items-center justify-center w-8 h-8 bg-gray-100 rounded-lg text-sm font-bold text-gray-600">
|
|
{index + 1}
|
|
</span>
|
|
</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">
|
|
{participant.first_name?.charAt(0)}{participant.last_name?.charAt(0)}
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-semibold text-gray-900">
|
|
{participant.first_name} {participant.last_name}
|
|
</p>
|
|
<p className="text-xs text-gray-500">{participant.email}</p>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-center">
|
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold bg-blue-100 text-blue-700">
|
|
{participant.tickets_played}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-center">
|
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold bg-purple-100 text-purple-700">
|
|
{participant.prizes_won}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Bouton de tirage au sort */}
|
|
<div className="mt-6 bg-gradient-to-r from-amber-500 via-orange-500 to-red-500 rounded-2xl p-1">
|
|
<div className="bg-white rounded-xl p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-14 h-14 bg-gradient-to-br from-amber-400 to-orange-500 rounded-xl flex items-center justify-center shadow-lg">
|
|
<Trophy className="w-7 h-7 text-white" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-xl font-bold text-gray-900">
|
|
Prêt à lancer le tirage au sort ?
|
|
</h3>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
<span className="font-semibold text-orange-600">{participants.length}</span> participant{participants.length > 1 ? 's ont' : ' a'} une chance égale de gagner <span className="font-semibold text-purple-600">{prizeName}</span> ({prizeValue}€)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={conductDraw}
|
|
disabled={loading}
|
|
className="flex items-center gap-2 px-8 py-4 bg-gradient-to-r from-amber-500 via-orange-500 to-red-500 text-white font-bold text-lg rounded-xl shadow-lg hover:shadow-xl transform hover:scale-105 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
|
|
>
|
|
<Trophy className="w-6 h-6" />
|
|
Lancer le Tirage
|
|
</button>
|
|
</div>
|
|
</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>
|
|
);
|
|
}
|