Frontend : suppression du statut d'affectation (suite Étape 9)
Le back-end (Étape 9) a retiré AssignmentStatus : une affectation est valide
dès sa création, la retirer = supprimer la ligne.
- schema.ts : AssignmentRead/Create/Update sans `status` ;
MatrixShift.{confirmed, proposed} → `assigned`.
- mock : générateur sans statut (plus de rollStatus / DECLINED) ; matrice
= compte unique ; create → validation toujours exécutée ; PATCH ne porte
plus que source/locked ; runValidation compte toutes les affectations.
- lib/planning : suppression de isActive() et statusLabel().
- Grille + panneau : cellule « affectés / idéal », plus de +proposés ni de
badge confirmé/proposé ni de bouton « confirmer ».
- Vue par bénévole : une seule trame d'affectation (bleu).
- Mobile bénévole : plus de bouton « JE CONFIRME » ni de bannière.
- Mobile référent : plus de libellé de statut par équipier.
- Onglet 03 « Confirmations » supprimé (route, page, nav, ROUTE_ROLES) —
il n'a plus d'objet. Coordination : onglets 01, 02, 06.
- i18n : clés de statut / de l'écran Confirmations retirées.
- Vérifs : build tsc + vite, eslint (0 problème), npm run smoke.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwswqSChc3Rwtu61pRXiY2
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
6e16db4b7e
commit
8d333deb23
@@ -3,7 +3,6 @@ import { hoursBetween } from '@/lib/time';
|
||||
import type {
|
||||
AccountRead,
|
||||
AssignmentRead,
|
||||
AssignmentStatus,
|
||||
PositionCategory,
|
||||
PositionRead,
|
||||
ShiftRead,
|
||||
@@ -105,12 +104,6 @@ function iso(dateStr: string, hour: number): string {
|
||||
return `${dateStr}T${String(hour).padStart(2, '0')}:00:00`;
|
||||
}
|
||||
|
||||
function rollStatus(roll: number): AssignmentStatus {
|
||||
if (roll < 0.72) return 'CONFIRMED';
|
||||
if (roll < 0.95) return 'PROPOSED';
|
||||
return 'DECLINED';
|
||||
}
|
||||
|
||||
function demoEmail(firstName: string, lastName: string): string {
|
||||
const n = (s: string) => s.toLowerCase().normalize('NFD').replace(/[^a-z]/g, '');
|
||||
return `${n(firstName)}.${n(lastName)}@mail.fr`;
|
||||
@@ -240,33 +233,23 @@ export function buildMockState(): MockState {
|
||||
}
|
||||
|
||||
for (const v of pool.slice(0, target)) {
|
||||
const status = rollStatus(rnd());
|
||||
assignments.push({
|
||||
id: `a${seq}`,
|
||||
edition_id,
|
||||
volunteer_id: v.id,
|
||||
shift_id: shift.id,
|
||||
status,
|
||||
source: 'SOLVER',
|
||||
locked: false,
|
||||
created_at: NOW,
|
||||
updated_at: NOW,
|
||||
});
|
||||
seq += 1;
|
||||
if (status !== 'DECLINED') {
|
||||
busy.add(`${v.id}|${shift.id}`);
|
||||
dayHours.set(`${v.id}|${dateStr}`, (dayHours.get(`${v.id}|${dateStr}`) ?? 0) + duration);
|
||||
totalHours.set(v.id, (totalHours.get(v.id) ?? 0) + duration);
|
||||
}
|
||||
busy.add(`${v.id}|${shift.id}`);
|
||||
dayHours.set(`${v.id}|${dateStr}`, (dayHours.get(`${v.id}|${dateStr}`) ?? 0) + duration);
|
||||
totalHours.set(v.id, (totalHours.get(v.id) ?? 0) + duration);
|
||||
}
|
||||
}
|
||||
|
||||
assignments
|
||||
.filter((a) => a.volunteer_id === me.id)
|
||||
.forEach((a, i) => {
|
||||
a.status = i === 0 ? 'CONFIRMED' : 'PROPOSED';
|
||||
});
|
||||
|
||||
OVERLOADED.forEach((index) => {
|
||||
const v = volunteers[index];
|
||||
const friday = shifts.filter((s) => s.starts_at.slice(0, 10) === '2026-10-17');
|
||||
@@ -284,7 +267,6 @@ export function buildMockState(): MockState {
|
||||
edition_id,
|
||||
volunteer_id: v.id,
|
||||
shift_id: shift.id,
|
||||
status: 'CONFIRMED',
|
||||
source: 'MANUAL',
|
||||
locked: false,
|
||||
created_at: NOW,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { hoursBetween, naiveMinutes } from '@/lib/time';
|
||||
|
||||
import type {
|
||||
AccountRead,
|
||||
AssignmentRead,
|
||||
AssignmentValidateResult,
|
||||
MatrixResponse,
|
||||
ShiftRead,
|
||||
TokenResponse,
|
||||
Violation,
|
||||
} from '../schema';
|
||||
import { ACTIVE_ASSIGNMENT_STATUSES } from '../schema';
|
||||
import type { AccountRead, TokenResponse } from '../schema';
|
||||
import { getAuthToken } from '../session';
|
||||
import { ApiError, type PlanningApi } from '../types';
|
||||
import { buildMockState, type MockState } from './dataset';
|
||||
@@ -85,8 +85,7 @@ function runValidation(volunteerId: string, candidate: ShiftRead, excludeAssignm
|
||||
(a) =>
|
||||
a.volunteer_id === volunteerId &&
|
||||
a.id !== excludeAssignmentId &&
|
||||
a.shift_id !== candidate.id &&
|
||||
ACTIVE_ASSIGNMENT_STATUSES.includes(a.status),
|
||||
a.shift_id !== candidate.id,
|
||||
)
|
||||
.map((a) => shiftById(a.shift_id));
|
||||
|
||||
@@ -180,12 +179,9 @@ function hasBlocking(violations: Violation[]): boolean {
|
||||
}
|
||||
|
||||
function buildMatrix(): MatrixResponse {
|
||||
const counts = new Map<string, { confirmed: number; proposed: number }>();
|
||||
const counts = new Map<string, number>();
|
||||
for (const a of state.assignments) {
|
||||
const entry = counts.get(a.shift_id) ?? { confirmed: 0, proposed: 0 };
|
||||
if (a.status === 'CONFIRMED') entry.confirmed += 1;
|
||||
if (a.status === 'PROPOSED') entry.proposed += 1;
|
||||
counts.set(a.shift_id, entry);
|
||||
counts.set(a.shift_id, (counts.get(a.shift_id) ?? 0) + 1);
|
||||
}
|
||||
return {
|
||||
edition_id: state.positions[0]?.edition_id ?? '',
|
||||
@@ -199,18 +195,14 @@ function buildMatrix(): MatrixResponse {
|
||||
shifts: state.shifts
|
||||
.filter((s) => s.position_id === position.id)
|
||||
.sort((a, b) => a.starts_at.localeCompare(b.starts_at))
|
||||
.map((s) => {
|
||||
const c = counts.get(s.id) ?? { confirmed: 0, proposed: 0 };
|
||||
return {
|
||||
shift_id: s.id,
|
||||
starts_at: s.starts_at,
|
||||
ends_at: s.ends_at,
|
||||
min_staff: s.min_staff,
|
||||
ideal_staff: s.ideal_staff,
|
||||
confirmed: c.confirmed,
|
||||
proposed: c.proposed,
|
||||
};
|
||||
}),
|
||||
.map((s) => ({
|
||||
shift_id: s.id,
|
||||
starts_at: s.starts_at,
|
||||
ends_at: s.ends_at,
|
||||
min_staff: s.min_staff,
|
||||
ideal_staff: s.ideal_staff,
|
||||
assigned: counts.get(s.id) ?? 0,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -390,13 +382,11 @@ export function createMockApi(): PlanningApi {
|
||||
) {
|
||||
throw new ApiError(409, 'assignment.duplicate');
|
||||
}
|
||||
const status = body.status ?? 'PROPOSED';
|
||||
let warnings: Violation[] = [];
|
||||
if (ACTIVE_ASSIGNMENT_STATUSES.includes(status)) {
|
||||
warnings = runValidation(body.volunteer_id, shift);
|
||||
if (hasBlocking(warnings)) {
|
||||
throw new ApiError(422, 'error.validation_failed', warnings);
|
||||
}
|
||||
// Une affectation est toujours validée à la création : une violation
|
||||
// bloquante l'interdit, les avertissements sont renvoyés.
|
||||
const warnings = runValidation(body.volunteer_id, shift);
|
||||
if (hasBlocking(warnings)) {
|
||||
throw new ApiError(422, 'error.validation_failed', warnings);
|
||||
}
|
||||
const now = touch();
|
||||
const assignment: AssignmentRead = {
|
||||
@@ -404,7 +394,6 @@ export function createMockApi(): PlanningApi {
|
||||
edition_id: shift.edition_id,
|
||||
volunteer_id: body.volunteer_id,
|
||||
shift_id: body.shift_id,
|
||||
status,
|
||||
source: body.source ?? 'MANUAL',
|
||||
locked: body.locked ?? false,
|
||||
created_at: now,
|
||||
@@ -416,19 +405,11 @@ export function createMockApi(): PlanningApi {
|
||||
update: (_edition, id, body) => {
|
||||
const a = state.assignments.find((x) => x.id === id) ?? notFound();
|
||||
Object.assign(a, {
|
||||
...('status' in body ? { status: body.status } : {}),
|
||||
...('source' in body ? { source: body.source } : {}),
|
||||
...('locked' in body ? { locked: body.locked } : {}),
|
||||
updated_at: touch(),
|
||||
});
|
||||
let warnings: Violation[] = [];
|
||||
if (ACTIVE_ASSIGNMENT_STATUSES.includes(a.status)) {
|
||||
warnings = runValidation(a.volunteer_id, shiftById(a.shift_id), a.id);
|
||||
if (hasBlocking(warnings)) {
|
||||
throw new ApiError(422, 'error.validation_failed', warnings);
|
||||
}
|
||||
}
|
||||
return delay({ assignment: a, warnings });
|
||||
return delay({ assignment: a, warnings: [] as Violation[] });
|
||||
},
|
||||
remove: (_edition, id) => {
|
||||
state.assignments = state.assignments.filter((x) => x.id !== id);
|
||||
@@ -487,12 +468,10 @@ export function createMockApi(): PlanningApi {
|
||||
};
|
||||
}
|
||||
|
||||
/** Heures actives d'un bénévole (utilitaire mock, exposé pour les tests). */
|
||||
/** Heures affectées d'un bénévole (utilitaire mock, exposé pour les tests). */
|
||||
export function mockVolunteerHours(volunteerId: string): number {
|
||||
return state.assignments
|
||||
.filter(
|
||||
(a) => a.volunteer_id === volunteerId && ACTIVE_ASSIGNMENT_STATUSES.includes(a.status),
|
||||
)
|
||||
.filter((a) => a.volunteer_id === volunteerId)
|
||||
.reduce((sum, a) => {
|
||||
const s = state.shifts.find((x) => x.id === a.shift_id);
|
||||
return s ? sum + hoursBetween(s.starts_at, s.ends_at) : sum;
|
||||
|
||||
Reference in New Issue
Block a user