Retrait des disponibilités et des limites de temps dans la journée
Disponibilités déclarées supprimées (table `availability`, migration 0022, API, éditeur de la fiche bénévole, badge de couverture du panneau d'affectation, colonnes des statistiques) ainsi que les règles OUTSIDE_AVAILABILITY, DAILY_HOURS_EXCEEDED et INSUFFICIENT_REST, les malus de disponibilité et de repos du solveur et le filtre « Surchargés » de la vue bénévoles. Seul le chevauchement reste contrôlé. La case « je suis disponible » du formulaire public est conservée. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f671bb85bf
commit
d722a81a87
@@ -18,14 +18,9 @@ import { t } from '@/i18n';
|
||||
|
||||
const hm = (time: string) => time.slice(0, 5); // "HH:MM:SS" -> "HH:MM"
|
||||
|
||||
const RULE_CODES = [
|
||||
'SHIFT_OVERLAP',
|
||||
'OUTSIDE_AVAILABILITY',
|
||||
'DAILY_HOURS_EXCEEDED',
|
||||
'INSUFFICIENT_REST',
|
||||
] as const;
|
||||
const RULE_CODES = ['SHIFT_OVERLAP'] as const;
|
||||
|
||||
// Les seuils du moteur sont figés (CLAUDE.md §3). L'édition (nom, dates) est
|
||||
// Le moteur de validation n'a qu'une règle, figée (CLAUDE.md §3). L'édition (nom, dates) est
|
||||
// éditable — c'est là qu'on ajoute ou retire des jours.
|
||||
export function ConfigRules() {
|
||||
const volunteers = useVolunteers();
|
||||
@@ -62,9 +57,7 @@ export function ConfigRules() {
|
||||
<HelpTip text={t('config.rules.thresholdsNote')} side="bottom" />
|
||||
</PanelHeader>
|
||||
<div className="flex flex-col gap-2 p-[13px]">
|
||||
<InfoRow label={t('config.rules.maxDaily')} value="8 h" />
|
||||
<InfoRow label={t('config.rules.minRest')} value="8 h" />
|
||||
<div className="mt-2 border-t-2 border-ink pt-2">
|
||||
<div>
|
||||
<div className="mb-1 font-display text-[9px] font-extrabold uppercase tracking-widest">
|
||||
{t('config.rules.rulesTitle')}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
||||
import { useMemo, useRef, useState, type ReactNode } from 'react';
|
||||
|
||||
import { ApiError } from '@/api';
|
||||
import { roleLabelKey } from '@/auth/roles';
|
||||
@@ -10,16 +10,13 @@ import {
|
||||
useCreateVolunteer,
|
||||
useDeleteAccount,
|
||||
useDeleteVolunteer,
|
||||
useEdition,
|
||||
useExecuteImport,
|
||||
useExportMecenatReceipts,
|
||||
useExportVolunteers,
|
||||
usePositions,
|
||||
useReplaceAvailability,
|
||||
useUpdateAccount,
|
||||
useUpdatePosition,
|
||||
useUpdateVolunteer,
|
||||
useVolunteerAvailability,
|
||||
useVolunteerPhotoUrl,
|
||||
useVolunteers,
|
||||
} from '@/api/hooks';
|
||||
@@ -870,8 +867,6 @@ export function ConfigVolunteers() {
|
||||
|
||||
<PublicFormAnswers volunteer={selected} patch={patch} />
|
||||
|
||||
<AvailabilityEditor volunteerId={selected.id} />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -941,7 +936,6 @@ function AddVolunteerForm({ onClose }: { onClose: () => void }) {
|
||||
<div className="flex items-center justify-between bg-ink px-[14px] py-[11px] text-cream">
|
||||
<span className="flex items-center gap-[7px] font-display text-[14px] font-black">
|
||||
{t('config.vol.addTitle')}
|
||||
<HelpTip text={t('config.vol.addAvailabilityHint')} side="bottom" />
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1361,148 +1355,3 @@ function ImportWizard({ onClose }: { onClose: () => void }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
// Éditeur de disponibilités (fiche bénévole) //
|
||||
// --------------------------------------------------------------------------- //
|
||||
type WindowRow = { date: string; start: string; end: string };
|
||||
|
||||
function editionDays(start: string, end: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (
|
||||
let d = Date.parse(`${start}T00:00:00Z`);
|
||||
d <= Date.parse(`${end}T00:00:00Z`);
|
||||
d += 86_400_000
|
||||
) {
|
||||
out.push(new Date(d).toISOString().slice(0, 10));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function AvailabilityEditor({ volunteerId }: { volunteerId: string }) {
|
||||
const edition = useEdition();
|
||||
const windows = useVolunteerAvailability(volunteerId);
|
||||
const replace = useReplaceAvailability();
|
||||
|
||||
const [rows, setRows] = useState<WindowRow[]>([]);
|
||||
const [dirty, setDirty] = useState(false);
|
||||
|
||||
const serverRows = useMemo<WindowRow[]>(
|
||||
() =>
|
||||
(windows.data ?? []).map((w) => ({
|
||||
date: w.date,
|
||||
start: w.start_time.slice(0, 5),
|
||||
end: w.end_time.slice(0, 5),
|
||||
})),
|
||||
[windows.data],
|
||||
);
|
||||
|
||||
// Recharge depuis le serveur tant que l'utilisateur n'a rien modifié.
|
||||
useEffect(() => {
|
||||
if (!dirty) setRows(serverRows);
|
||||
}, [serverRows, dirty]);
|
||||
|
||||
// Repart de zéro quand on change de bénévole.
|
||||
useEffect(() => {
|
||||
setDirty(false);
|
||||
}, [volunteerId]);
|
||||
|
||||
const days = edition.data
|
||||
? editionDays(edition.data.start_date, edition.data.end_date)
|
||||
: [];
|
||||
|
||||
const patch = (i: number, next: Partial<WindowRow>) => {
|
||||
setDirty(true);
|
||||
setRows((rs) => rs.map((r, j) => (j === i ? { ...r, ...next } : r)));
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
replace.mutate(
|
||||
{
|
||||
volunteerId,
|
||||
body: {
|
||||
windows: rows.map((r) => ({
|
||||
date: r.date,
|
||||
start_time: `${r.start}:00`,
|
||||
end_time: `${r.end}:00`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
{ onSuccess: () => setDirty(false) },
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FieldLabel>
|
||||
{t('config.vol.avail.title')} <HelpTip text={t('config.vol.avail.hint')} />
|
||||
</FieldLabel>
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
{rows.map((row, i) => (
|
||||
<div key={i} className="flex flex-wrap items-center gap-1">
|
||||
<select
|
||||
value={row.date}
|
||||
onChange={(e) => patch(i, { date: e.target.value })}
|
||||
className="min-w-0 flex-1 border-2 border-ink bg-cream p-[6px] font-mono text-[11px]"
|
||||
>
|
||||
{days.map((d) => (
|
||||
<option key={d} value={d}>
|
||||
{shortDayLabel(`${d}T12:00`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="time"
|
||||
value={row.start}
|
||||
onChange={(e) => patch(i, { start: e.target.value })}
|
||||
className="border-2 border-ink bg-cream p-[5px] font-mono text-[11px]"
|
||||
/>
|
||||
<input
|
||||
type="time"
|
||||
value={row.end}
|
||||
onChange={(e) => patch(i, { end: e.target.value })}
|
||||
className="border-2 border-ink bg-cream p-[5px] font-mono text-[11px]"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setDirty(true);
|
||||
setRows((rs) => rs.filter((_, j) => j !== i));
|
||||
}}
|
||||
aria-label={t('action.remove')}
|
||||
className="h-[26px] w-[26px] flex-none cursor-pointer border-2 border-ink bg-cream font-display text-[11px] font-bold hover:bg-orange"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{rows.length === 0 ? (
|
||||
<span className="font-mono text-[10px] opacity-55">{t('config.vol.avail.none')}</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={days.length === 0}
|
||||
onClick={() => {
|
||||
setDirty(true);
|
||||
setRows((rs) => [...rs, { date: days[0], start: '08:00', end: '18:00' }]);
|
||||
}}
|
||||
className="cursor-pointer border-2 border-ink bg-cream px-3 py-[6px] font-display text-[10px] font-extrabold uppercase hover:bg-yellow disabled:opacity-50"
|
||||
>
|
||||
+ {t('config.vol.avail.add')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!dirty || replace.isPending}
|
||||
onClick={save}
|
||||
className="cursor-pointer border-2 border-ink bg-yellow px-3 py-[6px] font-display text-[10px] font-extrabold uppercase hover:bg-green hover:text-white disabled:opacity-50"
|
||||
>
|
||||
{t('config.vol.avail.save')}
|
||||
</button>
|
||||
</div>
|
||||
{replace.isError ? <ErrorNote error={replace.error} /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user