Files
THE-TIP-TOP/frontend/src/components/admin/EmployeeDeliveries.jsx
Emma781227 2201446ee6 changement
2026-09-14 22:30:40 +02:00

289 lines
13 KiB
JavaScript

import { useEffect, useRef, useState } from 'react'
import { getEmployeesApi, getEmployeeDeliveriesApi, getDeliveryByTicketCodeApi } from '../../services/admin'
const STATUS_BADGES = {
DELIVERED: { label: 'Lot remis', className: 'bg-green-100 text-green-700' },
PENDING: { label: 'En attente de retrait', className: 'bg-amber-100 text-amber-700' },
CANCELLED: { label: 'Expiré', className: 'bg-gray-200 text-gray-700' },
NOT_PLAYED: { label: 'Pas encore joué', className: 'bg-gray-100 text-gray-600' },
}
const INPUT_CLASS =
'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]'
const BUTTON_CLASS =
'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'
function formatDateTime(dateStr) {
return new Date(dateStr).toLocaleString('fr-FR', {
day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit',
})
}
const fullName = (person) => `${person.firstname} ${person.lastname}`
// Traçabilité des remises de lots : liste des employés, lots remis par chacun,
// et recherche inverse par numéro de ticket.
// `refreshKey` change quand un employé est créé, pour recharger la liste.
export default function EmployeeDeliveries({ refreshKey, onAuthError }) {
const [employees, setEmployees] = useState(null)
const [employeesError, setEmployeesError] = useState('')
const [filter, setFilter] = useState('')
const [selected, setSelected] = useState(null)
const [deliveries, setDeliveries] = useState(null)
const [deliveriesLoading, setDeliveriesLoading] = useState(false)
const [deliveriesError, setDeliveriesError] = useState('')
const detailRef = useRef(null)
const [ticketCode, setTicketCode] = useState('')
const [ticketResult, setTicketResult] = useState(null)
const [ticketLoading, setTicketLoading] = useState(false)
const [ticketError, setTicketError] = useState('')
useEffect(() => {
getEmployeesApi()
.then((data) => {
setEmployees(data.employees)
setEmployeesError('')
})
.catch((err) => {
if (!onAuthError(err)) setEmployeesError(err.message)
})
}, [refreshKey, onAuthError])
const openEmployee = async (employee) => {
setSelected(employee)
setDeliveries(null)
setDeliveriesError('')
setDeliveriesLoading(true)
try {
const data = await getEmployeeDeliveriesApi(employee.id)
setDeliveries(data.deliveries)
} catch (err) {
if (!onAuthError(err)) setDeliveriesError(err.message)
} finally {
setDeliveriesLoading(false)
detailRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}
}
const handleTicketSearch = async (e) => {
e.preventDefault()
setTicketError('')
setTicketResult(null)
setTicketLoading(true)
try {
setTicketResult(await getDeliveryByTicketCodeApi(ticketCode))
} catch (err) {
if (!onAuthError(err)) setTicketError(err.message)
} finally {
setTicketLoading(false)
}
}
const needle = filter.trim().toLowerCase()
const visibleEmployees = (employees ?? []).filter((employee) =>
!needle || `${fullName(employee)} ${employee.email}`.toLowerCase().includes(needle)
)
return (
<div className="bg-white rounded-xl shadow p-4 sm:p-6 space-y-6">
<div>
<h2 className="text-base sm:text-lg font-bold text-gray-800 mb-1">Employés et remises des lots</h2>
<p className="text-xs sm:text-sm text-gray-500">
Retrouvez quel employé a remis un lot et à quelle date, par numéro de ticket ou par employé.
</p>
</div>
{/* ── Recherche par numéro de ticket ── */}
<div>
<form onSubmit={handleTicketSearch} className="flex flex-wrap items-end gap-3">
<div className="flex-1 min-w-[200px]">
<label htmlFor="delivery-ticket-code" className="block text-xs font-medium text-gray-700 mb-1">
Numéro du ticket
</label>
<input
type="text"
id="delivery-ticket-code"
value={ticketCode}
onChange={(e) => setTicketCode(e.target.value)}
required
placeholder="Ex. A1B2C3D4E5"
className={`${INPUT_CLASS} uppercase placeholder:normal-case`}
/>
</div>
<button type="submit" disabled={ticketLoading || !ticketCode.trim()} className={BUTTON_CLASS}>
{ticketLoading ? 'Recherche…' : 'Rechercher'}
</button>
</form>
{ticketError && <p className="text-red-500 text-xs sm:text-sm mt-3">{ticketError}</p>}
{ticketResult && (
<div className="mt-4 p-4 rounded-lg border border-gray-200 bg-gray-50 text-sm space-y-1.5" data-testid="ticket-result">
<div className="flex items-center justify-between flex-wrap gap-2">
<p className="font-mono font-semibold text-gray-800">{ticketResult.ticketCode}</p>
<span className={`text-xs font-semibold px-2.5 py-1 rounded-full ${STATUS_BADGES[ticketResult.status]?.className}`}>
{STATUS_BADGES[ticketResult.status]?.label ?? ticketResult.status}
</span>
</div>
<p className="text-gray-600">Lot : <span className="font-medium text-gray-800">{ticketResult.lot.name}</span></p>
{ticketResult.client && (
<p className="text-gray-600">
Gagné par : <span className="font-medium text-gray-800">{fullName(ticketResult.client)}</span>{' '}
<span className="text-gray-500">({ticketResult.client.email})</span>
{ticketResult.playedAt && <> le {formatDateTime(ticketResult.playedAt)}</>}
</p>
)}
{ticketResult.deliveredBy ? (
<p className="text-gray-600">
Remis par :{' '}
<button
type="button"
onClick={() => openEmployee(ticketResult.deliveredBy)}
className="font-semibold text-[#006B6B] hover:underline"
>
{fullName(ticketResult.deliveredBy)}
</button>{' '}
le <span className="font-medium text-gray-800">{formatDateTime(ticketResult.deliveredAt)}</span>
</p>
) : (
ticketResult.status !== 'NOT_PLAYED' && (
<p className="text-gray-500">Ce lot n'a pas encore été remis.</p>
)
)}
</div>
)}
</div>
{/* ── Liste des employés ── */}
<div className="pt-6 border-t border-gray-100">
<div className="flex items-end justify-between flex-wrap gap-3 mb-3">
<h3 className="text-sm font-semibold text-gray-800">
Comptes employés
{employees && <span className="text-gray-500 font-normal"> - {employees.length}</span>}
</h3>
<div className="w-full sm:w-64">
<label htmlFor="employee-filter" className="sr-only">Rechercher un employé</label>
<input
type="search"
id="employee-filter"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Rechercher un employé…"
className={INPUT_CLASS}
/>
</div>
</div>
{employeesError && <p className="text-red-500 text-sm">{employeesError}</p>}
{!employees && !employeesError && <p className="text-gray-500 text-sm">Chargement…</p>}
{employees && (
<div className="overflow-x-auto -mx-4 sm:mx-0">
<table className="w-full text-sm min-w-[520px]">
<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">Créé le</th>
<th className="pb-2 px-4 sm:px-0 text-center">Lots remis</th>
</tr>
</thead>
<tbody>
{visibleEmployees.length === 0 && (
<tr>
<td colSpan={4} className="py-6 text-center text-gray-500 text-sm">
{employees.length === 0 ? "Aucun compte employé pour l'instant" : 'Aucun employé ne correspond à la recherche'}
</td>
</tr>
)}
{visibleEmployees.map((employee) => (
<tr key={employee.id} className={`border-b last:border-0 ${selected?.id === employee.id ? 'bg-[#006B6B]/5' : ''}`}>
<td className="py-3 px-4 sm:px-0 text-xs sm:text-sm">
<button
type="button"
onClick={() => openEmployee(employee)}
className="font-semibold text-[#006B6B] hover:underline text-left"
>
{fullName(employee)}
</button>
{employee.mustChangePassword && (
<span className="block text-[11px] text-amber-700">Première connexion en attente</span>
)}
</td>
<td className="py-3 px-4 sm:px-0 text-gray-500 text-xs">{employee.email}</td>
<td className="py-3 px-4 sm:px-0 text-gray-500 text-xs">
{new Date(employee.createdAt).toLocaleDateString('fr-FR')}
</td>
<td className="py-3 px-4 sm:px-0 text-center font-semibold text-gray-800">{employee.deliveredCount}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* ── Lots remis par l'employé sélectionné ── */}
{selected && (
<div ref={detailRef} className="pt-6 border-t border-gray-100">
<div className="flex items-center justify-between flex-wrap gap-2 mb-3">
<h3 className="text-sm font-semibold text-gray-800">
Lots remis par {fullName(selected)}
{deliveries && <span className="text-gray-500 font-normal"> — {deliveries.length}</span>}
</h3>
<button
type="button"
onClick={() => setSelected(null)}
className="border border-gray-200 text-gray-600 hover:bg-gray-50 font-semibold px-3 py-1.5 rounded-lg transition-colors text-xs"
>
Fermer
</button>
</div>
{deliveriesLoading && <p className="text-gray-500 text-sm">Chargement…</p>}
{deliveriesError && <p className="text-red-500 text-sm">{deliveriesError}</p>}
{deliveries && (
<div className="overflow-x-auto -mx-4 sm:mx-0">
<table className="w-full text-sm min-w-[560px]">
<thead>
<tr className="text-left text-gray-500 border-b text-xs sm:text-sm">
<th className="pb-2 px-4 sm:px-0">Ticket</th>
<th className="pb-2 px-4 sm:px-0">Lot</th>
<th className="pb-2 px-4 sm:px-0">Client</th>
<th className="pb-2 px-4 sm:px-0 text-right">Remis le</th>
</tr>
</thead>
<tbody>
{deliveries.length === 0 && (
<tr>
<td colSpan={4} className="py-6 text-center text-gray-500 text-sm">
Cet employé n'a encore remis aucun lot
</td>
</tr>
)}
{deliveries.map((delivery) => (
<tr key={delivery.claimId} className="border-b last:border-0">
<td className="py-3 px-4 sm:px-0 font-mono text-xs text-gray-800">{delivery.ticketCode}</td>
<td className="py-3 px-4 sm:px-0 text-xs sm:text-sm text-gray-800">{delivery.lot.name}</td>
<td className="py-3 px-4 sm:px-0 text-xs text-gray-500">
{fullName(delivery.client)}
<span className="block">{delivery.client.email}</span>
</td>
<td className="py-3 px-4 sm:px-0 text-right text-xs text-gray-500 whitespace-nowrap">
{formatDateTime(delivery.deliveredAt)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
</div>
)
}