ajout des champs date de naissance et gender sur l'inscription, modification de la permition de lancer le jeu concour par l'administrateur, envoie des mail a chaque lot remporter
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { getStatsApi, getUsersApi, drawWinnerApi, getWinnerApi, getEmailingContactsApi } from '../services/admin'
|
||||
import { getStatsApi, getUsersApi, drawWinnerApi, getWinnerApi, getEmailingContactsApi, setGameSettingsApi } from '../services/admin'
|
||||
import { searchTicketApi, deliverClaimApi } from '../services/employee'
|
||||
import { useHandleAuthError } from '../hooks/useHandleAuthError'
|
||||
|
||||
@@ -13,6 +13,32 @@ function StatCard({ label, value, sub }) {
|
||||
)
|
||||
}
|
||||
|
||||
// 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',
|
||||
@@ -31,6 +57,12 @@ export default function Admin() {
|
||||
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)
|
||||
@@ -58,6 +90,25 @@ export default function Admin() {
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
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)
|
||||
@@ -164,21 +215,61 @@ export default function Admin() {
|
||||
<h1 className="text-xl sm:text-2xl font-bold text-gray-800">Tableau de bord Admin</h1>
|
||||
|
||||
{/* Fenêtre du jeu-concours */}
|
||||
{stats?.gameWindow?.configured && (
|
||||
<div className={`rounded-xl shadow p-4 sm:p-6 ${stats.gameWindow.isOpen ? 'bg-white' : 'bg-red-50 border border-red-200'}`}>
|
||||
{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">
|
||||
Jeu-concours {stats.gameWindow.isOpen ? 'en cours' : 'terminé'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Début : {formatDate(stats.gameWindow.startDate)} — Fin des participations et réclamations : {formatDate(stats.gameWindow.claimDeadline)}
|
||||
{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 ${stats.gameWindow.isOpen ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
|
||||
{stats.gameWindow.isOpen ? 'Ouvert' : 'Fermé'}
|
||||
<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-400 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>
|
||||
)}
|
||||
|
||||
@@ -440,6 +531,13 @@ export default function Admin() {
|
||||
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'email de notification n'a pas pu être envoyé au gagnant. Merci de le contacter directement à l'adresse ci-dessus.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
/* Pas encore de tirage */
|
||||
|
||||
Reference in New Issue
Block a user