Inscription en deux temps, emails de confirmation et export Excel des bénévoles
ci-cd / build-backend (push) Successful in 30s
ci-cd / build-frontend (push) Successful in 18s
ci-cd / deploy (push) Successful in 12s

Formulaire public — paiement en ligne :
- Le bénévole n'est plus compté en base tant qu'un paiement en ligne choisi
  n'est pas confirmé (PendingFormSubmission, promu en Volunteer à la
  confirmation, webhook ou vérification en direct).
- Type de paiement « mécénat » : étape d'adresse postale avant le paiement,
  conservée sur le bénévole pour un reçu fiscal (cerfa) ultérieur.
- Page de confirmation au retour de SumUp, avec relance possible après
  échec/expiration sans ressaisir le formulaire.
- « Je paierai sur place » remplace « je ne participe pas » ; tous les
  champs activés par la coordination sont désormais obligatoires.
- La fiche bénévole n'affiche plus l'état des cases à cocher, seulement le
  résultat réel du paiement (libellé, montant, statut).

Emails :
- Email de confirmation d'inscription à la création du bénévole (tient lieu
  de reçu de paiement quand une cotisation est payée en ligne).
- Mise en forme HTML au thème de l'application (logo de l'édition en
  en-tête) pour tous les emails envoyés, texte brut conservé en repli.

Export Excel des bénévoles (COORDINATION) : coordonnées, vœux résolus en
noms lisibles, cotisation et mécénat.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-09-30 07:41:58 +02:00
co-authored by Claude Sonnet 5
parent 68d42ee303
commit a2b748566b
34 changed files with 2318 additions and 291 deletions
+34 -10
View File
@@ -12,6 +12,7 @@ import {
useDeleteVolunteer,
useEdition,
useExecuteImport,
useExportVolunteers,
usePositions,
useReplaceAvailability,
useUpdateAccount,
@@ -187,7 +188,7 @@ function PublicFormAnswers({
volunteer.has_photo ||
Boolean(volunteer.companion_wish_note) ||
volunteer.self_declared_available ||
volunteer.willing_to_pay_fee;
volunteer.fee_payment_status !== null;
if (!hasAnything) return null;
const feeStatusKey = volunteer.fee_payment_status
@@ -230,15 +231,6 @@ function PublicFormAnswers({
/>
<span className="font-mono text-[11px]">{t('form.field.available')}</span>
</label>
<label className="flex cursor-pointer items-center gap-[7px]">
<input
type="checkbox"
checked={volunteer.willing_to_pay_fee}
onChange={(e) => patch({ willing_to_pay_fee: e.target.checked })}
className="h-[15px] w-[15px] accent-ink"
/>
<span className="font-mono text-[11px]">{t('form.field.willingToPayFee')}</span>
</label>
{feeStatusKey ? (
<span className="border-2 border-ink bg-cream px-[8px] py-[3px] font-mono text-[10.5px] font-bold">
{volunteer.fee_payment_label ? `${volunteer.fee_payment_label} (${volunteer.fee_amount} €) — ` : ''}
@@ -246,6 +238,13 @@ function PublicFormAnswers({
</span>
) : null}
</div>
{volunteer.mecenat_address ? (
<p className="font-mono text-[10.5px] leading-relaxed opacity-70">
{t('config.vol.mecenatAddress')} : {volunteer.mecenat_address}, {volunteer.mecenat_postal_code}{' '}
{volunteer.mecenat_city}, {volunteer.mecenat_country}
</p>
) : null}
</div>
);
}
@@ -489,6 +488,7 @@ export function ConfigVolunteers() {
const positions = usePositions();
const updateVolunteer = useUpdateVolunteer();
const deleteVolunteer = useDeleteVolunteer();
const exportVolunteers = useExportVolunteers();
const [mode, setMode] = useState<Mode>('list');
const [selectedId, setSelectedId] = useState<string | null>(null);
@@ -522,6 +522,19 @@ export function ConfigVolunteers() {
const rows = filtered.slice(0, 60);
const patch = (body: Parameters<typeof updateVolunteer.mutate>[0]['body']) =>
selected && updateVolunteer.mutate({ id: selected.id, body });
const handleExport = async () => {
try {
const blob = await exportVolunteers.mutateAsync();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'benevoles.xlsx';
a.click();
URL.revokeObjectURL(url);
} catch {
// Erreur exposée via exportVolunteers.isError / .error, affichée plus bas.
}
};
if (mode === 'add') {
return <AddVolunteerForm onClose={() => setMode('list')} />;
@@ -556,7 +569,18 @@ export function ConfigVolunteers() {
>
↓ {t('config.vol.importButton2')}
</button>
<button
type="button"
onClick={handleExport}
disabled={exportVolunteers.isPending}
className="cursor-pointer border-[3px] border-ink bg-cream px-[14px] py-3 font-display text-[12px] font-black uppercase tracking-wide shadow-brutal hover:bg-yellow disabled:cursor-not-allowed disabled:opacity-50"
>
↑ {exportVolunteers.isPending ? t('config.vol.exporting') : t('config.vol.exportButton')}
</button>
</div>
{exportVolunteers.isError ? (
<ErrorNote error={exportVolunteers.error} />
) : null}
</div>
<div className="flex-none border-[3px] border-ink bg-paper px-[14px] py-3 shadow-brutal-lg">
<div className="font-mono text-[24px] font-bold">{total}</div>