From 8d333deb2336e96378d8b633b631cea203645f8c Mon Sep 17 00:00:00 2001 From: AdamThomas Date: Thu, 10 Sep 2026 07:51:11 +0200 Subject: [PATCH] =?UTF-8?q?Frontend=20:=20suppression=20du=20statut=20d'af?= =?UTF-8?q?fectation=20(suite=20=C3=89tape=209)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01AwswqSChc3Rwtu61pRXiY2 --- frontend/README.md | 15 ++- frontend/scripts/smoke.tsx | 2 - frontend/src/api/mock/dataset.ts | 24 +--- frontend/src/api/mock/mock-client.ts | 63 ++++------ frontend/src/api/schema.ts | 13 +- frontend/src/app/AppShell.tsx | 9 +- frontend/src/app/router.tsx | 2 - frontend/src/auth/roles.ts | 9 +- frontend/src/i18n/locales/fr.json | 31 +---- frontend/src/lib/planning.ts | 14 +-- frontend/src/pages/GrillePage.tsx | 9 +- frontend/src/pages/MobileLeadPage.tsx | 28 ++--- frontend/src/pages/MobileVolunteerPage.tsx | 43 +------ frontend/src/pages/PublicationPage.tsx | 115 ------------------ frontend/src/pages/VolunteerViewPage.tsx | 24 +--- frontend/src/pages/grille/AssignmentPanel.tsx | 27 +--- 16 files changed, 75 insertions(+), 353 deletions(-) delete mode 100644 frontend/src/pages/PublicationPage.tsx diff --git a/frontend/README.md b/frontend/README.md index 817b8a2..227ba10 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -38,7 +38,7 @@ Visibilité des onglets par rôle : | Onglet | Coordination | Référent | Bénévole | |--------|:---:|:---:|:---:| -| 01 Grille · 02 Vue bénévole · 03 Confirmations · 06 Configuration | ✅ | — | — | +| 01 Grille · 02 Vue bénévole · 06 Configuration | ✅ | — | — | | 04 Bénévole · mobile | — | — | ✅ | | 05 Référent · mobile | — | ✅ | — | @@ -46,18 +46,21 @@ L'accueil après connexion dépend du rôle (`homePath`). ## Écrans (onglets) +Depuis l'Étape 9 back-end, une affectation n'a plus de statut : elle est +valide dès sa création, la retirer = supprimer la ligne. Il n'y a donc plus +d'écran « Confirmations » (ancien onglet 03). + | # | Route | Source de données | Actions | |---|-------|-------------------|---------| -| 01 | `/grille` | `GET /matrix` | clic cellule → panneau : `GET /assignments?shift_id`, `POST /assignments/validate` par candidat, `POST` / `PATCH` / `DELETE /assignments` | +| 01 | `/grille` | `GET /matrix` (`assigned` / `ideal`) | clic cellule → panneau : `GET /assignments?shift_id`, `POST /assignments/validate` par candidat, `POST` / `DELETE /assignments` | | 02 | `/benevoles` | `volunteers` + `shifts` + `assignments` | lecture (surcharge / oubliés calculés côté client) | -| 03 | `/publication` | `assignments` (statut `PROPOSED`) | lecture — publication versionnée & emails **hors périmètre** (CLAUDE.md §4) | -| 04 | `/mobile-benevole` | `assignments?volunteer_id` (du compte) | `PATCH /assignments/{id}` (confirmer)¹ | +| 04 | `/mobile-benevole` | `assignments?volunteer_id` (du compte) | lecture¹ | | 05 | `/mobile-referent` | poste du référent → `assignments?shift_id` | pointage **local** (hors périmètre) | | 06 | `/configuration` | | `positions` CRUD, `shifts` CRUD (min/idéal, ajout/retrait), `volunteers` CRUD (dont suppression RGPD). Messages = maquette locale. | ¹ En mode `http`, l'API n'expose pas encore aux bénévoles le détail des -créneaux ni l'auto-confirmation : l'écran dégrade proprement (créneaux sans -détail, message d'info). Fonctionne intégralement en mode `mock`. +créneaux : l'écran dégrade proprement (créneaux sans détail, message d'info). +Fonctionne intégralement en mode `mock`. ## Commandes diff --git a/frontend/scripts/smoke.tsx b/frontend/scripts/smoke.tsx index 5ee9554..e2f914d 100644 --- a/frontend/scripts/smoke.tsx +++ b/frontend/scripts/smoke.tsx @@ -22,7 +22,6 @@ import { ConfigPage } from '@/pages/ConfigPage'; import { GrillePage } from '@/pages/GrillePage'; import { MobileLeadPage } from '@/pages/MobileLeadPage'; import { MobileVolunteerPage } from '@/pages/MobileVolunteerPage'; -import { PublicationPage } from '@/pages/PublicationPage'; import { VolunteerViewPage } from '@/pages/VolunteerViewPage'; const e = EDITION_ID; @@ -156,7 +155,6 @@ if (coordinator) { setAuthToken(coordinatorToken); await renderProtected('/grille', coordinator, createElement(GrillePage)); await renderProtected('/benevoles', coordinator, createElement(VolunteerViewPage)); - await renderProtected('/publication', coordinator, createElement(PublicationPage)); await renderProtected('/configuration', coordinator, createElement(ConfigPage)); } if (volunteerAccount) { diff --git a/frontend/src/api/mock/dataset.ts b/frontend/src/api/mock/dataset.ts index 46328a1..0ef0c13 100644 --- a/frontend/src/api/mock/dataset.ts +++ b/frontend/src/api/mock/dataset.ts @@ -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, diff --git a/frontend/src/api/mock/mock-client.ts b/frontend/src/api/mock/mock-client.ts index e0e2097..d19a37f 100644 --- a/frontend/src/api/mock/mock-client.ts +++ b/frontend/src/api/mock/mock-client.ts @@ -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(); + const counts = new Map(); 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; diff --git a/frontend/src/api/schema.ts b/frontend/src/api/schema.ts index 37eed0f..76ac328 100644 --- a/frontend/src/api/schema.ts +++ b/frontend/src/api/schema.ts @@ -12,16 +12,14 @@ export type PositionCategory = | 'STAGE' | 'OTHER'; -export type AssignmentStatus = 'PROPOSED' | 'CONFIRMED' | 'DECLINED' | 'CANCELLED'; +// Depuis l'Étape 9 back-end : une affectation est valide dès sa création, +// il n'y a plus de statut (proposée / confirmée…). La retirer = supprimer la ligne. export type AssignmentSource = 'SOLVER' | 'MANUAL'; export type TshirtSize = 'XS' | 'S' | 'M' | 'L' | 'XL' | 'XXL' | 'XXXL'; export type AvailabilityStatus = 'AVAILABLE' | 'IF_NEEDED' | 'UNAVAILABLE'; export type Role = 'COORDINATOR' | 'LEAD' | 'VOLUNTEER'; export type AuthMethod = 'password' | 'magic_link'; -/** Statuts « actifs » : seuls comptés pour chevauchement / cumul / repos. */ -export const ACTIVE_ASSIGNMENT_STATUSES: AssignmentStatus[] = ['PROPOSED', 'CONFIRMED']; - export type ViolationCode = | 'SHIFT_OVERLAP' | 'OUTSIDE_AVAILABILITY' @@ -149,7 +147,6 @@ export interface AssignmentRead { edition_id: string; volunteer_id: string; shift_id: string; - status: AssignmentStatus; source: AssignmentSource; locked: boolean; created_at: string; @@ -159,13 +156,11 @@ export interface AssignmentRead { export interface AssignmentCreate { volunteer_id: string; shift_id: string; - status?: AssignmentStatus; source?: AssignmentSource; locked?: boolean; } export interface AssignmentUpdate { - status?: AssignmentStatus; source?: AssignmentSource; locked?: boolean; } @@ -194,8 +189,8 @@ export interface MatrixShift { ends_at: string; min_staff: number; ideal_staff: number; - confirmed: number; - proposed: number; + /** Nombre d'affectations sur ce créneau. */ + assigned: number; } export interface MatrixPosition { diff --git a/frontend/src/app/AppShell.tsx b/frontend/src/app/AppShell.tsx index af6b3a8..03daf07 100644 --- a/frontend/src/app/AppShell.tsx +++ b/frontend/src/app/AppShell.tsx @@ -1,7 +1,7 @@ import { NavLink, Outlet } from 'react-router-dom'; import { API_MODE } from '@/api'; -import { useAssignments, useVolunteers } from '@/api/hooks'; +import { useVolunteers } from '@/api/hooks'; import { useAuth } from '@/auth/auth-context'; import { NAV_TABS, canAccess, roleLabelKey } from '@/auth/roles'; import { t } from '@/i18n'; @@ -12,9 +12,7 @@ export function AppShell() { const isCoordinator = account?.role === 'COORDINATOR'; const volunteers = useVolunteers({ enabled: isCoordinator }); - const assignments = useAssignments(undefined, { enabled: isCoordinator }); - const proposedCount = (assignments.data ?? []).filter((a) => a.status === 'PROPOSED').length; const tabs = account ? NAV_TABS.filter((tab) => canAccess(tab.to, account.role)) : []; const initials = account ? account.email.slice(0, 2).toUpperCase() @@ -70,11 +68,6 @@ export function AppShell() { {t('header.mockMode')} ) : null} - {isCoordinator && proposedCount > 0 ? ( -
- {t('header.proposed', { count: proposedCount })} -
- ) : null}
}, { path: 'grille', element: }, { path: 'benevoles', element: }, - { path: 'publication', element: }, { path: 'mobile-benevole', element: }, { path: 'mobile-referent', element: }, { path: 'configuration', element: }, diff --git a/frontend/src/auth/roles.ts b/frontend/src/auth/roles.ts index dbfa58b..c3809ad 100644 --- a/frontend/src/auth/roles.ts +++ b/frontend/src/auth/roles.ts @@ -1,13 +1,13 @@ import type { Role } from '@/api/schema'; // Visibilité des onglets par rôle (cf. demande produit) : -// 01 Grille · 02 Vue par bénévole · 03 Confirmations · 06 Configuration → coordination -// 04 Bénévole · mobile → bénévoles -// 05 Référent · mobile → référents +// 01 Grille · 02 Vue par bénévole · 06 Configuration → coordination +// 04 Bénévole · mobile → bénévoles +// 05 Référent · mobile → référents +// (l'onglet 03 « Confirmations » a disparu avec le statut d'affectation, Étape 9.) export const ROUTE_ROLES: Record = { '/grille': ['COORDINATOR'], '/benevoles': ['COORDINATOR'], - '/publication': ['COORDINATOR'], '/configuration': ['COORDINATOR'], '/mobile-benevole': ['VOLUNTEER'], '/mobile-referent': ['LEAD'], @@ -16,7 +16,6 @@ export const ROUTE_ROLES: Record = { export const NAV_TABS: Array<{ to: string; num: string; labelKey: string }> = [ { to: '/grille', num: '01', labelKey: 'tab.grille' }, { to: '/benevoles', num: '02', labelKey: 'tab.volunteers' }, - { to: '/publication', num: '03', labelKey: 'tab.publication' }, { to: '/mobile-benevole', num: '04', labelKey: 'tab.mobileVolunteer' }, { to: '/mobile-referent', num: '05', labelKey: 'tab.mobileLead' }, { to: '/configuration', num: '06', labelKey: 'tab.config' }, diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index 7c4f905..cec11be 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -50,20 +50,13 @@ "header.title": "GESTION DES BÉNÉVOLES", "header.meta": "BÉNÉVOLAT · {count} INSCRITS", "header.mockMode": "DONNÉES DE DÉMO", - "header.proposed": "{count} PROPOSITION(S)", "tab.grille": "Grille des besoins", "tab.volunteers": "Vue par bénévole", - "tab.publication": "Confirmations", "tab.mobileVolunteer": "Bénévole · mobile", "tab.mobileLead": "Référent · mobile", "tab.config": "Configuration", - "assignment.status.proposed": "Proposée", - "assignment.status.confirmed": "Confirmée", - "assignment.status.declined": "Refusée", - "assignment.status.cancelled": "Annulée", - "position.category.bar": "Bar", "position.category.ticketing": "Billetterie", "position.category.welcome": "Accueil", @@ -98,8 +91,7 @@ "grille.panel.empty.title": "Cliquez une cellule de la grille pour affecter des bénévoles.", "grille.panel.empty.body": "Le panneau s'ouvre ici, sans quitter la grille : candidats triés par charge, alertes bloquantes et avertissements du moteur de validation.", "grille.panel.slotDuration": "CRÉNEAU DE {hours} H", - "grille.panel.confirmed": "CONFIRMÉS", - "grille.panel.proposedCount": "PROPOSÉS {count}", + "grille.panel.assigned": "AFFECTÉS", "grille.panel.min": "MINIMUM {count}", "grille.panel.ideal": "IDÉAL {count}", "grille.panel.assignmentsTitle": "AFFECTATIONS ({count})", @@ -117,8 +109,7 @@ "volunteers.filter.all": "TOUS", "volunteers.filter.overloaded": "SURCHARGÉS", "volunteers.filter.unassigned": "SANS AFFECTATION", - "volunteers.legend.confirmed": "AFFECTATION CONFIRMÉE", - "volunteers.legend.proposed": "AFFECTATION PROPOSÉE", + "volunteers.legend.assigned": "AFFECTATION", "volunteers.count": "{count} BÉNÉVOLES", "volunteers.totalHours": "TOTAL H", "volunteers.name": "NOM", @@ -126,19 +117,7 @@ "volunteers.badge.overloaded": "▲ SURCHARGE", "volunteers.badge.unassigned": "○ OUBLIÉ", "volunteers.empty": "Aucun bénévole ne correspond à ce filtre — bonne nouvelle. Repassez sur « TOUS » pour revoir l'ensemble de l'équipe.", - "volunteers.footnote": "Surcharge = plus de 8 h sur une même journée ou moins de 8 h entre deux créneaux. Oublié = aucune affectation active.", - - "publication.stat.proposed": "AFFECTATIONS PROPOSÉES", - "publication.stat.concerned": "BÉNÉVOLES CONCERNÉS", - "publication.stat.confirmed": "AFFECTATIONS CONFIRMÉES", - "publication.toConfirmTitle": "EN ATTENTE DE CONFIRMATION", - "publication.allConfirmed": "Aucune affectation en attente : tout est confirmé.", - "publication.mailPreviewTitle": "APERÇU DU MESSAGE (MAQUETTE)", - "publication.mailTo": "À : {count} bénévoles concernés", - "publication.mailSubject": "Objet — Ton planning bénévole a été mis à jour", - "publication.mailBody": "Ton planning pour le festival vient d'être mis à jour. Merci de confirmer chaque créneau depuis ton planning personnel.", - "publication.mailCta": "VOIR MON PLANNING →", - "publication.outOfScope": "La publication versionnée et l'envoi d'emails / SMS sont hors périmètre du premier incrément (CLAUDE.md §4). Cet écran liste les affectations « proposées » que les bénévoles n'ont pas encore confirmées.", + "volunteers.footnote": "Surcharge = plus de 8 h sur une même journée ou moins de 8 h entre deux créneaux. Oublié = aucune affectation.", "design.noteTitle": "CHOIX DE CONCEPTION", @@ -147,10 +126,8 @@ "mobileVol.greeting": "SALUT {name}", "mobileVol.slots": "{count} CRÉNEAUX", "mobileVol.hours": "{count} HEURES", - "mobileVol.toConfirm": "⚠ {count} CRÉNEAU(X) À CONFIRMER", "mobileVol.location": "📍 {location}", - "mobileVol.confirm": "JE CONFIRME", - "mobileVol.confirmed": "✓ CRÉNEAU CONFIRMÉ", + "mobileVol.slotConfirmed": "✓ TU ES ATTENDU SUR CE CRÉNEAU", "mobileVol.noSlots": "Aucun créneau pour l'instant. La coordination t'affectera bientôt.", "mobileVol.noAccount": "Ce compte n'est rattaché à aucun bénévole. Contactez la coordination.", "mobileVol.detailsUnavailable": "Le détail des créneaux (poste, horaires) n'est pas encore exposé aux bénévoles par l'API.", diff --git a/frontend/src/lib/planning.ts b/frontend/src/lib/planning.ts index 5fec709..6426784 100644 --- a/frontend/src/lib/planning.ts +++ b/frontend/src/lib/planning.ts @@ -5,23 +5,14 @@ import type { ShiftRead, Violation, } from '@/api/schema'; -import { ACTIVE_ASSIGNMENT_STATUSES } from '@/api/schema'; import { t } from '@/i18n'; import { datePart, hoursBetween, naiveMinutes, timePart } from './time'; -export function isActive(a: AssignmentRead): boolean { - return ACTIVE_ASSIGNMENT_STATUSES.includes(a.status); -} - export function categoryLabel(category: PositionCategory): string { return t(`position.category.${category.toLowerCase()}`); } -export function statusLabel(status: string): string { - return t(`assignment.status.${status.toLowerCase()}`); -} - /** Colonne de la grille : une (date, heure de début) distincte. */ export interface LatticeColumn { key: string; // `YYYY-MM-DD|HH:MM` @@ -63,14 +54,13 @@ export function latticeFromMatrix(matrix: MatrixResponse) { return buildLattice(matrix.positions.flatMap((p) => p.shifts.map((s) => ({ starts_at: s.starts_at })))); } -/** Heures actives cumulées par bénévole. */ +/** Heures cumulées par bénévole. */ export function hoursByVolunteer( assignments: AssignmentRead[], shiftsById: Map>, ): Map { const totals = new Map(); for (const a of assignments) { - if (!isActive(a)) continue; const shift = shiftsById.get(a.shift_id); if (!shift) continue; totals.set( @@ -88,7 +78,7 @@ export function isOverloaded( shiftsById: Map>, ): boolean { const shifts = assignments - .filter((a) => a.volunteer_id === volunteerId && isActive(a)) + .filter((a) => a.volunteer_id === volunteerId) .map((a) => shiftsById.get(a.shift_id)) .filter((s): s is ShiftRead => Boolean(s)) .sort((a, b) => a.starts_at.localeCompare(b.starts_at)); diff --git a/frontend/src/pages/GrillePage.tsx b/frontend/src/pages/GrillePage.tsx index 54fbd26..1d5ba8d 100644 --- a/frontend/src/pages/GrillePage.tsx +++ b/frontend/src/pages/GrillePage.tsx @@ -210,7 +210,7 @@ function PositionRow({ if (!shift) { return
; } - const assigned = shift.confirmed + shift.proposed; + const assigned = shift.assigned; const isCrit = assigned < shift.min_staff; const isInc = !isCrit && assigned < shift.ideal_staff; const glyph = isCrit ? '▲' : isInc ? '◧' : '✓'; @@ -225,12 +225,9 @@ function PositionRow({ isInc && 'bg-yellow-soft hover:bg-yellow', !isCrit && !isInc && 'bg-paper hover:bg-green/10', )} - title={`${shift.confirmed} confirmé(s) · ${shift.proposed} proposé(s) · min ${shift.min_staff} · idéal ${shift.ideal_staff}`} + title={`${assigned} affecté(s) · min ${shift.min_staff} · idéal ${shift.ideal_staff}`} > - {shift.confirmed} - {shift.proposed > 0 ? ( - +{shift.proposed} - ) : null} + {assigned} /{shift.ideal_staff} {glyph} diff --git a/frontend/src/pages/MobileLeadPage.tsx b/frontend/src/pages/MobileLeadPage.tsx index bc8951a..f27f646 100644 --- a/frontend/src/pages/MobileLeadPage.tsx +++ b/frontend/src/pages/MobileLeadPage.tsx @@ -5,7 +5,6 @@ import { useAuth } from '@/auth/auth-context'; import { ErrorNote, Loading } from '@/components/state'; import { DesignNote, PhoneFrame } from '@/pages/mobile/PhoneFrame'; import { hourRangeLabel } from '@/lib/time'; -import { isActive, statusLabel } from '@/lib/planning'; import { t } from '@/i18n'; import { cx } from '@/lib/cx'; @@ -40,20 +39,17 @@ export function MobileLeadPage() { if (error) return ; if (!position || !shift) return null; - const team = (shiftAssignments.data ?? []) - .filter(isActive) - .map((a) => { - const volunteer = volunteersById.get(a.volunteer_id); - return { - id: a.id, - name: volunteer ? `${volunteer.first_name} ${volunteer.last_name}` : a.volunteer_id, - initials: volunteer ? `${volunteer.first_name[0]}${volunteer.last_name[0]}` : '?', - phone: volunteer?.phone ?? '', - status: statusLabel(a.status), - present: presence[a.id] === 'present', - absent: presence[a.id] === 'absent', - }; - }); + const team = (shiftAssignments.data ?? []).map((a) => { + const volunteer = volunteersById.get(a.volunteer_id); + return { + id: a.id, + name: volunteer ? `${volunteer.first_name} ${volunteer.last_name}` : a.volunteer_id, + initials: volunteer ? `${volunteer.first_name[0]}${volunteer.last_name[0]}` : '?', + phone: volunteer?.phone ?? '', + present: presence[a.id] === 'present', + absent: presence[a.id] === 'absent', + }; + }); const presentCount = team.filter((m) => m.present).length; const absentCount = team.filter((m) => m.absent).length; @@ -110,7 +106,7 @@ export function MobileLeadPage() { {member.name} - {member.status} · {member.phone} + {member.phone} {member.present ? ( diff --git a/frontend/src/pages/MobileVolunteerPage.tsx b/frontend/src/pages/MobileVolunteerPage.tsx index d5460e2..528b463 100644 --- a/frontend/src/pages/MobileVolunteerPage.tsx +++ b/frontend/src/pages/MobileVolunteerPage.tsx @@ -1,18 +1,11 @@ import { useMemo } from 'react'; import { ApiError } from '@/api'; -import { - useAssignments, - usePositions, - useShifts, - useUpdateAssignment, - useVolunteer, -} from '@/api/hooks'; +import { useAssignments, usePositions, useShifts, useVolunteer } from '@/api/hooks'; import { useAuth } from '@/auth/auth-context'; import { ErrorNote, Loading } from '@/components/state'; import { DesignNote, PhoneFrame } from '@/pages/mobile/PhoneFrame'; import { hourRangeLabel, hoursBetween, longDayLabel, naiveMinutes } from '@/lib/time'; -import { isActive } from '@/lib/planning'; import { t } from '@/i18n'; export function MobileVolunteerPage() { @@ -25,7 +18,6 @@ export function MobileVolunteerPage() { }); const shifts = useShifts(); const positions = usePositions(); - const updateAssignment = useUpdateAssignment(); const shiftsById = useMemo( () => new Map((shifts.data ?? []).map((s) => [s.id, s])), @@ -54,7 +46,6 @@ export function MobileVolunteerPage() { (positions.error instanceof ApiError && positions.error.status === 403); const slots = (assignments.data ?? []) - .filter(isActive) .map((a) => ({ assignment: a, shift: shiftsById.get(a.shift_id) ?? null })) .sort((a, b) => { const av = a.shift ? naiveMinutes(a.shift.starts_at) : 0; @@ -69,7 +60,6 @@ export function MobileVolunteerPage() { 0, ) * 10, ) / 10; - const toConfirm = slots.filter((s) => s.assignment.status !== 'CONFIRMED').length; return (
@@ -91,12 +81,6 @@ export function MobileVolunteerPage() {
- {toConfirm > 0 ? ( -
- {t('mobileVol.toConfirm', { count: toConfirm })} -
- ) : null} -
{detailsUnavailable ? (
@@ -128,28 +112,9 @@ export function MobileVolunteerPage() { {t('mobileVol.location', { location: position.location })}
) : null} - {assignment.status === 'CONFIRMED' ? ( -
- {t('mobileVol.confirmed')} -
- ) : ( - - )} - {updateAssignment.error instanceof ApiError && - updateAssignment.variables?.id === assignment.id ? ( -
- {t(updateAssignment.error.code)} -
- ) : null} +
+ {t('mobileVol.slotConfirmed')} +
); diff --git a/frontend/src/pages/PublicationPage.tsx b/frontend/src/pages/PublicationPage.tsx deleted file mode 100644 index 7974846..0000000 --- a/frontend/src/pages/PublicationPage.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { useMemo } from 'react'; - -import { useAssignments, usePositions, useShifts, useVolunteers } from '@/api/hooks'; -import { Panel, PanelHeader, StatCard } from '@/components/ui'; -import { ErrorNote, Loading } from '@/components/state'; -import { hourRangeLabel, shortDayLabel } from '@/lib/time'; -import { t } from '@/i18n'; - -export function PublicationPage() { - const assignments = useAssignments(); - const volunteers = useVolunteers(); - const shifts = useShifts(); - const positions = usePositions(); - - const shiftsById = useMemo( - () => new Map((shifts.data ?? []).map((s) => [s.id, s])), - [shifts.data], - ); - const positionsById = useMemo( - () => new Map((positions.data ?? []).map((p) => [p.id, p])), - [positions.data], - ); - const volunteersById = useMemo( - () => new Map((volunteers.data ?? []).map((v) => [v.id, v])), - [volunteers.data], - ); - - const rows = useMemo(() => { - return (assignments.data ?? []) - .filter((a) => a.status === 'PROPOSED') - .map((a) => { - const shift = shiftsById.get(a.shift_id); - const position = shift ? positionsById.get(shift.position_id) : undefined; - const volunteer = volunteersById.get(a.volunteer_id); - return { - id: a.id, - volunteerName: volunteer ? `${volunteer.first_name} ${volunteer.last_name}` : a.volunteer_id, - positionName: position?.name ?? '', - label: shift ? `${shortDayLabel(shift.starts_at)} · ${hourRangeLabel(shift.starts_at, shift.ends_at)}` : '', - }; - }) - .sort((a, b) => a.volunteerName.localeCompare(b.volunteerName, 'fr')); - }, [assignments.data, shiftsById, positionsById, volunteersById]); - - if (assignments.isLoading || shifts.isLoading) return ; - if (assignments.error) return ; - - const confirmed = (assignments.data ?? []).filter((a) => a.status === 'CONFIRMED').length; - const concerned = new Set(rows.map((r) => r.volunteerName)).size; - - return ( -
-
-
- - - -
- - - {t('publication.toConfirmTitle')} - {rows.length === 0 ? ( -
- - ✓ - - - {t('publication.allConfirmed')} - -
- ) : ( - rows.map((row) => ( -
- - {t('assignment.status.proposed')} - - - {row.volunteerName} - - {row.positionName} · {row.label} - - -
- )) - )} -
-
- -
- - {t('publication.mailPreviewTitle')} -
-
- {t('publication.mailTo', { count: concerned })} -
-
- {t('publication.mailSubject')} -
-

{t('publication.mailBody')}

-
- {t('publication.mailCta')} -
-
-
- -
- {t('publication.outOfScope')} -
-
-
- ); -} diff --git a/frontend/src/pages/VolunteerViewPage.tsx b/frontend/src/pages/VolunteerViewPage.tsx index d6a0776..9c46856 100644 --- a/frontend/src/pages/VolunteerViewPage.tsx +++ b/frontend/src/pages/VolunteerViewPage.tsx @@ -4,7 +4,7 @@ import { useAssignments, usePositions, useShifts, useVolunteers } from '@/api/ho import type { AssignmentRead, ShiftRead, VolunteerRead } from '@/api/schema'; import { Chip } from '@/components/ui'; import { ErrorNote, Loading } from '@/components/state'; -import { buildLattice, hoursByVolunteer, isActive, isOverloaded } from '@/lib/planning'; +import { buildLattice, hoursByVolunteer, isOverloaded } from '@/lib/planning'; import { datePart, shortDayLabel, timePart } from '@/lib/time'; import { t } from '@/i18n'; import { cx } from '@/lib/cx'; @@ -39,7 +39,6 @@ export function VolunteerViewPage() { const all = assignments.data ?? []; const byVolunteer = new Map(); for (const a of all) { - if (!isActive(a)) continue; const list = byVolunteer.get(a.volunteer_id) ?? []; list.push(a); byVolunteer.set(a.volunteer_id, list); @@ -92,10 +91,7 @@ export function VolunteerViewPage() {
{t('legend.title')} - {t('volunteers.legend.confirmed')} - - {t('volunteers.legend.proposed')} - + {t('volunteers.legend.assigned')}
@@ -206,24 +202,12 @@ function VolunteerRow({ {columns.map((column) => { const cell = row.cellByKey.get(column.key); if (!cell) return
; - if (cell.status === 'CONFIRMED') { - return ( -
- {abbr(cell.shift.position_id)} -
- ); - } return (
- - {abbr(cell.shift.position_id)} - + {abbr(cell.shift.position_id)}
); })} diff --git a/frontend/src/pages/grille/AssignmentPanel.tsx b/frontend/src/pages/grille/AssignmentPanel.tsx index 5c521f4..78fb31c 100644 --- a/frontend/src/pages/grille/AssignmentPanel.tsx +++ b/frontend/src/pages/grille/AssignmentPanel.tsx @@ -7,14 +7,13 @@ import { useDeleteAssignment, usePositions, useShifts, - useUpdateAssignment, useValidateAssignment, useVolunteers, } from '@/api/hooks'; import type { MatrixShift, PositionCategory, VolunteerRead } from '@/api/schema'; -import { Badge, Btn } from '@/components/ui'; +import { Btn } from '@/components/ui'; import { Loading } from '@/components/state'; -import { categoryLabel, hoursByVolunteer, isActive, violationText } from '@/lib/planning'; +import { categoryLabel, hoursByVolunteer, violationText } from '@/lib/planning'; import { hourRangeLabel, hoursBetween, shortDayLabel } from '@/lib/time'; import { t } from '@/i18n'; @@ -41,7 +40,6 @@ export function AssignmentPanel({ const shifts = useShifts(); const positions = usePositions(); const deleteAssignment = useDeleteAssignment(); - const updateAssignment = useUpdateAssignment(); const shiftsById = useMemo( () => new Map((shifts.data ?? []).map((s) => [s.id, s])), @@ -58,7 +56,7 @@ export function AssignmentPanel({ const slotsByVolunteer = useMemo(() => { const map = new Map(); for (const a of allAssignments.data ?? []) { - if (isActive(a)) map.set(a.volunteer_id, (map.get(a.volunteer_id) ?? 0) + 1); + map.set(a.volunteer_id, (map.get(a.volunteer_id) ?? 0) + 1); } return map; }, [allAssignments.data]); @@ -114,11 +112,7 @@ export function AssignmentPanel({
- {t('grille.panel.confirmed')}{' '} - {shift.confirmed} - - - {t('grille.panel.proposedCount', { count: shift.proposed })} + {t('grille.panel.assigned')} {shift.assigned} {t('grille.panel.min', { count: shift.min_staff })} {t('grille.panel.ideal', { count: shift.ideal_staff })} @@ -149,19 +143,6 @@ export function AssignmentPanel({ {t('grille.panel.hoursTotal', { hours: round(hours.get(volunteer.id) ?? 0) })} - {assignment.status === 'CONFIRMED' ? ( - {t('assignment.status.confirmed')} - ) : ( - - )}