changement
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user