`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
92 lines
3.6 KiB
TypeScript
92 lines
3.6 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test';
|
||
|
||
import {
|
||
formatWeekParam,
|
||
isoWeekOf,
|
||
previousIsoWeek,
|
||
} from '../../src/domain/planning/week';
|
||
|
||
/**
|
||
* Ce que chaque rôle voit du planning.
|
||
*
|
||
* Ces tests se connectent eux-mêmes : ils ne peuvent pas réutiliser la session
|
||
* partagée de la direction, d'où le projet « anonyme ».
|
||
*/
|
||
|
||
async function signIn(page: Page, email: string) {
|
||
await page.goto('/connexion');
|
||
await page.getByLabel('Adresse électronique').fill(email);
|
||
await page.getByLabel('Mot de passe').fill('planflow-demo-2026');
|
||
await page.getByRole('button', { name: 'Se connecter' }).click();
|
||
// Attendre la sortie de l'écran de connexion : naviguer tout de suite
|
||
// annulerait la redirection en cours, et le test repartirait déconnecté.
|
||
await expect(page).not.toHaveURL(/\/connexion/);
|
||
}
|
||
|
||
const current = isoWeekOf(new Date());
|
||
const CURRENT_WEEK = formatWeekParam(current);
|
||
const PUBLISHED_WEEK = formatWeekParam(previousIsoWeek(current));
|
||
|
||
test('un salarié ne voit pas une semaine non publiée', async ({ page }) => {
|
||
await signIn(page, 'salarie@example.test');
|
||
|
||
// Le seed laisse la semaine courante en brouillon et publie la précédente.
|
||
await page.goto(`/planning/semaine?semaine=${CURRENT_WEEK}`);
|
||
await expect(page.getByText(/n’est pas encore publié/).first()).toBeVisible();
|
||
|
||
// Le masquage n'est pas cosmétique : les créneaux ne sont pas chargés, donc
|
||
// absents du HTML. Un simple `display: none` les y laisserait.
|
||
expect(await page.content()).not.toContain('09:00–17:00');
|
||
|
||
// Ni bouton de publication ni ajout de créneau : les capacités manquent.
|
||
await expect(page.getByRole('button', { name: 'Publier' })).toHaveCount(0);
|
||
await expect(page.getByRole('button', { name: /\+ Créneau/ })).toHaveCount(0);
|
||
});
|
||
|
||
test('un salarié voit la semaine publiée', async ({ page }) => {
|
||
await signIn(page, 'salarie@example.test');
|
||
await page.goto(`/planning/semaine?semaine=${PUBLISHED_WEEK}`);
|
||
|
||
await expect(page.getByText('Publiée').first()).toBeVisible();
|
||
await expect(page.getByText('09:00–17:00').first()).toBeVisible();
|
||
});
|
||
|
||
test('un manager voit le brouillon et peut le publier', async ({ page }) => {
|
||
await signIn(page, 'manager.nantes@example.test');
|
||
await page.goto(`/planning/semaine?semaine=${CURRENT_WEEK}`);
|
||
|
||
await expect(page.getByText('Brouillon').first()).toBeVisible();
|
||
await expect(page.getByRole('button', { name: 'Publier' }).first()).toBeVisible();
|
||
});
|
||
|
||
test('un salarié voit un accueil adapté, pas un tableau de bord vide', async ({
|
||
page,
|
||
}) => {
|
||
await signIn(page, 'salarie@example.test');
|
||
await page.goto('/');
|
||
|
||
// Lui servir une erreur d'autorisation sur la page d'accueil serait absurde ;
|
||
// lui montrer des indicateurs vides le serait tout autant.
|
||
await expect(page.getByRole('heading', { name: 'Aperçu' })).toBeVisible();
|
||
await expect(page.getByText('Mon planning')).toBeVisible();
|
||
await expect(page.getByText('Effectif en fin de mois')).toHaveCount(0);
|
||
await expect(page.getByText('Rotation')).toHaveCount(0);
|
||
});
|
||
|
||
test('un manager ne voit que les établissements de son périmètre', async ({
|
||
page,
|
||
}) => {
|
||
await signIn(page, 'manager.nantes@example.test');
|
||
await page.goto('/');
|
||
|
||
// Le périmètre s'applique avant l'agrégation : les mouvements d'un autre
|
||
// établissement ne doivent pas transparaître, même fondus dans un total.
|
||
await expect(page.getByRole('heading', { name: 'Aperçu RH' })).toBeVisible();
|
||
await expect(
|
||
page.getByRole('link', { name: 'Nantes Atlantis', exact: true }),
|
||
).toBeVisible();
|
||
await expect(
|
||
page.getByRole('link', { name: 'Rennes Alma', exact: true }),
|
||
).toHaveCount(0);
|
||
});
|