feat: improve design of Lots, Marketing & Tirages pages

PrizeManagement (Lots & Prix):
- Add stats cards (Total, Stock, Distribués, Taux)
- Modern card design with gradient headers per prize type
- Stock progress bars and better layout

Marketing Data:
- Improved stat cards with gradient backgrounds
- Modern chart containers with icon badges
- Enhanced export section with gradient header

Tirages:
- Improved existing draw alert with grid layout
- Modern participants table with avatars
- Enhanced draw result display with cards

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
soufiane 2025-12-03 16:41:28 +01:00
parent 8823967782
commit 055db16529
3 changed files with 510 additions and 336 deletions

View File

@ -238,60 +238,68 @@ export default function MarketingPage() {
{/* Statistiques globales */} {/* Statistiques globales */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<Card className="p-6"> <div className="bg-gradient-to-br from-blue-50 to-blue-100 rounded-2xl p-6 border border-blue-200">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm text-gray-600">Total Clients</p> <p className="text-sm text-blue-600 font-medium">Total Clients</p>
<p className="text-3xl font-bold text-gray-900 mt-1"> <p className="text-3xl font-bold text-blue-700 mt-1">
{stats.totalClients.toLocaleString()} {stats.totalClients.toLocaleString()}
</p> </p>
</div> </div>
<Users className="w-12 h-12 text-blue-500" /> <div className="w-14 h-14 bg-blue-200 rounded-xl flex items-center justify-center">
<Users className="w-7 h-7 text-blue-600" />
</div>
</div>
</div> </div>
</Card>
<Card className="p-6"> <div className="bg-gradient-to-br from-green-50 to-green-100 rounded-2xl p-6 border border-green-200">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm text-gray-600">Participants Actifs</p> <p className="text-sm text-green-600 font-medium">Participants Actifs</p>
<p className="text-3xl font-bold text-green-600 mt-1"> <p className="text-3xl font-bold text-green-700 mt-1">
{stats.activeParticipants.toLocaleString()} {stats.activeParticipants.toLocaleString()}
</p> </p>
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-green-600 mt-1">
{((stats.activeParticipants / stats.totalClients) * 100).toFixed(1)}% du total {((stats.activeParticipants / stats.totalClients) * 100).toFixed(1)}% du total
</p> </p>
</div> </div>
<UserCheck className="w-12 h-12 text-green-500" /> <div className="w-14 h-14 bg-green-200 rounded-xl flex items-center justify-center">
<UserCheck className="w-7 h-7 text-green-600" />
</div>
</div>
</div> </div>
</Card>
<Card className="p-6"> <div className="bg-gradient-to-br from-purple-50 to-purple-100 rounded-2xl p-6 border border-purple-200">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm text-gray-600">Gagnants</p> <p className="text-sm text-purple-600 font-medium">Gagnants</p>
<p className="text-3xl font-bold text-purple-600 mt-1"> <p className="text-3xl font-bold text-purple-700 mt-1">
{stats.winners.toLocaleString()} {stats.winners.toLocaleString()}
</p> </p>
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-purple-600 mt-1">
{((stats.winners / stats.activeParticipants) * 100).toFixed(1)}% de conversion {stats.activeParticipants > 0 ? ((stats.winners / stats.activeParticipants) * 100).toFixed(1) : 0}% de conversion
</p> </p>
</div> </div>
<Gift className="w-12 h-12 text-purple-500" /> <div className="w-14 h-14 bg-purple-200 rounded-xl flex items-center justify-center">
<Gift className="w-7 h-7 text-purple-600" />
</div>
</div>
</div> </div>
</Card>
<Card className="p-6"> <div className="bg-gradient-to-br from-orange-50 to-orange-100 rounded-2xl p-6 border border-orange-200">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm text-gray-600">Inactifs</p> <p className="text-sm text-orange-600 font-medium">Inactifs</p>
<p className="text-3xl font-bold text-orange-600 mt-1"> <p className="text-3xl font-bold text-orange-700 mt-1">
{stats.inactiveParticipants.toLocaleString()} {stats.inactiveParticipants.toLocaleString()}
</p> </p>
<p className="text-xs text-gray-500 mt-1">À réactiver</p> <p className="text-xs text-orange-600 mt-1">À réactiver</p>
</div>
<div className="w-14 h-14 bg-orange-200 rounded-xl flex items-center justify-center">
<TrendingUp className="w-7 h-7 text-orange-600" />
</div>
</div> </div>
<TrendingUp className="w-12 h-12 text-orange-500" />
</div> </div>
</Card>
</div> </div>
{/* Graphiques - Affichés uniquement s'il y a des données valides */} {/* Graphiques - Affichés uniquement s'il y a des données valides */}
@ -300,9 +308,11 @@ export default function MarketingPage() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{/* Répartition par genre */} {/* Répartition par genre */}
{filteredGenderData.length > 0 && ( {filteredGenderData.length > 0 && (
<Card className="p-6"> <div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
<h2 className="text-xl font-bold mb-4 flex items-center gap-2"> <h2 className="text-xl font-bold mb-4 flex items-center gap-2">
<BarChart3 className="w-6 h-6 text-blue-600" /> <div className="w-10 h-10 bg-blue-100 rounded-xl flex items-center justify-center">
<BarChart3 className="w-5 h-5 text-blue-600" />
</div>
Répartition par Genre Répartition par Genre
</h2> </h2>
<ResponsiveContainer width="100%" height={300}> <ResponsiveContainer width="100%" height={300}>
@ -324,14 +334,16 @@ export default function MarketingPage() {
<Legend /> <Legend />
</PieChart> </PieChart>
</ResponsiveContainer> </ResponsiveContainer>
</Card> </div>
)} )}
{/* Répartition par âge */} {/* Répartition par âge */}
{filteredAgeData.length > 0 && ( {filteredAgeData.length > 0 && (
<Card className="p-6"> <div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
<h2 className="text-xl font-bold mb-4 flex items-center gap-2"> <h2 className="text-xl font-bold mb-4 flex items-center gap-2">
<BarChart3 className="w-6 h-6 text-green-600" /> <div className="w-10 h-10 bg-green-100 rounded-xl flex items-center justify-center">
<BarChart3 className="w-5 h-5 text-green-600" />
</div>
Répartition par Âge Répartition par Âge
</h2> </h2>
<ResponsiveContainer width="100%" height={300}> <ResponsiveContainer width="100%" height={300}>
@ -341,54 +353,60 @@ export default function MarketingPage() {
<YAxis /> <YAxis />
<Tooltip /> <Tooltip />
<Legend /> <Legend />
<Bar dataKey="count" fill="#10b981" name="Participants" /> <Bar dataKey="count" fill="#10b981" name="Participants" radius={[4, 4, 0, 0]} />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</Card> </div>
)} )}
</div> </div>
{/* Top villes */} {/* Top villes */}
{filteredCityData.length > 0 && ( {filteredCityData.length > 0 && (
<Card className="p-6 mb-6"> <div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-6 mb-6">
<h2 className="text-xl font-bold mb-4 flex items-center gap-2"> <h2 className="text-xl font-bold mb-4 flex items-center gap-2">
<MapPin className="w-6 h-6 text-purple-600" /> <div className="w-10 h-10 bg-purple-100 rounded-xl flex items-center justify-center">
<MapPin className="w-5 h-5 text-purple-600" />
</div>
Top Villes ({filteredCityData.length}) Top Villes ({filteredCityData.length})
</h2> </h2>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4"> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
{filteredCityData.slice(0, 10).map((city, index) => ( {filteredCityData.slice(0, 10).map((city, index) => (
<div <div
key={index} key={index}
className="bg-gradient-to-br from-purple-50 to-blue-50 p-4 rounded-lg border border-purple-200" className="bg-gradient-to-br from-purple-50 to-indigo-50 p-4 rounded-xl border border-purple-100 hover:shadow-md transition-shadow"
> >
<p className="text-sm text-gray-600">#{index + 1}</p> <span className="inline-block px-2 py-0.5 bg-purple-200 text-purple-700 text-xs font-bold rounded-full mb-2">#{index + 1}</span>
<p className="font-bold text-gray-900 truncate">{city.city}</p> <p className="font-bold text-gray-900 truncate">{city.city}</p>
<p className="text-2xl font-bold text-purple-600">{city.count}</p> <p className="text-2xl font-bold text-purple-600">{city.count}</p>
</div> </div>
))} ))}
</div> </div>
</Card> </div>
)} )}
</> </>
)} )}
{/* Section Export */} {/* Section Export */}
<Card className="p-6"> <div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
<h2 className="text-xl font-bold mb-4 flex items-center gap-2"> <div className="bg-gradient-to-r from-blue-600 to-indigo-600 px-6 py-4">
<Download className="w-6 h-6 text-blue-600" /> <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>
Exporter les Données pour Emailing Exporter les Données pour Emailing
</h2> </h2>
</div>
<div className="space-y-4"> <div className="p-6 space-y-5">
{/* Sélection du segment */} {/* Sélection du segment */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-semibold text-gray-700 mb-2">
Segment à exporter Segment à exporter
</label> </label>
<select <select
value={selectedSegment} value={selectedSegment}
onChange={(e) => setSelectedSegment(e.target.value)} onChange={(e) => setSelectedSegment(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="w-full px-4 py-3 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">
@ -407,11 +425,11 @@ export default function MarketingPage() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{filteredCityData.length > 0 && ( {filteredCityData.length > 0 && (
<div> <div>
<label className="block text-sm text-gray-700 mb-1">Ville</label> <label className="block text-sm font-medium text-gray-700 mb-2">Ville</label>
<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-3 py-2 border border-gray-300 rounded-lg text-sm" className="w-full px-4 py-3 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) => (
@ -425,11 +443,11 @@ export default function MarketingPage() {
{filteredGenderData.length > 0 && ( {filteredGenderData.length > 0 && (
<div> <div>
<label className="block text-sm text-gray-700 mb-1">Genre</label> <label className="block text-sm font-medium text-gray-700 mb-2">Genre</label>
<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-3 py-2 border border-gray-300 rounded-lg text-sm" className="w-full px-4 py-3 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) => (
@ -444,12 +462,12 @@ export default function MarketingPage() {
)} )}
{/* Boutons d'action */} {/* Boutons d'action */}
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-2">
<Button <Button
onClick={exportSegmentData} onClick={exportSegmentData}
isLoading={exportLoading} isLoading={exportLoading}
disabled={exportLoading} disabled={exportLoading}
className="bg-blue-600 hover:bg-blue-700" className="bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700"
> >
<Download className="w-5 h-5 mr-2" /> <Download className="w-5 h-5 mr-2" />
Exporter en CSV Exporter en CSV
@ -461,7 +479,11 @@ export default function MarketingPage() {
</Button> </Button>
</div> </div>
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg"> <div className="p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100 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>
<p className="text-sm text-blue-800"> <p className="text-sm text-blue-800">
<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
@ -469,7 +491,8 @@ export default function MarketingPage() {
</p> </p>
</div> </div>
</div> </div>
</Card> </div>
</div>
</div> </div>
); );
} }

View File

@ -378,47 +378,62 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
{/* Alerte si un tirage existe déjà */} {/* Alerte si un tirage existe déjà */}
{hasExistingDraw && existingDraw && ( {hasExistingDraw && existingDraw && (
<Card className="p-6 mb-6 border-yellow-500 bg-yellow-50"> <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="flex items-start gap-4"> <div className="bg-gradient-to-r from-amber-500 to-yellow-500 px-6 py-3">
<AlertCircle className="w-6 h-6 text-yellow-600 flex-shrink-0 mt-1" /> <div className="flex items-center gap-3">
<div className="flex-1"> <div className="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center">
<h3 className="font-bold text-yellow-900 mb-2">Un tirage a déjà é effectué!</h3> <Trophy className="w-5 h-5 text-white" />
<div className="space-y-1 text-sm text-yellow-800"> </div>
<p><strong>Date:</strong> {new Date(existingDraw.draw_date).toLocaleString('fr-FR')}</p> <h3 className="font-bold text-white text-lg">Tirage déjà effectué</h3>
<p><strong>Gagnant:</strong> {existingDraw.winner_name} ({existingDraw.winner_email})</p> </div>
<p><strong>Prix:</strong> {existingDraw.prize_name} - {existingDraw.prize_value}</p> </div>
<p><strong>Participants éligibles:</strong> {existingDraw.eligible_participants} / {existingDraw.total_participants}</p> <div className="p-6">
<p> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
<strong>Statut:</strong>{' '} <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 <span
className={`px-2 py-1 rounded text-xs ${ className={`inline-flex px-3 py-1 rounded-full text-sm font-semibold ${
existingDraw.status === 'CLAIMED' existingDraw.status === 'CLAIMED'
? 'bg-green-100 text-green-800' ? 'bg-green-100 text-green-700'
: existingDraw.status === 'NOTIFIED' : existingDraw.status === 'NOTIFIED'
? 'bg-blue-100 text-blue-800' ? 'bg-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-800' : 'bg-gray-100 text-gray-700'
}`} }`}
> >
{existingDraw.status} {existingDraw.status === 'CLAIMED' ? 'Récupéré' : existingDraw.status === 'NOTIFIED' ? 'Notifié' : existingDraw.status}
</span> </span>
</p> </div>
</div> </div>
<div className="flex gap-2 mt-4 flex-wrap"> <div className="flex gap-2 flex-wrap">
<Button onClick={downloadReport} size="sm" variant="outline"> <Button onClick={downloadReport} size="sm" variant="outline" className="rounded-xl">
<Download className="w-4 h-4 mr-1" /> <Download className="w-4 h-4 mr-1" />
Télécharger rapport Télécharger rapport
</Button> </Button>
{existingDraw.status === 'COMPLETED' && ( {existingDraw.status === 'COMPLETED' && (
<Button onClick={markAsNotified} size="sm" variant="outline"> <Button onClick={markAsNotified} size="sm" variant="outline" className="rounded-xl">
<Mail className="w-4 h-4 mr-1" /> <Mail className="w-4 h-4 mr-1" />
Marquer comme notifié Marquer comme notifié
</Button> </Button>
)} )}
{existingDraw.status === 'NOTIFIED' && ( {existingDraw.status === 'NOTIFIED' && (
<Button onClick={markAsClaimed} size="sm" variant="outline"> <Button onClick={markAsClaimed} size="sm" variant="outline" className="rounded-xl">
<CheckCircle className="w-4 h-4 mr-1" /> <CheckCircle className="w-4 h-4 mr-1" />
Marquer comme récupéré Marquer comme récupéré
</Button> </Button>
@ -428,7 +443,7 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
onClick={deleteDraw} onClick={deleteDraw}
size="sm" size="sm"
variant="outline" variant="outline"
className="border-red-300 text-red-700 hover:bg-red-50 hover:border-red-400" 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-1" />
Annuler ce tirage Annuler ce tirage
@ -436,47 +451,54 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
</div> </div>
</div> </div>
</div> </div>
</Card>
)} )}
{/* Liste des participants éligibles */} {/* Liste des participants éligibles */}
<Card className="p-6 mb-6"> <div className="bg-white rounded-2xl shadow-sm border border-gray-100 mb-6 overflow-hidden">
<div className="flex items-center justify-between mb-6"> <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> <div>
<h2 className="text-2xl font-bold flex items-center gap-2"> <h2 className="text-xl font-bold text-white">Participants Éligibles</h2>
<Users className="w-6 h-6 text-blue-600" /> <p className="text-blue-100 text-sm">
Participants Éligibles
</h2>
<p className="text-gray-600 text-sm mt-1">
{loading {loading
? 'Chargement en cours...' ? 'Chargement en cours...'
: participants.length > 0 : participants.length > 0
? `${participants.length} participant${participants.length > 1 ? 's' : ''} éligible${participants.length > 1 ? 's' : ''} au tirage` ? `${participants.length} participant${participants.length > 1 ? 's' : ''} éligible${participants.length > 1 ? 's' : ''}`
: 'Aucun participant chargé'} : 'Aucun participant'}
</p> </p>
</div> </div>
</div>
<Button <Button
onClick={loadParticipants} onClick={loadParticipants}
isLoading={loading} isLoading={loading}
disabled={loading} disabled={loading}
size="sm" size="sm"
className="bg-white text-blue-600 hover:bg-blue-50 rounded-xl"
> >
<RefreshCw className="w-4 h-4 mr-2" /> <RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
Actualiser Actualiser
</Button> </Button>
</div> </div>
</div>
<div className="p-6">
{loading && ( {loading && (
<div className="text-center py-12"> <div className="text-center py-12">
<RefreshCw className="w-12 h-12 text-blue-600 animate-spin mx-auto mb-4" /> <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> <p className="text-gray-600">Chargement des participants éligibles...</p>
</div> </div>
)} )}
{!loading && participants.length === 0 && ( {!loading && participants.length === 0 && (
<div className="text-center py-12 bg-gray-50 rounded-lg"> <div className="text-center py-12 bg-gray-50 rounded-xl">
<Users className="w-16 h-16 text-gray-400 mx-auto mb-4" /> <div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<p className="text-gray-600 text-lg font-medium">Aucun participant éligible</p> <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"> <p className="text-gray-500 text-sm mt-2">
Vérifiez que des participants ont joué au moins {minTickets} ticket{minTickets > 1 ? 's' : ''} Vérifiez que des participants ont joué au moins {minTickets} ticket{minTickets > 1 ? 's' : ''}
</p> </p>
@ -485,51 +507,52 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
{!loading && participants.length > 0 && ( {!loading && participants.length > 0 && (
<> <>
<div className="overflow-x-auto"> <div className="overflow-x-auto rounded-xl border border-gray-100">
<table className="w-full"> <table className="w-full">
<thead className="bg-gradient-to-r from-blue-50 to-purple-50"> <thead className="bg-gradient-to-r from-gray-50 to-gray-100">
<tr> <tr>
<th className="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
# #
</th> </th>
<th className="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
Nom Complet Participant
</th> </th>
<th className="px-6 py-4 text-left text-xs font-bold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider">
Email
</th>
<th className="px-6 py-4 text-center text-xs font-bold text-gray-700 uppercase tracking-wider">
Tickets Joués Tickets Joués
</th> </th>
<th className="px-6 py-4 text-center text-xs font-bold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider">
Lots Gagnés Lots Gagnés
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="divide-y divide-gray-100">
{participants.map((participant, index) => ( {participants.map((participant, index) => (
<tr key={participant.id} className="hover:bg-blue-50 transition-colors"> <tr key={participant.id} className="hover:bg-blue-50 transition-colors">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"> <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} {index + 1}
</span>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center"> <div className="flex items-center gap-3">
<User className="w-5 h-5 text-blue-500 mr-2" /> <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">
<span className="text-sm font-medium text-gray-900"> {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} {participant.first_name} {participant.last_name}
</span> </p>
<p className="text-xs text-gray-500">{participant.email}</p>
</div>
</div> </div>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
{participant.email}
</td>
<td className="px-6 py-4 whitespace-nowrap text-center"> <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-medium bg-blue-100 text-blue-800"> <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} {participant.tickets_played}
</span> </span>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-center"> <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-medium bg-purple-100 text-purple-800"> <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} {participant.prizes_won}
</span> </span>
</td> </td>
@ -540,7 +563,7 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
</div> </div>
{/* Bouton de tirage au sort */} {/* Bouton de tirage au sort */}
<div className="mt-8 p-6 bg-gradient-to-r from-yellow-50 to-orange-50 rounded-lg border-2 border-yellow-200"> <div className="mt-6 p-6 bg-gradient-to-r from-amber-50 to-orange-50 rounded-xl border-2 border-amber-200">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-lg font-bold text-gray-900 mb-1"> <h3 className="text-lg font-bold text-gray-900 mb-1">
@ -554,7 +577,7 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
onClick={conductDraw} onClick={conductDraw}
disabled={loading} disabled={loading}
size="lg" size="lg"
className="bg-gradient-to-r from-yellow-500 to-orange-500 hover:from-yellow-600 hover:to-orange-600" className="bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 rounded-xl shadow-lg"
> >
<Trophy className="w-5 h-5 mr-2" /> <Trophy className="w-5 h-5 mr-2" />
Lancer le Tirage Lancer le Tirage
@ -563,63 +586,87 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
</div> </div>
</> </>
)} )}
</Card> </div>
</div>
{/* Résultat du tirage */} {/* Résultat du tirage */}
{drawResult && ( {drawResult && (
<Card className="p-6 bg-gradient-to-br from-yellow-50 to-orange-50 border-yellow-500"> <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="text-center"> <div className="bg-gradient-to-r from-amber-500 to-orange-500 px-6 py-4 text-center">
<Trophy className="w-16 h-16 text-yellow-600 mx-auto mb-4" /> <Trophy className="w-12 h-12 text-white mx-auto mb-2" />
<h2 className="text-3xl font-bold text-gray-900 mb-2"> <h2 className="text-2xl font-bold text-white">
Félicitations au gagnant ! Félicitations au gagnant !
</h2> </h2>
</div>
<div className="bg-white rounded-lg p-6 my-6 shadow-lg"> <div className="p-8">
<User className="w-12 h-12 text-blue-600 mx-auto mb-3" /> <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<h3 className="text-2xl font-bold text-gray-900 mb-2"> {/* 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} {drawResult.winner.name}
</h3> </h3>
<p className="text-gray-600 mb-1">{drawResult.winner.email}</p> </div>
<p className="text-sm text-gray-500"> </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) {drawResult.winner.ticketsPlayed} ticket(s) joué(s)
</p> </p>
</div> </div>
</div>
<div className="bg-white rounded-lg p-6 shadow-lg"> {/* Prix */}
<Gift className="w-12 h-12 text-purple-600 mx-auto mb-3" /> <div className="bg-white rounded-2xl p-6 shadow-lg border border-purple-100">
<h3 className="text-xl font-bold text-gray-900 mb-1"> <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} {drawResult.prize.name}
</h3> </h3>
<p className="text-2xl font-bold text-purple-600"> </div>
</div>
<p className="text-3xl font-bold text-purple-600">
{drawResult.prize.value} {drawResult.prize.value}
</p> </p>
</div> </div>
</div>
<div className="mt-6 grid grid-cols-3 gap-4 text-center"> {/* Stats */}
<div> <div className="grid grid-cols-3 gap-4 mb-6">
<p className="text-2xl font-bold text-gray-900"> <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} {drawResult.statistics.totalParticipants}
</p> </p>
<p className="text-sm text-gray-600">Total participants</p> <p className="text-sm text-gray-500">Total participants</p>
</div> </div>
<div> <div className="bg-white rounded-xl p-4 text-center border border-blue-100">
<p className="text-2xl font-bold text-blue-600"> <p className="text-3xl font-bold text-blue-600">
{drawResult.statistics.eligibleParticipants} {drawResult.statistics.eligibleParticipants}
</p> </p>
<p className="text-sm text-gray-600">Éligibles</p> <p className="text-sm text-gray-500">Éligibles</p>
</div> </div>
<div> <div className="bg-white rounded-xl p-4 text-center border border-green-100">
<p className="text-2xl font-bold text-green-600">1</p> <p className="text-3xl font-bold text-green-600">1</p>
<p className="text-sm text-gray-600">Gagnant</p> <p className="text-sm text-gray-500">Gagnant</p>
</div> </div>
</div> </div>
<div className="mt-6 flex gap-3 justify-center"> <div className="flex gap-3 justify-center">
<Button onClick={downloadReport}> <Button onClick={downloadReport} className="rounded-xl">
<Download className="w-4 h-4 mr-2" /> <Download className="w-4 h-4 mr-2" />
Télécharger le rapport Télécharger le rapport
</Button> </Button>
<Button variant="outline" onClick={() => { <Button variant="outline" className="rounded-xl" onClick={() => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
window.location.reload(); window.location.reload();
} }
@ -629,7 +676,7 @@ ${report.draw.notifiedAt ? `📧 Gagnant notifié le: ${new Date(report.draw.not
</Button> </Button>
</div> </div>
</div> </div>
</Card> </div>
)} )}
</div> </div>
); );

View File

@ -1,8 +1,9 @@
'use client'; 'use client';
import { useState, useEffect } 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';
export default function PrizeManagement() { export default function PrizeManagement() {
const [prizes, setPrizes] = useState<Prize[]>([]); const [prizes, setPrizes] = useState<Prize[]>([]);
@ -91,86 +92,189 @@ export default function PrizeManagement() {
setIsModalOpen(false); setIsModalOpen(false);
}; };
// Calculer les stats
const prizeStats = useMemo(() => {
const totalStock = prizes.reduce((acc, p) => acc + (p.initialStock || p.stock || 0), 0);
const totalUsed = prizes.reduce((acc, p) => acc + (p.ticketsUsed || 0), 0);
const activeCount = prizes.filter(p => p.isActive).length;
return { totalStock, totalUsed, activeCount, totalPrizes: prizes.length };
}, [prizes]);
// Fonction pour obtenir l'icône et la couleur selon le type
const getPrizeStyle = (type: string) => {
switch (type) {
case 'GRAND_PRIZE':
return { bg: 'from-purple-500 to-purple-600', icon: Trophy, color: 'purple' };
case 'PHYSICAL':
return { bg: 'from-blue-500 to-blue-600', icon: Package, color: 'blue' };
default:
return { bg: 'from-emerald-500 to-emerald-600', icon: Gift, color: 'emerald' };
}
};
if (loading) { if (loading) {
return <div className="text-center py-8">Chargement des prix...</div>; return (
<div className="flex items-center justify-center py-16">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-purple-600 mx-auto mb-4"></div>
<p className="text-gray-600">Chargement des lots...</p>
</div>
</div>
);
} }
return ( return (
<div className="p-6"> <div className="p-6">
<div className="flex justify-between items-center mb-6"> {/* Stats rapides */}
<h1 className="text-2xl font-bold">Gestion des Prix</h1> <div className="grid grid-cols-2 md:grid-cols-4 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">
<Gift className="w-5 h-5 text-purple-600" />
</div>
<div>
<p className="text-2xl font-bold text-purple-700">{prizeStats.totalPrizes}</p>
<p className="text-xs text-purple-600">Total Lots</p>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-green-50 to-green-100 rounded-xl p-4 border border-green-200">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-green-200 rounded-lg flex items-center justify-center">
<Archive className="w-5 h-5 text-green-600" />
</div>
<div>
<p className="text-2xl font-bold text-green-700">{prizeStats.totalStock}</p>
<p className="text-xs text-green-600">Stock Total</p>
</div>
</div>
</div>
<div className="bg-gradient-to-br from-blue-50 to-blue-100 rounded-xl p-4 border border-blue-200">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-200 rounded-lg flex items-center justify-center">
<Package className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="text-2xl font-bold text-blue-700">{prizeStats.totalUsed}</p>
<p className="text-xs text-blue-600">Distribués</p>
</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}%</p>
<p className="text-xs text-amber-600">Taux distrib.</p>
</div>
</div>
</div>
</div> </div>
{error && ( {error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4"> <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-xl mb-6 flex items-center gap-3">
{error} <div className="w-10 h-10 bg-red-100 rounded-full flex items-center justify-center">
<X className="w-5 h-5 text-red-600" />
</div>
<span>{error}</span>
</div> </div>
)} )}
{/* Liste des prix */} {/* Liste des prix */}
{prizes.length === 0 ? ( {prizes.length === 0 ? (
<div className="text-center py-12 text-gray-500"> <div className="text-center py-16 bg-gray-50 rounded-2xl border-2 border-dashed border-gray-200">
Aucun prix trouvé. Cliquez sur "Ajouter un prix" pour commencer. <div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Gift className="w-8 h-8 text-gray-400" />
</div>
<p className="text-gray-500 font-medium">Aucun lot trouvé</p>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{prizes.map((prize) => ( {prizes.map((prize) => {
const style = getPrizeStyle(prize.type);
const IconComponent = style.icon;
const stockRemaining = (prize.initialStock || 0) - (prize.ticketsUsed || 0);
return (
<div <div
key={prize.id} key={prize.id}
className="border rounded-lg p-4 shadow-sm hover:shadow-md transition" className="bg-white rounded-2xl border border-gray-100 shadow-sm hover:shadow-lg transition-all overflow-hidden"
> >
<div className="flex justify-between items-start mb-3"> {/* Header avec gradient */}
<h3 className="font-semibold text-lg">{prize.name}</h3> <div className={`bg-gradient-to-r ${style.bg} p-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">
<IconComponent className="w-5 h-5 text-white" />
</div>
<h3 className="font-bold text-white text-lg">{prize.name}</h3>
</div>
<span <span
className={`px-2 py-1 rounded text-xs ${ className={`px-3 py-1 rounded-full text-xs font-semibold ${
prize.isActive prize.isActive
? 'bg-green-100 text-green-800' ? 'bg-white/20 text-white'
: 'bg-gray-100 text-gray-800' : 'bg-gray-800/20 text-white/70'
}`} }`}
> >
{prize.isActive ? 'Actif' : 'Inactif'} {prize.isActive ? 'Actif' : 'Inactif'}
</span> </span>
</div> </div>
</div>
<p className="text-sm text-gray-600 mb-2">{prize.description}</p> {/* Contenu */}
<div className="p-5">
<p className="text-sm text-gray-600 mb-4">{prize.description}</p>
<div className="space-y-1 text-sm mb-3"> {/* Affichage spécial pour le Grand Prix */}
<p><strong>Type:</strong> {prize.type}</p>
<p><strong>Valeur:</strong> {prize.value}</p>
{/* Affichage spécial pour le Grand Prix (tirage au sort) */}
{prize.type === 'GRAND_PRIZE' ? ( {prize.type === 'GRAND_PRIZE' ? (
<> <div className="p-4 bg-gradient-to-r from-purple-50 to-pink-50 rounded-xl border border-purple-100">
<p><strong>Probabilité:</strong> N/A (Tirage au sort)</p> <div className="flex items-center gap-2 mb-2">
<div className="mt-2 p-2 bg-purple-50 border border-purple-200 rounded"> <Trophy className="w-5 h-5 text-purple-600" />
<p className="text-purple-800 font-semibold text-xs"> <span className="font-bold text-purple-800">TIRAGE AU SORT</span>
🎯 TIRAGE AU SORT </div>
</p> <p className="text-sm text-purple-700">
<p className="text-purple-700 text-xs mt-1">
Attribué lors du tirage final parmi les participants éligibles Attribué lors du tirage final parmi les participants éligibles
</p> </p>
</div> </div>
</>
) : ( ) : (
<> <div className="space-y-3">
<p><strong>Probabilité:</strong> {(prize.probability * 100).toFixed(1)}%</p> {/* Probabilité */}
<p> <div className="flex items-center justify-between p-3 bg-gray-50 rounded-xl">
<strong>Stock généré:</strong> {prize.initialStock !== undefined ? prize.initialStock : prize.stock} <span className="text-sm text-gray-600">Probabilité</span>
</p> <span className="font-bold text-gray-900">{(prize.probability * 100).toFixed(1)}%</span>
<p> </div>
<strong>Stock restant:</strong>{' '}
<span className={((prize.initialStock || 0) - (prize.ticketsUsed || 0)) === 0 ? 'text-red-600 font-semibold' : 'text-green-600 font-semibold'}> {/* Stock */}
{(prize.initialStock || 0) - (prize.ticketsUsed || 0)} <div className="p-3 bg-gray-50 rounded-xl">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-600">Stock</span>
<span className={`font-bold ${stockRemaining === 0 ? 'text-red-600' : 'text-green-600'}`}>
{stockRemaining} / {prize.initialStock || prize.stock}
</span> </span>
</p> </div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className={`h-2 rounded-full ${stockRemaining === 0 ? 'bg-red-500' : 'bg-green-500'}`}
style={{ width: `${((prize.initialStock || prize.stock) > 0 ? (stockRemaining / (prize.initialStock || prize.stock)) * 100 : 0)}%` }}
></div>
</div>
</div>
{/* Tickets utilisés */}
{prize.ticketsUsed !== undefined && prize.ticketsUsed > 0 && ( {prize.ticketsUsed !== undefined && prize.ticketsUsed > 0 && (
<p><strong>Tickets utilisés:</strong> {prize.ticketsUsed}</p> <div className="flex items-center justify-between p-3 bg-blue-50 rounded-xl">
<span className="text-sm text-blue-700">Distribués</span>
<span className="font-bold text-blue-800">{prize.ticketsUsed}</span>
</div>
)} )}
</> </div>
)} )}
</div> </div>
</div> </div>
))} );
})}
</div> </div>
)} )}
</div> </div>