Mode de paiement de la cotisation visible (en ligne, sur place)
Jusqu'ici seul un paiement en ligne apparaissait ; un bénévole qui paiera sur place ne se distinguait pas d'un bénévole sans cotisation. Mode déduit des champs existants (lib/fee.ts:feeMode, exporting/xlsx.py:_payment_mode), sans migration : - fiche : section « Cotisation » toujours visible — paiement en ligne (type, montant, statut, mécénat et adresse du reçu) ou case « Paiera sur place » modifiable à la main - liste : colonne « Cotisation » (Sur place / En ligne ✓ / Mécénat ✓ / —) - export Excel : colonne « Mode de paiement » Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a6e7b6e7fb
commit
ff57bbd863
@@ -311,6 +311,19 @@
|
||||
"config.vol.publicForm.title": "Formulaire public",
|
||||
"config.vol.publicForm.hint": "Réponses saisies par le bénévole lui-même via le formulaire public d'inscription.",
|
||||
"config.vol.colWishes": "VŒUX",
|
||||
"config.vol.colFee": "COTISATION",
|
||||
"config.vol.fee.title": "Cotisation",
|
||||
"config.vol.fee.online": "Paiement en ligne (SumUp)",
|
||||
"config.vol.fee.mecenat": "Mécénat — reçu fiscal",
|
||||
"config.vol.fee.onSite": "Paiera sur place",
|
||||
"config.vol.fee.none": "aucune cotisation déclarée",
|
||||
"config.vol.fee.onSiteShort": "Sur place",
|
||||
"config.vol.fee.onlineShort": "En ligne",
|
||||
"config.vol.fee.mecenatShort": "Mécénat",
|
||||
"config.vol.fee.short.pending": "en attente",
|
||||
"config.vol.fee.short.paid": "payée",
|
||||
"config.vol.fee.short.failed": "échouée",
|
||||
"config.vol.fee.short.expired": "expirée",
|
||||
"config.vol.wishesCount": "{count} vœu(x)",
|
||||
"config.vol.addAvailabilityHint": "Les disponibilités se renseignent sur la fiche du bénévole, une fois celui-ci créé.",
|
||||
"config.vol.avail.title": "Disponibilités",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { VolunteerRead } from '@/api/schema';
|
||||
|
||||
/** Comment un bénévole règle sa cotisation — déduit des champs existants,
|
||||
* même règle que `app/exporting/xlsx.py:_payment_mode` :
|
||||
* - `ONLINE` : un paiement SumUp a été lancé (type, montant et statut sur
|
||||
* `fee_payment_*`) ;
|
||||
* - `ON_SITE` : « je participe à la cotisation » sans paiement en ligne
|
||||
* (« Je paierai sur place » au formulaire, ou case cochée à la main) ;
|
||||
* - `NONE` : rien de déclaré. */
|
||||
export type FeeMode = 'ONLINE' | 'ON_SITE' | 'NONE';
|
||||
|
||||
export function feeMode(
|
||||
volunteer: Pick<VolunteerRead, 'fee_payment_status' | 'willing_to_pay_fee'>,
|
||||
): FeeMode {
|
||||
if (volunteer.fee_payment_status !== null) return 'ONLINE';
|
||||
if (volunteer.willing_to_pay_fee) return 'ON_SITE';
|
||||
return 'NONE';
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import type {
|
||||
} from '@/api/schema';
|
||||
import { Chip, FieldLabel, HelpTip, TextInput } from '@/components/ui';
|
||||
import { ErrorNote, Loading } from '@/components/state';
|
||||
import { feeMode } from '@/lib/fee';
|
||||
import { foldAccents } from '@/lib/planning';
|
||||
import { t } from '@/i18n';
|
||||
import { shortDayLabel } from '@/lib/time';
|
||||
@@ -188,14 +189,9 @@ function PublicFormAnswers({
|
||||
const hasAnything =
|
||||
volunteer.has_photo ||
|
||||
Boolean(volunteer.companion_wish_note) ||
|
||||
volunteer.self_declared_available ||
|
||||
volunteer.fee_payment_status !== null;
|
||||
volunteer.self_declared_available;
|
||||
if (!hasAnything) return null;
|
||||
|
||||
const feeStatusKey = volunteer.fee_payment_status
|
||||
? FEE_STATUS_LABEL_KEY[volunteer.fee_payment_status]
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 border-2 border-ink bg-sand p-[9px]">
|
||||
<FieldLabel>
|
||||
@@ -232,20 +228,75 @@ function PublicFormAnswers({
|
||||
/>
|
||||
<span className="font-mono text-[11px]">{t('form.field.available')}</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} €) — ` : ''}
|
||||
{t(feeStatusKey)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</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}
|
||||
/**
|
||||
* Cotisation du bénévole (voir `lib/fee.ts:feeMode`) : paiement en ligne
|
||||
* (type, montant, statut, mécénat), paiement sur place, ou rien. Toujours
|
||||
* affichée, pour qu'un « rien » se voie aussi ; le paiement sur place reste
|
||||
* modifiable à la main (bénévole ajouté sans le formulaire, changement
|
||||
* d'avis…), un paiement en ligne non — son statut vient de SumUp.
|
||||
*/
|
||||
function FeeSection({
|
||||
volunteer,
|
||||
patch,
|
||||
}: {
|
||||
volunteer: VolunteerRead;
|
||||
patch: (body: VolunteerUpdate) => void;
|
||||
}) {
|
||||
const mode = feeMode(volunteer);
|
||||
return (
|
||||
<div className="flex flex-col gap-2 border-2 border-ink bg-sand p-[9px]">
|
||||
<FieldLabel>{t('config.vol.fee.title')}</FieldLabel>
|
||||
{mode === 'ONLINE' && volunteer.fee_payment_status ? (
|
||||
<>
|
||||
<div className="flex flex-wrap items-center gap-[6px]">
|
||||
<span className="border-2 border-ink bg-blue px-[8px] py-[3px] font-mono text-[10.5px] font-bold">
|
||||
{t('config.vol.fee.online')}
|
||||
</span>
|
||||
{volunteer.mecenat_address ? (
|
||||
<span className="border-2 border-ink bg-mauve px-[8px] py-[3px] font-mono text-[10.5px] font-bold">
|
||||
{t('config.vol.fee.mecenat')}
|
||||
</span>
|
||||
) : null}
|
||||
<span
|
||||
className={cx(
|
||||
'border-2 border-ink px-[8px] py-[3px] font-mono text-[10.5px] font-bold',
|
||||
volunteer.fee_payment_status === 'PAID' ? 'bg-green text-cream' : 'bg-yellow-soft',
|
||||
)}
|
||||
>
|
||||
{t(FEE_STATUS_LABEL_KEY[volunteer.fee_payment_status])}
|
||||
</span>
|
||||
</div>
|
||||
{volunteer.fee_payment_label ? (
|
||||
<p className="font-mono text-[11px]">
|
||||
{volunteer.fee_payment_label} — {volunteer.fee_amount} €
|
||||
</p>
|
||||
) : null}
|
||||
{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}
|
||||
</>
|
||||
) : (
|
||||
<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] font-bold">{t('config.vol.fee.onSite')}</span>
|
||||
{mode === 'NONE' ? (
|
||||
<span className="font-mono text-[10px] opacity-60">— {t('config.vol.fee.none')}</span>
|
||||
) : null}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -483,6 +534,35 @@ function issueParams(issue: ImportIssue): Record<string, string | number> {
|
||||
return out;
|
||||
}
|
||||
|
||||
const LIST_COLUMNS =
|
||||
'minmax(0,2fr) minmax(0,1.4fr) minmax(0,2fr) minmax(0,1.2fr) minmax(0,1.2fr)';
|
||||
|
||||
/** Résumé d'une ligne de la liste : mode de paiement (et mécénat), statut
|
||||
* d'un paiement en ligne non abouti. */
|
||||
function FeeTag({ volunteer }: { volunteer: VolunteerRead }) {
|
||||
const mode = feeMode(volunteer);
|
||||
if (mode === 'NONE') return <span className="font-mono text-[10px] opacity-40">—</span>;
|
||||
if (mode === 'ON_SITE') {
|
||||
return (
|
||||
<span className="truncate font-mono text-[10px] font-bold">{t('config.vol.fee.onSiteShort')}</span>
|
||||
);
|
||||
}
|
||||
const paid = volunteer.fee_payment_status === 'PAID';
|
||||
return (
|
||||
<span className={cx('truncate font-mono text-[10px] font-bold', paid ? 'text-green' : 'text-orange-dark')}>
|
||||
{t(volunteer.mecenat_address ? 'config.vol.fee.mecenatShort' : 'config.vol.fee.onlineShort')}
|
||||
{paid ? ' ✓' : ` · ${t(FEE_STATUS_SHORT_KEY[volunteer.fee_payment_status ?? 'PENDING'])}`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const FEE_STATUS_SHORT_KEY: Record<NonNullable<VolunteerRead['fee_payment_status']>, string> = {
|
||||
PENDING: 'config.vol.fee.short.pending',
|
||||
PAID: 'config.vol.fee.short.paid',
|
||||
FAILED: 'config.vol.fee.short.failed',
|
||||
EXPIRED: 'config.vol.fee.short.expired',
|
||||
};
|
||||
|
||||
export function ConfigVolunteers() {
|
||||
const volunteers = useVolunteers();
|
||||
const artists = useArtists();
|
||||
@@ -641,12 +721,13 @@ export function ConfigVolunteers() {
|
||||
<div className="border-[3px] border-ink bg-paper shadow-brutal-lg">
|
||||
<div
|
||||
className="grid gap-[10px] bg-ink px-[10px] py-[7px] font-display text-[9.5px] font-extrabold uppercase tracking-wide text-cream"
|
||||
style={{ gridTemplateColumns: 'minmax(0,2fr) minmax(0,1.4fr) minmax(0,2fr) minmax(0,1.4fr)' }}
|
||||
style={{ gridTemplateColumns: LIST_COLUMNS }}
|
||||
>
|
||||
<span>{t('config.vol.colName')}</span>
|
||||
<span>{t('config.vol.colPhone')}</span>
|
||||
<span>{t('config.vol.colEmail')}</span>
|
||||
<span>{t('config.vol.colWishes')}</span>
|
||||
<span>{t('config.vol.colFee')}</span>
|
||||
</div>
|
||||
{rows.map((volunteer) => {
|
||||
const active = volunteer.id === selected?.id;
|
||||
@@ -659,7 +740,7 @@ export function ConfigVolunteers() {
|
||||
'grid w-full items-center gap-[10px] border-b border-sand-line px-[10px] py-[7px] text-left',
|
||||
active ? 'bg-yellow' : 'hover:bg-cream',
|
||||
)}
|
||||
style={{ gridTemplateColumns: 'minmax(0,2fr) minmax(0,1.4fr) minmax(0,2fr) minmax(0,1.4fr)' }}
|
||||
style={{ gridTemplateColumns: LIST_COLUMNS }}
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-[7px]">
|
||||
<span className="flex h-6 w-6 flex-none items-center justify-center border-2 border-ink bg-paper font-display text-[9.5px] font-extrabold">
|
||||
@@ -684,6 +765,7 @@ export function ConfigVolunteers() {
|
||||
volunteer.wished_companion_ids.length,
|
||||
})}
|
||||
</span>
|
||||
<FeeTag volunteer={volunteer} />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -784,6 +866,8 @@ export function ConfigVolunteers() {
|
||||
searchPlaceholder={t('config.vol.searchCompanion')}
|
||||
/>
|
||||
|
||||
<FeeSection volunteer={selected} patch={patch} />
|
||||
|
||||
<PublicFormAnswers volunteer={selected} patch={patch} />
|
||||
|
||||
<AvailabilityEditor volunteerId={selected.id} />
|
||||
|
||||
Reference in New Issue
Block a user