Files
planflow/tests/unit/planning-presence.test.ts
T
MichaelandClaude Opus 5 5f656d05af Corriger le bandeau de compteurs, et régler impression et productivité
Le bandeau de la grille affichait contrat, planifié, écart, dimanches et jours
de repos. PLAN.md §7.4 en demande cinq autres : heures contractuelles,
planifié, absences, écart, repos compensateur. La capture du produit audité le
confirme.

L'écart n'était pas cosmétique. Les dimanches travaillés et les jours de repos
se lisent déjà sur la grille, colonne par colonne. Le repos compensateur, lui,
ne se lit nulle part ailleurs : c'est une contrepartie due — au dimanche
travaillé, au dépassement de contingent — et l'omettre du bandeau masquait la
seule valeur qui dit qu'une dette existe.

Le compteur RC n'était alimenté par rien : les repos posés n'étaient tout
simplement pas chargés avec la semaine. Ils le sont, et seul le repos
compensateur y entre — le repos hebdomadaire est un droit déjà pris, les
additionner ferait disparaître la dette dans un total qui ne veut rien dire.

Deux écrans de réglages complètent le lot. L'impression gouverne le planning
affiché en salle : orientation, densité, totaux, dimanche, colonne
d'émargement. Cette dernière porte un avertissement — un émargement papier ne
vaut pas décompte du temps de travail, et le décompte reste celui de PlanFlow.

L'objectif de productivité est nullable et non « zéro par défaut » : zéro est un
objectif inatteignable, l'absence de valeur dit qu'aucun objectif n'a été fixé.
Vider le champ efface donc l'objectif au lieu de le mettre à zéro. L'écran
rappelle aussi ce que l'indicateur n'est pas : il éclaire une décision
d'organisation, il ne mesure pas le travail d'une personne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 13:18:31 +02:00

113 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
compensatoryRestMinutes: 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,
});
});
});