changement
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { getStatsApi, getUsersApi, drawWinnerApi, getWinnerApi } from '../services/admin'
|
||||
import { getStatsApi, getUsersApi, drawWinnerApi, getWinnerApi, getEmailingContactsApi } from '../services/admin'
|
||||
import { searchTicketApi, deliverClaimApi } from '../services/employee'
|
||||
import { useHandleAuthError } from '../hooks/useHandleAuthError'
|
||||
|
||||
@@ -40,6 +40,11 @@ export default function Admin() {
|
||||
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]) => {
|
||||
@@ -67,6 +72,37 @@ export default function Admin() {
|
||||
}
|
||||
}
|
||||
|
||||
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([`${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)
|
||||
@@ -127,6 +163,25 @@ 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'}`}>
|
||||
<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)}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats principales */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4">
|
||||
<StatCard label="Participants" value={stats?.users} />
|
||||
@@ -142,6 +197,39 @@ export default function Admin() {
|
||||
<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">
|
||||
@@ -171,13 +259,80 @@ export default function Admin() {
|
||||
</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-400 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)"
|
||||
|
||||
Reference in New Issue
Block a user