Étape 21 : formulaire public d'inscription des bénévoles
Retire « constructeur de formulaire » des exclusions du CLAUDE.md (§ 5) — décision explicite prise en conversation. Un formulaire par édition (COORDINATION choisit les champs actifs parmi neuf : email, téléphone, vœux de poste — max 2, vœux d'artiste — un par soir de festival, vœu de compagnon en texte libre, disponibilité/ cotisation déclaratives, photo, commentaire ; nom/prénom toujours collectés) ouvert/fermé à volonté, avec un lien à partager (/inscription, aucun compte requis). Backend : FormTemplate/FormField (migration 0013), quatre nouveaux champs sur Volunteer (photo, companion_wish_note, self_declared_available, willing_to_pay_fee), app/api/forms.py — les deux seuls endpoints publics de toute l'API (GET .../form/public, POST .../form/public/submit), protégés par hCaptcha (app/forms/captcha.py, désactivé sans HCAPTCHA_SECRET_KEY) et un filet de sécurité global par édition (form_submission_limiter). resolve_owned_ids/set_artists/ set_wished_positions déplacés vers app/api/_common.py, partagés avec volunteers.py. Frontend : écran de configuration (/inscriptions) avec lien copiable et bascule ouvert/fermé, formulaire public (/inscription, hors RequireAuth) avec widget hCaptcha chargé dynamiquement, capture photo native (<input capture>), vœux de poste/artiste avec les mêmes règles que le back-end. Réponses du formulaire visibles et éditables depuis la fiche bénévole existante (ConfigVolunteers.tsx). Vérifié : pytest (273 passés), ruff, alembic check, tsc --noEmit, eslint, vite build, smoke (rendu SSR de chaque écran + flux de bout en bout du formulaire en mode démonstration) — tous verts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
55c5d539a8
commit
82088f3b0c
@@ -18,6 +18,7 @@ import {
|
||||
useUpdatePosition,
|
||||
useUpdateVolunteer,
|
||||
useVolunteerAvailability,
|
||||
useVolunteerPhotoUrl,
|
||||
useVolunteers,
|
||||
} from '@/api/hooks';
|
||||
import type {
|
||||
@@ -30,6 +31,7 @@ import type {
|
||||
PositionRead,
|
||||
VolunteerCreate,
|
||||
VolunteerRead,
|
||||
VolunteerUpdate,
|
||||
} from '@/api/schema';
|
||||
import { Chip, FieldLabel, HelpTip, TextInput } from '@/components/ui';
|
||||
import { ErrorNote, Loading } from '@/components/state';
|
||||
@@ -158,6 +160,79 @@ function VolunteerAccessSection({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Réponses du formulaire public d'inscription (voir `app/api/forms.py`) —
|
||||
* note de compagnon en texte libre (à résoudre manuellement en vrai vœu via
|
||||
* le sélecteur ci-dessus une fois la personne visée elle-même inscrite),
|
||||
* cases informatives, photo. N'apparaît que si le bénévole vient
|
||||
* effectivement de ce formulaire (sinon rien à afficher).
|
||||
*/
|
||||
function PublicFormAnswers({
|
||||
volunteer,
|
||||
patch,
|
||||
}: {
|
||||
volunteer: VolunteerRead;
|
||||
patch: (body: VolunteerUpdate) => void;
|
||||
}) {
|
||||
const photoUrl = useVolunteerPhotoUrl(volunteer.id, volunteer.has_photo);
|
||||
|
||||
const hasAnything =
|
||||
volunteer.has_photo ||
|
||||
Boolean(volunteer.companion_wish_note) ||
|
||||
volunteer.self_declared_available ||
|
||||
volunteer.willing_to_pay_fee;
|
||||
if (!hasAnything) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 border-2 border-ink bg-sand p-[9px]">
|
||||
<FieldLabel>
|
||||
{t('config.vol.publicForm.title')} <HelpTip text={t('config.vol.publicForm.hint')} />
|
||||
</FieldLabel>
|
||||
|
||||
{photoUrl ? (
|
||||
<img
|
||||
src={photoUrl}
|
||||
alt=""
|
||||
className="h-[90px] w-[90px] flex-none border-2 border-ink object-cover"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{volunteer.companion_wish_note ? (
|
||||
<label className="block">
|
||||
<FieldLabel>{t('form.field.companionWish')}</FieldLabel>
|
||||
<textarea
|
||||
value={volunteer.companion_wish_note}
|
||||
onChange={(e) => patch({ companion_wish_note: e.target.value || null })}
|
||||
rows={2}
|
||||
className="w-full border-2 border-ink bg-cream p-[9px] font-display text-[13px] font-bold"
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-wrap gap-[14px]">
|
||||
<label className="flex cursor-pointer items-center gap-[7px]">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={volunteer.self_declared_available}
|
||||
onChange={(e) => patch({ self_declared_available: e.target.checked })}
|
||||
className="h-[15px] w-[15px] accent-ink"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Postes dont ce bénévole est référent (`Position.lead_account_id`). Cocher
|
||||
* un poste crée ou met à niveau son accès de connexion en RÉFÉRENT (LEAD) —
|
||||
@@ -638,6 +713,8 @@ export function ConfigVolunteers() {
|
||||
searchPlaceholder={t('config.vol.searchCompanion')}
|
||||
/>
|
||||
|
||||
<PublicFormAnswers volunteer={selected} patch={patch} />
|
||||
|
||||
<AvailabilityEditor volunteerId={selected.id} />
|
||||
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user