Files
THE-TIP-TOP/frontend/src/pages/Admin.jsx

662 lines
30 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react'
import { getStatsApi, getUsersApi, drawWinnerApi, getWinnerApi, getEmailingContactsApi, setGameSettingsApi } from '../services/admin'
import { searchTicketApi, deliverClaimApi } from '../services/employee'
import { useHandleAuthError } from '../hooks/useHandleAuthError'
function StatCard({ label, value, sub }) {
return (
<div className="bg-white rounded-xl shadow p-4 text-center">
<p className="text-2xl sm:text-3xl font-bold text-[#006B6B]">{value ?? '—'}</p>
<p className="text-xs sm:text-sm text-gray-500 mt-1">{label}</p>
{sub && <p className="text-xs text-gray-500 mt-0.5">{sub}</p>}
</div>
)
}
// Les cinq états renvoyés par l'API, chacun avec sa couleur : « programmé »
// ne doit pas être confondu avec « retraits uniquement ».
const PHASE_BADGES = {
NOT_CONFIGURED: { label: 'Non lancé', className: 'bg-amber-100 text-amber-700' },
SCHEDULED: { label: 'Programmé', className: 'bg-indigo-100 text-indigo-700' },
OPEN: { label: 'Participations ouvertes', className: 'bg-green-100 text-green-700' },
CLAIM_ONLY: { label: 'Retraits uniquement', className: 'bg-blue-100 text-blue-700' },
CLOSED: { label: 'Terminé', className: 'bg-red-100 text-red-700' },
}
const PHASE_PANELS = {
NOT_CONFIGURED: 'bg-amber-50 border border-amber-200',
SCHEDULED: 'bg-indigo-50 border border-indigo-200',
OPEN: 'bg-white',
CLAIM_ONLY: 'bg-white',
CLOSED: 'bg-red-50 border border-red-200',
}
const PHASE_TITLES = {
NOT_CONFIGURED: 'Jeu-concours non lancé',
SCHEDULED: 'Jeu-concours programmé',
OPEN: 'Jeu-concours en cours',
CLAIM_ONLY: 'Participations closes, retraits en cours',
CLOSED: 'Jeu-concours terminé',
}
function formatDate(dateStr) {
return new Date(dateStr).toLocaleDateString('fr-FR', {
day: '2-digit', month: 'long', year: 'numeric',
hour: '2-digit', minute: '2-digit',
})
}
export default function Admin() {
const handleAuthError = useHandleAuthError()
const [stats, setStats] = useState(null)
const [users, setUsers] = useState([])
const [winner, setWinner] = useState(undefined)
const [drawing, setDrawing] = useState(false)
const [drawError, setDrawError] = useState('')
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [showConfirm, setShowConfirm] = useState(false)
// Lancement du jeu-concours
const [startDateInput, setStartDateInput] = useState('')
const [settingsSaving, setSettingsSaving] = useState(false)
const [settingsError, setSettingsError] = useState('')
const [settingsOk, setSettingsOk] = useState('')
// Remise de lot
const [ticketCode, setTicketCode] = useState('')
const [ticketResult, setTicketResult] = useState(null)
const [ticketSearching, setTicketSearching] = useState(false)
const [ticketDelivering, setTicketDelivering] = useState(false)
const [ticketSearchErr, setTicketSearchErr] = useState('')
const [ticketDeliverErr, setTicketDeliverErr] = useState('')
const [ticketSuccess, setTicketSuccess] = useState(false)
// Contacts emailing
const [emailingContacts, setEmailingContacts] = useState(null)
const [emailingLoading, setEmailingLoading] = useState(false)
const [emailingError, setEmailingError] = useState('')
useEffect(() => {
Promise.all([getStatsApi(), getUsersApi(), getWinnerApi()])
.then(([s, u, w]) => {
setStats(s)
setUsers(u.users)
setWinner(w)
})
.catch((e) => {
if (!handleAuthError(e)) setError(e.message)
})
.finally(() => setLoading(false))
}, [handleAuthError])
const handleSaveGameSettings = async (e) => {
e.preventDefault()
setSettingsError('')
setSettingsOk('')
setSettingsSaving(true)
try {
// L'input datetime-local renvoie une heure locale sans fuseau : on la
// convertit en ISO pour que le serveur reçoive un instant sans ambiguïté.
const data = await setGameSettingsApi(new Date(startDateInput).toISOString())
setStats((prev) => (prev ? { ...prev, gameWindow: data.gameWindow } : prev))
setSettingsOk(data.message)
setStartDateInput('')
} catch (err) {
if (!handleAuthError(err)) setSettingsError(err.message)
} finally {
setSettingsSaving(false)
}
}
const handleDraw = async () => {
setShowConfirm(false)
setDrawing(true)
setDrawError('')
try {
const data = await drawWinnerApi()
setWinner(data)
} catch (e) {
if (!handleAuthError(e)) setDrawError(e.message)
} finally {
setDrawing(false)
}
}
const handleLoadEmailingContacts = async () => {
setEmailingLoading(true)
setEmailingError('')
try {
const data = await getEmailingContactsApi()
setEmailingContacts(data.contacts)
} catch (e) {
if (!handleAuthError(e)) setEmailingError(e.message)
} finally {
setEmailingLoading(false)
}
}
const handleExportEmailingCsv = () => {
if (!emailingContacts?.length) return
const header = ['Prénom', 'Nom', 'Email', 'Inscrit le']
const rows = emailingContacts.map((c) => [
c.firstname, c.lastname, c.email, new Date(c.createdAt).toLocaleDateString('fr-FR'),
])
const csv = [header, ...rows]
.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(';'))
.join('\n')
// Le BOM en tete du fichier est indispensable : sans lui, Excel ouvre le
// CSV en ANSI et casse tous les accents. Ne pas le supprimer.
// eslint-disable-next-line no-irregular-whitespace
const blob = new Blob([`${csv}`], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `contacts-emailing-${new Date().toISOString().slice(0, 10)}.csv`
link.click()
URL.revokeObjectURL(url)
}
const handleTicketSearch = async (e) => {
e.preventDefault()
setTicketResult(null)
setTicketSearchErr('')
setTicketDeliverErr('')
setTicketSuccess(false)
setTicketSearching(true)
try {
const data = await searchTicketApi(ticketCode.trim())
setTicketResult(data)
} catch (err) {
if (!handleAuthError(err)) setTicketSearchErr(err.message)
} finally {
setTicketSearching(false)
}
}
const handleTicketDeliver = async () => {
if (!ticketResult?.claim?.id) return
setTicketDeliverErr('')
setTicketDelivering(true)
try {
await deliverClaimApi(ticketResult.claim.id)
setTicketResult((prev) => ({
...prev,
claim: { ...prev.claim, status: 'DELIVERED', claimedAt: new Date().toISOString() },
}))
setTicketSuccess(true)
setStats((prev) => prev ? {
...prev,
claimsPending: Math.max(0, (prev.claimsPending ?? 1) - 1),
claimsDelivered: (prev.claimsDelivered ?? 0) + 1,
} : prev)
} catch (err) {
if (!handleAuthError(err)) setTicketDeliverErr(err.message)
} finally {
setTicketDelivering(false)
}
}
if (loading) return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<p className="text-gray-500">Chargement...</p>
</div>
)
if (error) return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center px-4">
<p className="text-red-500 text-sm">{error}</p>
</div>
)
const winnerUser = winner?.winner?.user
return (
<div className="min-h-screen bg-gray-50 p-4 sm:p-6">
<div className="max-w-5xl mx-auto space-y-6">
<h1 className="text-xl sm:text-2xl font-bold text-gray-800">Tableau de bord Admin</h1>
{/* Fenêtre du jeu-concours */}
{stats?.gameWindow && (
<div className={`rounded-xl shadow p-4 sm:p-6 ${PHASE_PANELS[stats.gameWindow.phase] ?? 'bg-white'}`}>
<div className="flex items-center justify-between flex-wrap gap-2">
<div>
<p className="text-sm font-semibold text-gray-800">
{PHASE_TITLES[stats.gameWindow.phase] ?? 'Jeu-concours'}
</p>
{stats.gameWindow.configured ? (
<p className="text-xs text-gray-500 mt-1">
Début : {formatDate(stats.gameWindow.startDate)}
<br />
Fin des participations : {formatDate(stats.gameWindow.participationDeadline)}
<br />
Fin des retraits de lots : {formatDate(stats.gameWindow.claimDeadline)}
</p>
) : (
<p className="text-xs text-gray-600 mt-1">
Choisissez une date de début pour ouvrir le jeu-concours.
</p>
)}
</div>
<span className={`text-xs font-semibold px-2.5 py-1 rounded-full ${PHASE_BADGES[stats.gameWindow.phase]?.className ?? 'bg-gray-100 text-gray-700'}`}>
{PHASE_BADGES[stats.gameWindow.phase]?.label ?? 'État inconnu'}
</span>
</div>
<form onSubmit={handleSaveGameSettings} className="mt-4 pt-4 border-t border-gray-200 flex flex-wrap items-end gap-3">
<div className="flex-1 min-w-[220px]">
<label htmlFor="game-start" className="block text-xs font-medium text-gray-700 mb-1">
{stats.gameWindow.configured ? 'Corriger la date de début' : 'Date de début du jeu-concours'}
</label>
<input
type="datetime-local"
id="game-start"
value={startDateInput}
onChange={(e) => setStartDateInput(e.target.value)}
required
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 bg-white focus:outline-none focus:ring-1 focus:ring-[#006B6B] focus:border-[#006B6B]"
/>
</div>
<button
type="submit"
disabled={settingsSaving || !startDateInput}
className="bg-[#006B6B] hover:bg-[#005959] disabled:opacity-50 text-white font-semibold px-4 py-2 rounded-lg transition-colors text-xs sm:text-sm"
>
{settingsSaving ? 'Enregistrement…' : stats.gameWindow.configured ? 'Mettre à jour' : 'Lancer le jeu'}
</button>
</form>
<p className="text-xs text-gray-500 mt-2">
Les participations durent 30 jours, puis 30 jours supplémentaires sont laissés pour retirer les lots.
</p>
{settingsError && <p className="text-red-500 text-xs mt-2">{settingsError}</p>}
{settingsOk && <p className="text-green-600 text-xs mt-2">{settingsOk}</p>}
</div>
)}
{/* Stats principales */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4">
<StatCard label="Participants" value={stats?.users} />
<StatCard label="Participations" value={stats?.participations} />
<StatCard label="Gains en attente" value={stats?.claimsPending} />
<StatCard label="Gains remis" value={stats?.claimsDelivered} />
</div>
{/* Stats tickets */}
<div className="grid grid-cols-3 gap-3 sm:gap-4">
<StatCard label="Tickets total" value={stats?.ticketsTotal} />
<StatCard label="Tickets utilisés" value={stats?.ticketsUsed} />
<StatCard label="Tickets restants" value={stats?.ticketsRemaining} />
</div>
{/* Stats démographiques */}
{stats?.genderStats && stats?.ageStats && (
<div className="grid sm:grid-cols-2 gap-3 sm:gap-4">
<div className="bg-white rounded-xl shadow p-4 sm:p-6">
<h2 className="text-base sm:text-lg font-bold text-gray-800 mb-4">Genre des participants</h2>
<div className="space-y-2">
{[
{ label: 'Femmes', value: stats.genderStats.women },
{ label: 'Hommes', value: stats.genderStats.men },
{ label: 'Autre / non renseigné', value: stats.genderStats.other },
].map((row) => (
<div key={row.label} className="flex items-center justify-between text-sm">
<span className="text-gray-500">{row.label}</span>
<span className="font-semibold text-gray-800">{row.value}</span>
</div>
))}
</div>
</div>
<div className="bg-white rounded-xl shadow p-4 sm:p-6">
<h2 className="text-base sm:text-lg font-bold text-gray-800 mb-4">Âge des participants</h2>
<div className="space-y-2">
{['-18', '18-24', '25-34', '35-44', '45-54', '55-64', '65+', 'unknown'].map((bracket) => (
<div key={bracket} className="flex items-center justify-between text-sm">
<span className="text-gray-500">{bracket === 'unknown' ? 'Non renseigné' : `${bracket} ans`}</span>
<span className="font-semibold text-gray-800">{stats.ageStats[bracket] ?? 0}</span>
</div>
))}
</div>
</div>
</div>
)}
{/* Répartition par lot */}
{stats?.lotsStats?.length > 0 && (
<div className="bg-white rounded-xl shadow p-4 sm:p-6">
<h2 className="text-base sm:text-lg font-bold text-gray-800 mb-4">Répartition des lots</h2>
<div className="overflow-x-auto -mx-4 sm:mx-0">
<table className="w-full text-sm min-w-[480px]">
<thead>
<tr className="text-left text-gray-500 border-b text-xs sm:text-sm">
<th className="pb-2 px-4 sm:px-0">Lot</th>
<th className="pb-2 px-4 sm:px-0 text-center">%</th>
<th className="pb-2 px-4 sm:px-0 text-center">Tickets</th>
<th className="pb-2 px-4 sm:px-0 text-center">Participations</th>
</tr>
</thead>
<tbody>
{stats.lotsStats.map((lot) => (
<tr key={lot.id} className="border-b last:border-0">
<td className="py-2.5 px-4 sm:px-0 font-medium text-gray-800 text-xs sm:text-sm">{lot.name}</td>
<td className="py-2.5 px-4 sm:px-0 text-center text-gray-500 text-xs sm:text-sm">{lot.percentage}%</td>
<td className="py-2.5 px-4 sm:px-0 text-center text-gray-600 text-xs sm:text-sm">{lot._count.tickets.toLocaleString('fr-FR')}</td>
<td className="py-2.5 px-4 sm:px-0 text-center text-[#006B6B] font-semibold text-xs sm:text-sm">{lot._count.participations}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* ── Contacts emailing ── */}
<div className="bg-white rounded-xl shadow p-4 sm:p-6">
<div className="flex items-center justify-between mb-4 flex-wrap gap-2">
<h2 className="text-base sm:text-lg font-bold text-gray-800">Contacts emailing</h2>
<div className="flex gap-2">
<button
onClick={handleLoadEmailingContacts}
disabled={emailingLoading}
className="bg-[#006B6B] hover:bg-[#005959] disabled:opacity-50 text-white font-semibold px-4 py-2 rounded-lg transition-colors text-xs sm:text-sm"
>
{emailingLoading ? 'Chargement…' : emailingContacts ? 'Actualiser' : 'Charger les contacts'}
</button>
{emailingContacts?.length > 0 && (
<button
onClick={handleExportEmailingCsv}
className="border border-gray-200 text-gray-600 hover:bg-gray-50 font-semibold px-4 py-2 rounded-lg transition-colors text-xs sm:text-sm"
>
Exporter en CSV
</button>
)}
</div>
</div>
{emailingError && <p className="text-red-500 text-sm mb-3">{emailingError}</p>}
<p className="text-sm text-gray-500 mb-3">
Clients ayant accepté d'être contactés à des fins marketing.
{emailingContacts && <> — <span className="font-semibold text-gray-800">{emailingContacts.length}</span> contact(s)</>}
</p>
{emailingContacts && (
<div className="overflow-x-auto -mx-4 sm:mx-0">
<table className="w-full text-sm min-w-[400px]">
<thead>
<tr className="text-left text-gray-500 border-b text-xs sm:text-sm">
<th className="pb-2 px-4 sm:px-0">Nom</th>
<th className="pb-2 px-4 sm:px-0">Email</th>
<th className="pb-2 px-4 sm:px-0 text-right">Inscrit le</th>
</tr>
</thead>
<tbody>
{emailingContacts.length === 0 && (
<tr>
<td colSpan={3} className="py-6 text-center text-gray-500 text-sm">
Aucun contact n'a accepté l'emailing pour le moment
</td>
</tr>
)}
{emailingContacts.map((contact) => (
<tr key={contact.email} className="border-b last:border-0">
<td className="py-3 px-4 sm:px-0 font-medium text-gray-800 whitespace-nowrap text-xs sm:text-sm">
{contact.firstname} {contact.lastname}
</td>
<td className="py-3 px-4 sm:px-0 text-gray-500 text-xs">{contact.email}</td>
<td className="py-3 px-4 sm:px-0 text-right text-gray-500 text-xs">
{new Date(contact.createdAt).toLocaleDateString('fr-FR')}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* ── Remise de lot ── */}
<div className="bg-white rounded-xl shadow p-4 sm:p-6">
<h2 className="text-base sm:text-lg font-bold text-gray-800 mb-4">Remise de lot</h2>
<form onSubmit={handleTicketSearch} className="flex gap-2 mb-4">
<label htmlFor="admin-ticket-code" className="sr-only">Code ticket</label>
<input
type="text"
id="admin-ticket-code"
value={ticketCode}
onChange={(e) => setTicketCode(e.target.value.toUpperCase())}
placeholder="Code ticket (10 caractères)"
maxLength={10}
className="flex-1 min-w-0 border border-gray-200 rounded-lg px-4 py-2.5 text-sm font-mono uppercase tracking-widest focus:outline-none focus:ring-2 focus:ring-[#006B6B] focus:border-[#006B6B]"
/>
<button
type="submit"
disabled={ticketSearching || ticketCode.trim().length === 0}
className="bg-[#006B6B] hover:bg-[#005959] disabled:opacity-50 text-white font-semibold px-4 py-2.5 rounded-lg transition-colors text-sm whitespace-nowrap"
>
{ticketSearching ? 'Recherche…' : 'Rechercher'}
</button>
</form>
{ticketSearchErr && (
<p className="text-red-500 text-sm mb-3">{ticketSearchErr}</p>
)}
{ticketResult && (
<div className="border border-gray-100 rounded-xl overflow-hidden">
{ticketSuccess && (
<div className="bg-green-500 px-4 py-2.5 flex items-center gap-2">
<svg className="w-4 h-4 text-white flex-shrink-0" fill="none" stroke="currentColor" strokeWidth={2.5} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
<p className="text-white text-sm font-medium">Lot remis avec succès.</p>
</div>
)}
<div className="px-4 py-3 flex items-start justify-between gap-3 border-b border-gray-100">
<div>
<p className="font-semibold text-gray-900 text-sm">{ticketResult.lot.name}</p>
<p className="text-gray-500 text-xs mt-0.5">{ticketResult.lot.description}</p>
{ticketResult.lot.value && <p className="text-[#006B6B] text-xs font-semibold mt-1">{ticketResult.lot.value} €</p>}
</div>
<span className={`flex-shrink-0 text-xs font-semibold px-2.5 py-1 rounded-full ${
ticketResult.claim?.status === 'DELIVERED' ? 'bg-green-100 text-green-700'
: ticketResult.claim?.status === 'CANCELLED' ? 'bg-gray-100 text-gray-500'
: 'bg-yellow-100 text-yellow-700'
}`}>
{ticketResult.claim?.status === 'DELIVERED' ? 'Remis'
: ticketResult.claim?.status === 'CANCELLED' ? 'Expiré'
: 'À remettre'}
</span>
</div>
<div className="px-4 py-3 flex items-center gap-3 border-b border-gray-100">
<div className="w-8 h-8 rounded-full bg-[#006B6B] flex items-center justify-center text-white text-xs font-bold flex-shrink-0">
{(ticketResult.client.firstname?.[0] ?? '?').toUpperCase()}{(ticketResult.client.lastname?.[0] ?? '?').toUpperCase()}
</div>
<div>
<p className="text-sm font-medium text-gray-800">{ticketResult.client.firstname} {ticketResult.client.lastname}</p>
<p className="text-xs text-gray-500">{ticketResult.client.email}</p>
</div>
</div>
<div className="px-4 py-3 space-y-2">
{ticketDeliverErr && <p className="text-red-500 text-xs">{ticketDeliverErr}</p>}
{ticketResult.claim?.status === 'PENDING' && !ticketSuccess && (
<button
onClick={handleTicketDeliver}
disabled={ticketDelivering}
className="w-full bg-[#006B6B] hover:bg-[#005959] disabled:opacity-50 text-white font-semibold py-2.5 rounded-lg transition-colors text-sm"
>
{ticketDelivering ? 'Confirmation…' : 'Confirmer la remise du lot'}
</button>
)}
<button
onClick={() => { setTicketCode(''); setTicketResult(null); setTicketSuccess(false); setTicketSearchErr(''); setTicketDeliverErr('') }}
className="w-full border border-gray-200 text-gray-500 hover:bg-gray-50 py-2 rounded-lg transition-colors text-sm"
>
Rechercher un autre code
</button>
</div>
</div>
)}
</div>
{/* ── Tirage au sort ── */}
<div className="bg-white rounded-xl shadow p-4 sm:p-6">
<h2 className="text-base sm:text-lg font-bold text-gray-800 mb-4">Grand tirage au sort</h2>
{winnerUser ? (
/* Gagnant déjà tiré */
<div className="space-y-4">
<div className="flex items-center gap-4 p-4 bg-[#006B6B]/5 rounded-xl border border-[#006B6B]/20">
<div className="w-14 h-14 rounded-full bg-[#006B6B] flex items-center justify-center text-white text-lg font-bold flex-shrink-0">
{(winnerUser.firstname?.[0] ?? '?').toUpperCase()}{(winnerUser.lastname?.[0] ?? '?').toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<p className="text-xs text-[#006B6B] font-semibold uppercase tracking-wider mb-0.5">Gagnant du grand tirage</p>
<p className="font-bold text-gray-900 text-base">{winnerUser.firstname} {winnerUser.lastname}</p>
<p className="text-gray-500 text-sm truncate">{winnerUser.email}</p>
</div>
<div className="flex-shrink-0">
<svg className="w-8 h-8 text-yellow-400" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l2.4 7.4H22l-6.2 4.5 2.4 7.4L12 17l-6.2 4.3 2.4-7.4L2 9.4h7.6z" />
</svg>
</div>
</div>
{winner?.winner?.createdAt && (
<p className="text-xs text-gray-500 text-right">
Tirage effectué le {formatDate(winner.winner.createdAt)}
</p>
)}
{/* Signalé uniquement au retour du tirage : le GET du gagnant ne
renvoie pas emailSent, et l'absence de la clé ne vaut pas échec. */}
{winner?.emailSent === false && (
<p className="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2">
L&apos;email de notification n&apos;a pas pu être envoyé au gagnant. Merci de le contacter directement à l&apos;adresse ci-dessus.
</p>
)}
</div>
) : (
/* Pas encore de tirage */
<div className="space-y-3">
<div className="p-4 bg-gray-50 rounded-xl border border-gray-200">
<p className="text-sm text-gray-600 mb-1">
<span className="font-semibold text-gray-800">{stats?.participations ?? 0}</span> participations enregistrées
</p>
<p className="text-sm text-gray-600">
<span className="font-semibold text-gray-800">{stats?.users ?? 0}</span> participants éligibles
</p>
</div>
{drawError && (
<p className="text-red-500 text-sm px-1">{drawError}</p>
)}
<button
onClick={() => setShowConfirm(true)}
disabled={drawing || (stats?.users ?? 0) === 0}
className="flex items-center gap-2 bg-[#006B6B] hover:bg-[#005959] disabled:opacity-50 text-white font-semibold px-5 py-3 rounded-xl transition-colors text-sm"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l2.4 7.4H22l-6.2 4.5 2.4 7.4L12 17l-6.2 4.3 2.4-7.4L2 9.4h7.6z" />
</svg>
{drawing ? 'Tirage en cours…' : 'Lancer le tirage au sort'}
</button>
</div>
)}
</div>
{/* Liste participants */}
<div className="bg-white rounded-xl shadow p-4 sm:p-6">
<h2 className="text-base sm:text-lg font-bold text-gray-800 mb-4">
Participants <span className="text-gray-500 font-normal text-sm">({users.length})</span>
</h2>
<div className="overflow-x-auto -mx-4 sm:mx-0">
<table className="w-full text-sm min-w-[400px]">
<thead>
<tr className="text-left text-gray-500 border-b text-xs sm:text-sm">
<th className="pb-2 px-4 sm:px-0">Nom</th>
<th className="pb-2 px-4 sm:px-0">Email</th>
<th className="pb-2 px-4 sm:px-0 text-right">Participations</th>
</tr>
</thead>
<tbody>
{users.length === 0 && (
<tr>
<td colSpan={3} className="py-6 text-center text-gray-500 text-sm">
Aucun participant pour le moment
</td>
</tr>
)}
{users.map((user) => (
<tr key={user.id} className="border-b last:border-0">
<td className="py-3 px-4 sm:px-0 font-medium text-gray-800 whitespace-nowrap text-xs sm:text-sm">
{user.firstname} {user.lastname}
</td>
<td className="py-3 px-4 sm:px-0 text-gray-500 text-xs">{user.email}</td>
<td className="py-3 px-4 sm:px-0 text-right text-[#006B6B] font-semibold text-xs sm:text-sm">
{user._count.participations}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
{/* ── Modal de confirmation du tirage ── */}
{showConfirm && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 px-4">
<div className="bg-white rounded-2xl w-full max-w-sm shadow-xl p-6">
<div className="flex justify-center mb-4">
<div className="w-14 h-14 rounded-full bg-yellow-100 flex items-center justify-center">
<svg className="w-7 h-7 text-yellow-500" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l2.4 7.4H22l-6.2 4.5 2.4 7.4L12 17l-6.2 4.3 2.4-7.4L2 9.4h7.6z" />
</svg>
</div>
</div>
<h3 className="text-lg font-bold text-gray-900 text-center mb-1">Lancer le tirage au sort ?</h3>
<p className="text-sm text-gray-500 text-center mb-4">
Un gagnant sera sélectionné aléatoirement parmi les <strong>{stats?.users}</strong> participants.
</p>
<div className="bg-red-50 border border-red-200 rounded-xl px-4 py-3 mb-5 flex gap-2 items-start">
<svg className="w-4 h-4 text-red-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
<p className="text-xs text-red-600">Cette action est <strong>irréversible</strong>. Le tirage ne peut être effectué qu'une seule fois.</p>
</div>
<div className="flex gap-3">
<button
onClick={() => setShowConfirm(false)}
className="flex-1 border border-gray-200 text-gray-600 hover:bg-gray-50 font-medium py-2.5 rounded-xl transition-colors text-sm"
>
Annuler
</button>
<button
onClick={handleDraw}
className="flex-1 bg-[#006B6B] hover:bg-[#005959] text-white font-semibold py-2.5 rounded-xl transition-colors text-sm"
>
Confirmer le tirage
</button>
</div>
</div>
</div>
)}
</div>
)
}