Files
planflow/tests/e2e/apercu.spec.ts
T
Claude 10a9c923e2 WP-09 : tableau de bord RH, et disparition des données de démonstration
`src/lib/demo` n'existe plus. Aucun écran de PlanFlow ne lit désormais autre
chose que la base.

Un indicateur doit être explicable
Chaque tuile est un lien vers ses lignes sources : profils incomplets, fins de
période d'essai, titres de séjour, entrées, sorties, avenants, journal des
absences. Un chiffre qu'on ne peut pas ouvrir ne se corrige pas — il se
conteste. Et chaque ligne mène à la fiche du salarié.

Les manques sont **nommés**, pas comptés : « 6 profils incomplets » n'aide
personne à agir, « il manque l'IBAN de trois salariés » se règle en un message.
Le NIR et l'IBAN sont contrôlés par la présence de leur colonne chiffrée, jamais
déchiffrés — savoir qu'une valeur existe n'exige pas de la lire.

Des chiffres qui refusent de mentir
- La rotation moyenne entrées et sorties : compter seulement les départs
  sous-estime la rotation d'une équipe qui recrute autant qu'elle perd.
- Sur un effectif nul, elle rend `—` et non « 0 % ». Zéro pour cent de rotation
  sur un établissement vide est une affirmation fausse, pas une absence de
  mouvement.
- L'absentéisme se rapporte aux jours **théoriquement travaillés**, pas aux
  jours calendaires : rapporter à 30 jours ferait passer un problème réel pour
  du bruit.
- Un taux horaire absent vaut zéro dans le coût, jamais une estimation :
  afficher un coût inventé serait pire qu'un coût partiel.

Les échéances remontent avant de tomber
Périodes d'essai à 45 jours, titres de séjour à 90. Les échéances **dépassées**
sont conservées et placées en tête : une période d'essai qu'on a laissé filer
est plus urgente qu'une échéance à venir, et la masquer parce qu'elle est passée
est précisément ce qui la rend coûteuse.

Périmètre et confidentialité
Le filtrage par établissement s'applique **avant** l'agrégation : les mouvements
d'un autre établissement ne transparaissent pas, même fondus dans un total. Le
journal des absences ne porte jamais le motif médical — un tableau de bord n'en
a aucun besoin.

Un salarié, qui n'a pas accès à l'annuaire, reçoit un accueil adapté plutôt
qu'une erreur d'autorisation ou un tableau de bord vide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
2026-08-08 12:40:19 +00:00

86 lines
2.5 KiB
TypeScript

import { expect, test } from '@playwright/test';
/**
* Tableau de bord RH.
*
* Le critère qui compte : **un indicateur doit être explicable**. Un chiffre
* qu'on ne peut pas ouvrir ne se corrige pas, il se conteste.
*/
test('chaque tuile mène à ses lignes sources', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Aperçu RH' })).toBeVisible();
// Les tuiles sont des liens, pas des chiffres décoratifs. On vise celle du
// contenu, pas l'entrée homonyme de la barre latérale.
const tile = page
.locator('main')
.getByRole('link', { name: /Profils incomplets/ })
.first();
await tile.click();
await expect(page).toHaveURL(/liste=profils/);
await expect(
page.locator('main').getByText('Profils incomplets').first(),
).toBeVisible();
});
test('la liste nomme ce qui manque plutôt que de le compter', async ({
page,
}) => {
await page.goto('/?liste=profils');
// « 6 profils incomplets » n'aide personne à agir ; « il manque l'IBAN » se
// règle en un message.
await expect(page.getByText(/Manque :/).first()).toBeVisible();
});
test('les indicateurs agrégés sont affichés', async ({ page }) => {
await page.goto('/');
await expect(page.getByText('Effectif en fin de mois')).toBeVisible();
await expect(page.getByText('Rotation')).toBeVisible();
await expect(page.getByText('Absentéisme')).toBeVisible();
await expect(page.getByText('Heures planifiées')).toBeVisible();
});
test('chaque ligne mène à la fiche du salarié', async ({ page }) => {
await page.goto('/?liste=profils');
const first = page
.locator('section')
.filter({ hasText: 'Profils incomplets' })
.locator('li a')
.first();
if (await first.isVisible()) {
await expect(first).toHaveAttribute('href', /\/equipe\//);
}
});
test('la navigation entre listes conserve le mois et l’établissement', async ({
page,
}) => {
await page.goto('/');
await page
.locator('main')
.getByRole('link', { name: '← Mois précédent' })
.click();
// Attendre la navigation avant de lire l'URL : la lire tout de suite rendrait
// celle d'avant le clic.
await expect(page).toHaveURL(/mois=/);
const month = new URL(page.url()).searchParams.get('mois');
expect(month).toBeTruthy();
await page
.locator('main')
.getByRole('link', { name: 'Journal des absences' })
.first()
.click();
// Changer de liste ne doit pas ramener au mois courant : on perdrait le
// contexte à chaque clic.
await expect(page).toHaveURL(new RegExp(`mois=${month}`));
});