Compléter les vues de planning
Deux des cinq vues de §9 manquaient. La vue des présences répond à une question que les quatre autres posent mal : qui est là aujourd'hui. La grille montre des créneaux, pas des personnes ; il fallait la lire colonne par colonne et compter de tête. Le planning à afficher est une mise en page pour la feuille punaisée en salle de pause — toutes les équipes à la suite, sans panneau d'édition ni compteurs, un aplat par créneau avec l'horaire et le code du poste écrits. Sans la capacité de voir le non publié, la feuille ne la contourne pas : imprimer un brouillon revient à le publier sur un mur. La règle « absent ou présent » part dans le domaine plutôt que dans les écrans. Les deux vues en tirent la même conclusion, et un export futur en tirera la même : deux implémentations finiraient par se contredire sur le seul cas qui compte, celui du congé validé après la construction de la semaine. Les créneaux restent alors au planning, et l'absence l'emporte — afficher « présent » enverrait un responsable chercher en rayon quelqu'un qui est chez lui. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
98b289784a
commit
e4be93a208
5 files changed
+665
No files matched your search
@@ -0,0 +1,111 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { BoardRow } from '@/domain/planning/board';
|
||||
import {
|
||||
absenceOnDay,
|
||||
dayState,
|
||||
headcountByDay,
|
||||
} from '@/domain/planning/presence';
|
||||
|
||||
function row(overrides: Partial<BoardRow> = {}): BoardRow {
|
||||
return {
|
||||
membershipId: 'm1',
|
||||
firstName: 'Camille',
|
||||
lastName: 'Ferrand',
|
||||
job: 'Vendeuse',
|
||||
forfaitJours: false,
|
||||
counters: {
|
||||
contractMinutes: 0,
|
||||
plannedMinutes: 0,
|
||||
absenceMinutes: 0,
|
||||
sundaysWorked: 0,
|
||||
restDays: 0,
|
||||
},
|
||||
days: [[], [], [], [], [], [], []],
|
||||
absences: [],
|
||||
unassigned: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const shift: BoardRow['days'][number][number] = {
|
||||
id: 's1',
|
||||
poste: 'cai',
|
||||
time: '09:00–17:00',
|
||||
minutes: 480,
|
||||
breakMinutes: 30,
|
||||
state: 'published',
|
||||
note: null,
|
||||
};
|
||||
|
||||
describe('dayState', () => {
|
||||
it('dit « repos » sur une case vide', () => {
|
||||
expect(dayState(row(), 0)).toBe('off');
|
||||
});
|
||||
|
||||
it('dit « présent » quand la case porte un créneau', () => {
|
||||
const days: BoardRow['days'] = [[shift], [], [], [], [], [], []];
|
||||
expect(dayState(row({ days }), 0)).toBe('present');
|
||||
});
|
||||
|
||||
it('fait primer l’absence sur le créneau resté au planning', () => {
|
||||
// Le cas qui compte : le congé est validé après la construction de la
|
||||
// semaine, les créneaux n'ont pas été retirés. Afficher « présent »
|
||||
// enverrait un responsable chercher quelqu'un qui est chez lui.
|
||||
const days: BoardRow['days'] = [[shift], [], [], [], [], [], []];
|
||||
const absences = [
|
||||
{ label: 'Congé payé', colorKey: 'neutral', startDay: 0, span: 2 },
|
||||
];
|
||||
expect(dayState(row({ days, absences }), 0)).toBe('absent');
|
||||
});
|
||||
});
|
||||
|
||||
describe('absenceOnDay', () => {
|
||||
const absences = [
|
||||
{ label: 'Congé payé', colorKey: 'neutral', startDay: 2, span: 3 },
|
||||
];
|
||||
|
||||
it('couvre le premier et le dernier jour de la plage', () => {
|
||||
// La borne haute est le dernier jour d'absence, pas le jour de reprise :
|
||||
// `startDay + span` est donc exclu.
|
||||
expect(absenceOnDay(row({ absences }), 2)?.label).toBe('Congé payé');
|
||||
expect(absenceOnDay(row({ absences }), 4)?.label).toBe('Congé payé');
|
||||
});
|
||||
|
||||
it('ne déborde ni avant ni après', () => {
|
||||
expect(absenceOnDay(row({ absences }), 1)).toBeUndefined();
|
||||
expect(absenceOnDay(row({ absences }), 5)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('headcountByDay', () => {
|
||||
it('compte présents, absents et repos sur chaque colonne', () => {
|
||||
const present = row({
|
||||
membershipId: 'a',
|
||||
days: [[shift], [shift], [], [], [], [], []],
|
||||
});
|
||||
const absent = row({
|
||||
membershipId: 'b',
|
||||
absences: [
|
||||
{ label: 'Maladie', colorKey: 'neutral', startDay: 0, span: 1 },
|
||||
],
|
||||
});
|
||||
|
||||
const tally = headcountByDay([present, absent], 7);
|
||||
|
||||
expect(tally[0]).toEqual({ present: 1, absent: 1, off: 0 });
|
||||
expect(tally[1]).toEqual({ present: 1, absent: 0, off: 1 });
|
||||
expect(tally[6]).toEqual({ present: 0, absent: 0, off: 2 });
|
||||
});
|
||||
|
||||
it('écarte la ligne des besoins non couverts', () => {
|
||||
// Elle ne porte personne : la compter en repos gonflerait un effectif qui
|
||||
// n'existe pas.
|
||||
const unassigned = row({ membershipId: null, unassigned: true });
|
||||
expect(headcountByDay([unassigned], 7)[0]).toEqual({
|
||||
present: 0,
|
||||
absent: 0,
|
||||
off: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user