Le planning quitte le module de démonstration : la grille lit `WeeklySchedule` et `Shift`, et les quatre derniers fichiers de `src/lib/demo/` qui portaient des salariés fictifs disparaissent. Modèle - `TeamMember` : rattachement d'un salarié à une équipe, distinct de `MembershipScope` (qui dit ce qu'un manager a le droit de voir). Sans lui, un salarié sans créneau n'apparaîtrait pas dans la grille — c'est-à-dire l'état de départ de toute semaine en construction. - RLS sur `WeeklySchedule`, `Shift`, `Rest`, `DailyNote` et `TeamMember`. Temps - `src/domain/planning/week.ts` : repérage par couple année ISO + semaine ISO, jamais par date de début. Le lundi 29 décembre 2025 appartient à la semaine 1 de 2026 ; une clé fondée sur la date ferait apparaître deux semaines 1. - `zonedInstant` / `zonedMidnight` corrigent le décalage mesuré **à l'instant visé**. Le 29 mars 2026, minuit est en UTC+1 et 09 h en UTC+2 : ajouter neuf heures à minuit donnerait 10 h locales. - La semaine du retour à l'heure d'hiver dure 169 h, celle du passage à l'heure d'été 167 — vérifié par test. Écritures - Création, modification, suppression de créneau ; publication et dépublication **par équipe**, avec verrou optimiste sur `version` : deux managers sur la même grille est le cas normal, pas l'exception. - Chevauchement refusé en transaction, pas seulement dans le formulaire. - Modifier une semaine publiée exige `planning.edit_published`, capacité que le rôle manager n'a pas : un salarié a organisé sa semaine sur ce qu'il a lu. - Toute mutation laisse une entrée d'audit ; la suppression écrit sa trace **avant** l'effacement, sinon l'état supprimé serait perdu. Lecture - `planning.view_unpublished` filtre en base : sans cette capacité, les brouillons ne sont pas chargés du tout. Un test vérifie que les horaires n'apparaissent pas dans le HTML servi — un masquage CSS les y laisserait. - Vue jour reconstruite sur les mêmes données, amplitude déduite de la journée réelle plutôt que figée à 06 h–21 h. Vérification - 26 tests unitaires sur le repérage des semaines et la mise en grille. - Parcours e2e : poser un créneau, refus de chevauchement, publier, dépublier ; et ce que voient un salarié et un manager sur la même semaine. - `scripts/dev-db.sh` : la base de développement est éphémère dans cet environnement, la remonter ne doit pas être une redécouverte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
97 lines
4.1 KiB
TypeScript
97 lines
4.1 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
|
|
/**
|
|
* Ce test se connecte en manager : il ne peut donc pas réutiliser la session
|
|
* partagée de la direction, d'où le projet « anonyme ».
|
|
*/
|
|
async function signIn(page: import('@playwright/test').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();
|
|
await expect(page.getByRole('heading', { name: 'Aperçu RH' })).toBeVisible();
|
|
}
|
|
|
|
test('la direction lit et alimente le registre de paramétrage', async ({
|
|
page,
|
|
}) => {
|
|
await signIn(page, 'direction@example.test');
|
|
await page.goto('/reglages/registre');
|
|
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Registre de paramétrage juridique' }),
|
|
).toBeVisible();
|
|
|
|
const parameter = `Durée quotidienne maximale ${Date.now()}`;
|
|
// Ciblage par attribut `name` : les libellés portent un texte d'aide, et
|
|
// celui de « Source » contient lui-même le mot « valeur », ce qui rend la
|
|
// correspondance par libellé ambiguë.
|
|
const form = page.locator('form').filter({ hasText: 'Consigner' });
|
|
await form.locator('input[name="key"]').fill(parameter);
|
|
await form.locator('input[name="value"]').fill('10 h');
|
|
await form
|
|
.locator('input[name="source"]')
|
|
.fill('IDCC 1517 — texte consolidé Legifrance');
|
|
await form.locator('input[name="population"]').fill('Tous les salariés');
|
|
await page.getByRole('button', { name: 'Consigner' }).click();
|
|
|
|
const row = page.getByRole('row', { name: new RegExp(parameter) });
|
|
await expect(row).toBeVisible();
|
|
|
|
// Consigné n'est pas approuvé : la matrice exige un approbateur nommé.
|
|
await row.getByRole('button', { name: 'Approuver' }).click();
|
|
await expect(row.getByText(/Approuvé le/)).toBeVisible();
|
|
});
|
|
|
|
test('un manager ne peut ni voir ni modifier les établissements', async ({
|
|
page,
|
|
}) => {
|
|
await signIn(page, 'manager.nantes@example.test');
|
|
|
|
// La barre latérale ne propose pas la section, mais c'est un confort :
|
|
// le contrôle qui compte est celui du serveur, testé en accédant à l'URL.
|
|
await page.goto('/reglages/etablissements');
|
|
|
|
// Le refus se manifeste par une erreur serveur, pas par une page qui
|
|
// s'affiche à moitié : la lecture elle-même est refusée.
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Établissements' }),
|
|
).toBeHidden();
|
|
});
|
|
|
|
test('un salarié sans compte applicatif est créable', async ({ page }) => {
|
|
await signIn(page, 'direction@example.test');
|
|
await page.goto('/equipe');
|
|
|
|
await expect(page.getByRole('heading', { name: 'Équipe' })).toBeVisible();
|
|
// L'effectif vient de la base, pas du module de démonstration.
|
|
await expect(page.getByText('E0001')).toBeVisible();
|
|
|
|
// Nom unique par exécution : la base de test n'est pas remise à zéro entre
|
|
// deux passages, et un nom fixe finirait par désigner plusieurs salariés.
|
|
const matricule = `E9${Date.now() % 100000}`;
|
|
const nom = `Sanscompte${matricule}`;
|
|
const form = page.locator('form').filter({ hasText: 'Ajouter' });
|
|
await form.locator('input[name="firstName"]').fill('Sans');
|
|
await form.locator('input[name="lastName"]').fill(nom);
|
|
await form.locator('input[name="employeeNumber"]').fill(matricule);
|
|
await page.getByRole('button', { name: 'Ajouter' }).click();
|
|
await expect(page.getByText('Salarié ajouté.')).toBeVisible();
|
|
|
|
// Rechargement explicite : ce qui est vérifié ici est la persistance et la
|
|
// présence dans l'annuaire, pas le moment exact où la revalidation atteint
|
|
// le rendu courant.
|
|
await page.reload();
|
|
|
|
// Un salarié sans adresse doit exister : la plupart des équipes de vente ne
|
|
// se connectent jamais à l'outil.
|
|
await expect(
|
|
page.getByRole('cell', { name: matricule, exact: true }),
|
|
).toBeVisible();
|
|
// Le nom vit sur le dossier, pas sur le compte : un salarié sans accès
|
|
// applicatif doit tout de même figurer nommément au registre du personnel.
|
|
await expect(
|
|
page.getByRole('link', { name: new RegExp(`Sans ${nom}`) }),
|
|
).toBeVisible();
|
|
});
|