import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; import { ApiError } from '@/api'; import { useAccounts, useAnalyzeImport, useArtists, useCreateAccount, useCreateVolunteer, useDeleteAccount, useDeleteVolunteer, useEdition, useExecuteImport, usePositions, useReplaceAvailability, useUpdateVolunteer, useVolunteerAvailability, useVolunteers, } from '@/api/hooks'; import type { ArtistRead, ImportAnalyzeResponse, ImportFieldKey, ImportIssue, ImportMapping, ImportReport, PositionRead, VolunteerCreate, VolunteerRead, } from '@/api/schema'; import { FieldLabel, HelpTip, TextInput } from '@/components/ui'; import { ErrorNote, Loading } from '@/components/state'; import { t } from '@/i18n'; import { shortDayLabel } from '@/lib/time'; import { cx } from '@/lib/cx'; const MAP_FIELDS: Array<{ key: ImportFieldKey; required: boolean }> = [ { key: 'first_name', required: true }, { key: 'last_name', required: true }, { key: 'email', required: false }, { key: 'phone', required: false }, { key: 'comment', required: false }, ]; const FIELD_LABEL: Record = { first_name: 'config.vol.field.firstName', last_name: 'config.vol.field.lastName', email: 'config.vol.field.email', phone: 'config.vol.field.phone', comment: 'config.vol.field.comment', }; type WishOption = { id: string; label: string; hint?: string }; function artistOptions(artists: ArtistRead[] | undefined): WishOption[] { return [...(artists ?? [])] .sort( (a, b) => a.performance_date.localeCompare(b.performance_date) || a.name.localeCompare(b.name, 'fr'), ) .map((a) => ({ id: a.id, label: a.name, hint: shortDayLabel(`${a.performance_date}T12:00`), })); } function positionOptions(positions: PositionRead[] | undefined): WishOption[] { return [...(positions ?? [])] .sort((a, b) => a.name.localeCompare(b.name, 'fr')) .map((p) => ({ id: p.id, label: p.name, hint: p.location ?? undefined })); } /** Les autres bénévoles, pour le vœu de compagnon (jamais soi-même). */ function volunteerOptions( volunteers: VolunteerRead[] | undefined, excludeId?: string, ): WishOption[] { return [...(volunteers ?? [])] .filter((v) => v.id !== excludeId) .sort( (a, b) => a.last_name.localeCompare(b.last_name, 'fr') || a.first_name.localeCompare(b.first_name, 'fr'), ) .map((v) => ({ id: v.id, label: `${v.first_name} ${v.last_name}` })); } /** * Accès de connexion (lien magique) du bénévole. Ajouter un email sur sa * fiche ne crée aucun compte : sans ce geste explicite, `auth/magic-link` ne * trouve personne à qui écrire et ne fait rien (par conception, pour ne * jamais révéler si un email existe). */ function VolunteerAccessSection({ volunteerId, email, }: { volunteerId: string; email: string | null; }) { const accounts = useAccounts(); const createAccount = useCreateAccount(); const deleteAccount = useDeleteAccount(); const account = accounts.data?.find((a) => a.volunteer_id === volunteerId); return (
{t('config.vol.access.title')} {account ? (
{t('config.vol.access.active', { email: account.email })}
) : ( )} {!account && !email ? (
{t('config.vol.access.needEmail')}
) : null} {createAccount.isError ? : null} {deleteAccount.isError ? : null}
); } function WishPicker({ label, options, selected, onChange, emptyLabel, addLabel, }: { label: ReactNode; options: WishOption[]; selected: string[]; onChange: (ids: string[]) => void; emptyLabel: string; addLabel: string; }) { const byId = new Map(options.map((o) => [o.id, o])); const available = options.filter((o) => !selected.includes(o.id)); return (
{label}
{selected.map((id) => ( ))} {selected.length === 0 ? ( {emptyLabel} ) : null}
); } type Mode = 'list' | 'add' | 'import'; /** Retire les paramètres nuls avant traduction (`t` n'accepte pas `null`). */ function issueParams(issue: ImportIssue): Record { const out: Record = {}; for (const [k, v] of Object.entries(issue.params)) { if (v != null) out[k] = v; } return out; } export function ConfigVolunteers() { const volunteers = useVolunteers(); const artists = useArtists(); const positions = usePositions(); const updateVolunteer = useUpdateVolunteer(); const deleteVolunteer = useDeleteVolunteer(); const [mode, setMode] = useState('list'); const [selectedId, setSelectedId] = useState(null); const [search, setSearch] = useState(''); const artistOpts = useMemo(() => artistOptions(artists.data), [artists.data]); const positionOpts = useMemo(() => positionOptions(positions.data), [positions.data]); const companionOpts = useMemo( () => volunteerOptions(volunteers.data, selectedId ?? undefined), [volunteers.data, selectedId], ); const filtered = useMemo(() => { const needle = search.trim().toLowerCase(); const list = [...(volunteers.data ?? [])]; if (!needle) return list; return list.filter((v) => `${v.first_name} ${v.last_name} ${v.email ?? ''}`.toLowerCase().includes(needle), ); }, [volunteers.data, search]); const selected = useMemo(() => { const list = volunteers.data ?? []; return list.find((v) => v.id === selectedId) ?? null; }, [volunteers.data, selectedId]); if (volunteers.isLoading) return ; if (volunteers.error) return ; const total = volunteers.data?.length ?? 0; const rows = filtered.slice(0, 60); const patch = (body: Parameters[0]['body']) => selected && updateVolunteer.mutate({ id: selected.id, body }); if (mode === 'add') { return setMode('list')} />; } if (mode === 'import') { return setMode('list')} />; } return (
{t('config.vol.importTitle')}
{total}
{t('config.vol.profiles')}
setSearch(e.target.value)} placeholder={t('config.vol.search')} className="w-full border-2 border-ink bg-cream p-[9px] font-mono text-[12px]" />
{total === 0 ? (
{t('config.vol.emptyList')}
) : rows.length === 0 ? (
{t('config.vol.noMatch')}
) : (
{t('config.vol.colName')} {t('config.vol.colPhone')} {t('config.vol.colEmail')} {t('config.vol.colWishes')}
{rows.map((volunteer) => { const active = volunteer.id === selected?.id; return ( ); })} {filtered.length > rows.length ? (
{t('config.vol.count', { count: filtered.length })}
) : null}
)}
{selected ? (