changement

This commit is contained in:
Emma781227
2026-09-14 22:30:40 +02:00
parent 8a6355cde1
commit 2201446ee6
54 changed files with 2495 additions and 139 deletions

View File

@@ -1,6 +1,8 @@
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 (
@@ -38,6 +40,13 @@ const PHASE_TITLES = {
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',
@@ -68,15 +77,19 @@ export default function Admin() {
const [emailingError, setEmailingError] = useState('')
// Création d'un compte employé
const [employeeForm, setEmployeeForm] = useState({ firstname: '', lastname: '', email: '', password: '' })
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)
})
@@ -96,8 +109,8 @@ export default function Admin() {
// 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('')
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 {
@@ -157,8 +170,9 @@ export default function Admin() {
setEmployeeSaving(true)
try {
const data = await createEmployeeApi(employeeForm)
setEmployeeOk(`Compte employé créé pour ${data.employee.firstname} ${data.employee.lastname} (${data.employee.email}).`)
setEmployeeForm({ firstname: '', lastname: '', email: '', password: '' })
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 {
@@ -180,6 +194,15 @@ export default function Admin() {
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">
@@ -236,6 +259,17 @@ export default function Admin() {
</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>
@@ -392,6 +426,7 @@ export default function Admin() {
<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">
@@ -428,19 +463,6 @@ export default function Admin() {
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-password" className="block text-xs font-medium text-gray-700 mb-1">Mot de passe</label>
<input
type="password"
id="employee-password"
value={employeeForm.password}
onChange={(e) => setEmployeeForm({ ...employeeForm, password: e.target.value })}
required
minLength={8}
placeholder="8 caractères minimum"
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
@@ -456,6 +478,9 @@ export default function Admin() {
</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>