Vœu de compagnon : traité comme réciproque par le solveur
Le vœu reste à sens unique en base (A choisit B sur sa fiche, B n'a rien à faire) mais si A souhaite B, B doit désormais profiter aussi de la présence de A — dans les deux sens : affecter A d'abord fait remonter B comme candidat, et affecter B d'abord fait remonter A, même si B n'a rien demandé. - Solveur (`loader.py`) : le vœu (A → B) alimente maintenant l'ensemble effectif des deux bénévoles (A et B), pas seulement celui du demandeur. Aucun changement dans `engine.py` : il ne fait que lire un ensemble déjà symétrisé. - Mock (`buildEffectiveCompanions`) et classement par défaut (`buildEffectiveCompanionIds` dans `lib/planning.ts`, utilisé par `AssignmentPanel.tsx`) : même symétrisation, pour rester cohérents avec le vrai solveur. - La fiche bénévole (vœux affichés/édités) n'est pas touchée : c'est le seul endroit qui reste à sens unique, par construction (la réciprocité n'est qu'une lecture, jamais une donnée enregistrée — sinon éditer sa propre liste risquerait d'effacer le vœu de quelqu'un d'autre). - Nouveau test d'intégration : le bénévole qui n'a rien demandé profite quand même de la présence de celui qui l'a choisi. - CLAUDE.md à jour. Vérifié : 193 tests backend pertinents (dont le nouveau), ruff, alembic check ; typecheck/lint/build/smoke frontend. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
166e2c3366
commit
11bc8cbe36
@@ -24,6 +24,7 @@ import { Loading } from '@/components/state';
|
||||
import {
|
||||
artistPerformanceWindow,
|
||||
availabilityCoverage,
|
||||
buildEffectiveCompanionIds,
|
||||
candidateChanceScore,
|
||||
categoryLabel,
|
||||
hoursByVolunteer,
|
||||
@@ -126,6 +127,11 @@ export function AssignmentPanel({
|
||||
const durationHours = hoursBetween(shift.starts_at, shift.ends_at);
|
||||
const assigned = useMemo(() => shiftAssignments.data ?? [], [shiftAssignments.data]);
|
||||
|
||||
const effectiveCompanionIds = useMemo(
|
||||
() => buildEffectiveCompanionIds(volunteers.data ?? []),
|
||||
[volunteers.data],
|
||||
);
|
||||
|
||||
// Raisons du score, par bénévole : vœu de poste, conflit avec un artiste
|
||||
// qu'il veut voir sur ce créneau (indépendant de sa disponibilité déclarée),
|
||||
// compagnons souhaités déjà affectés à ce créneau.
|
||||
@@ -140,14 +146,21 @@ export function AssignmentPanel({
|
||||
const artist = artistsById.get(id);
|
||||
return artist ? windowsOverlap(shift, artistPerformanceWindow(artist)) : false;
|
||||
});
|
||||
const effective = effectiveCompanionIds.get(v.id);
|
||||
let companionMatches = 0;
|
||||
if (effective) {
|
||||
for (const id of assignedIds) {
|
||||
if (effective.has(id)) companionMatches += 1;
|
||||
}
|
||||
}
|
||||
map.set(v.id, {
|
||||
wishesPosition: v.wished_position_ids.includes(positionId),
|
||||
conflictsWishedArtist,
|
||||
companionMatches: v.wished_companion_ids.filter((id) => assignedIds.has(id)).length,
|
||||
companionMatches,
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}, [volunteers.data, artistsById, shift, positionId, assigned]);
|
||||
}, [volunteers.data, artistsById, shift, positionId, assigned, effectiveCompanionIds]);
|
||||
|
||||
// Score de chance de chaque candidat pour CE créneau : vœu de poste,
|
||||
// disponibilité (dont conflit avec un artiste qu'il veut voir), compagnons
|
||||
|
||||
Reference in New Issue
Block a user