Files
THE-TIP-TOP/frontend/src/pages/Admin.jsx
Andil 13584d2b0f
All checks were successful
Tests / Backend - Tests skipped: 8, passed: 74
Backend Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 78.38% (551/703) * Lines of Code: 703 #### Quality Gates Summary No active quality gates.
Tests / Frontend - Lint, Tests et Build passed: 83
Frontend Coverage #### Project Overview No changes detected, that affect the code coverage. * Line Coverage: 84.94% (547/644) * Lines of Code: 644 #### Quality Gates Summary No active quality gates.
THE-TIP-TOP-MULTIBRANCH/pipeline/head This commit looks good
fix: improve admin game date form on mobile
2026-09-19 23:02:18 +00:00

639 lines
30 KiB
JavaScript

import { useState, useEffect } from 'react'
import { getStatsApi, getUsersApi, drawWinnerApi, getWinnerApi, getEmailingContactsApi, setGameSettingsApi, createEmployeeApi } from '../services/admin'
import EmployeeDeliveries from '../components/admin/EmployeeDeliveries'
import { useHandleAuthError } from '../hooks/useHandleAuthError'
import { formatDay, formatRange, gameDatesFromStart } from '../utils/gameDates'
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é',
}
// Valeur attendue par <input type="datetime-local"> : heure locale, sans fuseau.
function toDateTimeLocalValue(dateStr) {
const date = new Date(dateStr)
const pad = (n) => String(n).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`
}
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('')
// Contacts emailing
const [emailingContacts, setEmailingContacts] = useState(null)
const [emailingLoading, setEmailingLoading] = useState(false)
const [emailingError, setEmailingError] = useState('')
// Création d'un compte employé
const [employeeForm, setEmployeeForm] = useState({ firstname: '', lastname: '', email: '' })
const [employeeSaving, setEmployeeSaving] = useState(false)
const [employeeError, setEmployeeError] = useState('')
const [employeeOk, setEmployeeOk] = useState('')
// Incrémenté à chaque création pour recharger la liste des employés.
const [employeesVersion, setEmployeesVersion] = useState(0)
useEffect(() => {
Promise.all([getStatsApi(), getUsersApi(), getWinnerApi()])
.then(([s, u, w]) => {
setStats(s)
// Le champ part de la date enregistrée, celle affichée sur l'accueil.
if (s.gameWindow?.startDate) setStartDateInput(toDateTimeLocalValue(s.gameWindow.startDate))
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} La page d'accueil affiche désormais ces dates.`)
setStartDateInput(toDateTimeLocalValue(data.gameWindow.startDate))
} 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')
const blob = new Blob(['\uFEFF' + 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 handleCreateEmployee = async (e) => {
e.preventDefault()
setEmployeeError('')
setEmployeeOk('')
setEmployeeSaving(true)
try {
const data = await createEmployeeApi(employeeForm)
setEmployeeOk(`Compte créé pour ${data.employee.firstname} ${data.employee.lastname} : un email avec un mot de passe provisoire a été envoyé à ${data.employee.email}.`)
setEmployeeForm({ firstname: '', lastname: '', email: '' })
setEmployeesVersion((v) => v + 1)
} catch (err) {
if (!handleAuthError(err)) setEmployeeError(err.message)
} finally {
setEmployeeSaving(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
// Aperçu en direct des dates publiques pour la date saisie.
const startInputDate = startDateInput ? new Date(startDateInput) : null
const startPreview = startInputDate && !Number.isNaN(startInputDate.getTime())
? gameDatesFromStart(startInputDate)
: null
const savedStart = stats?.gameWindow?.startDate
const startPreviewUnsaved = Boolean(startPreview) &&
(!savedStart || toDateTimeLocalValue(savedStart) !== startDateInput)
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-col sm:flex-row sm:items-end gap-3"
>
<div className="w-full sm:flex-1 sm: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="w-full sm:w-auto 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 whitespace-nowrap"
>
{settingsSaving ? 'Enregistrement…' : stats.gameWindow.configured ? 'Mettre à jour' : 'Lancer le jeu'}
</button>
</form>
{startPreview && (
<p className="text-xs text-gray-700 mt-3" data-testid="homepage-dates-preview">
Affiché sur la page d'accueil :{' '}
<span className="font-semibold">
Du {formatRange(startPreview.start, startPreview.lastPlayDay, { padDay: true })}
</span>
, retrait des lots jusqu'au <span className="font-semibold">{formatDay(startPreview.lastClaimDay)}</span>
{startPreviewUnsaved && <span className="text-amber-700"> (non enregistré)</span>}
</p>
)}
<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>
{/* ── Création d'un compte employé ── */}
<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-1">Créer un compte employé</h2>
<p className="text-xs sm:text-sm text-gray-500 mb-4">
Donne accès à l'espace employé (remise des lots en boutique). L'admin ne gère pas les remises lui-même.
L'employé reçoit par email un mot de passe provisoire, qu'il devra changer à sa première connexion.
</p>
<form onSubmit={handleCreateEmployee} className="grid sm:grid-cols-2 gap-3">
<div>
<label htmlFor="employee-firstname" className="block text-xs font-medium text-gray-700 mb-1">Prénom</label>
<input
type="text"
id="employee-firstname"
value={employeeForm.firstname}
onChange={(e) => setEmployeeForm({ ...employeeForm, firstname: e.target.value })}
required
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 focus:outline-none focus:ring-1 focus:ring-[#006B6B] focus:border-[#006B6B]"
/>
</div>
<div>
<label htmlFor="employee-lastname" className="block text-xs font-medium text-gray-700 mb-1">Nom</label>
<input
type="text"
id="employee-lastname"
value={employeeForm.lastname}
onChange={(e) => setEmployeeForm({ ...employeeForm, lastname: e.target.value })}
required
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 focus:outline-none focus:ring-1 focus:ring-[#006B6B] focus:border-[#006B6B]"
/>
</div>
<div>
<label htmlFor="employee-email" className="block text-xs font-medium text-gray-700 mb-1">Email</label>
<input
type="email"
id="employee-email"
value={employeeForm.email}
onChange={(e) => setEmployeeForm({ ...employeeForm, email: e.target.value })}
required
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-700 focus:outline-none focus:ring-1 focus:ring-[#006B6B] focus:border-[#006B6B]"
/>
</div>
<div className="sm:col-span-2 flex items-center gap-3 pt-1">
<button
type="submit"
disabled={employeeSaving}
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"
>
{employeeSaving ? 'Création…' : 'Créer le compte'}
</button>
{employeeError && <p className="text-red-500 text-xs sm:text-sm">{employeeError}</p>}
{employeeOk && <p className="text-green-600 text-xs sm:text-sm">{employeeOk}</p>}
</div>
</form>
</div>
{/* ── Employés et remises des lots ── */}
<EmployeeDeliveries refreshKey={employeesVersion} onAuthError={handleAuthError} />
{/* ── 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>
)
}