diff --git a/prisma/migrations/20260811140000_print_productivity/migration.sql b/prisma/migrations/20260811140000_print_productivity/migration.sql new file mode 100644 index 0000000..3e71299 --- /dev/null +++ b/prisma/migrations/20260811140000_print_productivity/migration.sql @@ -0,0 +1,17 @@ +-- --------------------------------------------------------------------------- +-- Impression et productivité — PLAN.md §9, `/reglages/impression` et +-- `/reglages/productivite`. +-- +-- `productivityTargetPerHour` est nullable et non « 0 par défaut » : zéro est un +-- objectif — inatteignable — alors que l'absence de valeur dit qu'aucun objectif +-- n'a été fixé. Un écran qui affiche « 0 €/h attendu » pousse à corriger un +-- réglage qui n'existe pas. +-- --------------------------------------------------------------------------- + +ALTER TABLE "AccountPreferences" ADD COLUMN "printLandscape" BOOLEAN NOT NULL DEFAULT true; +ALTER TABLE "AccountPreferences" ADD COLUMN "printDensity" TEXT NOT NULL DEFAULT 'large'; +ALTER TABLE "AccountPreferences" ADD COLUMN "printContractTotals" BOOLEAN NOT NULL DEFAULT true; +ALTER TABLE "AccountPreferences" ADD COLUMN "printOtherTeams" BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE "AccountPreferences" ADD COLUMN "printSunday" BOOLEAN NOT NULL DEFAULT true; +ALTER TABLE "AccountPreferences" ADD COLUMN "printSignatureColumn" BOOLEAN NOT NULL DEFAULT true; +ALTER TABLE "AccountPreferences" ADD COLUMN "productivityTargetPerHour" DECIMAL(10,2); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 81f61f8..d9f48e3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -105,6 +105,25 @@ model AccountPreferences { /// Générer le matricule à la création plutôt que de le saisir. autoEmployeeNumber Boolean @default(false) + // --- Impression -------------------------------------------------------- + /// Paysage par défaut : une semaine tient en largeur, pas en hauteur. + printLandscape Boolean @default(true) + /// Densité : « large » lisible à distance, « compact » pour un gros effectif. + printDensity String @default("large") + printContractTotals Boolean @default(true) + /// Créneaux des autres équipes, pour situer une journée d'ensemble. + printOtherTeams Boolean @default(false) + printSunday Boolean @default(true) + /// Colonne d'émargement — une signature manuscrite ne vaut pas décompte du + /// temps de travail, mais reste demandée pour l'affichage en salle. + printSignatureColumn Boolean @default(true) + + // --- Productivité ------------------------------------------------------ + /// Objectif de chiffre d'affaires par heure travaillée, en euros. + /// Nul tant qu'aucun objectif n'est fixé : zéro serait un objectif, pas une + /// absence d'objectif. + productivityTargetPerHour Decimal? @db.Decimal(10, 2) + updatedAt DateTime @updatedAt account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) diff --git a/src/app/(app)/reglages/impression/PrintForm.tsx b/src/app/(app)/reglages/impression/PrintForm.tsx new file mode 100644 index 0000000..a8591af --- /dev/null +++ b/src/app/(app)/reglages/impression/PrintForm.tsx @@ -0,0 +1,102 @@ +'use client'; + +import { useActionState } from 'react'; + +import { FormError, SubmitButton } from '@/components/ui/Form'; +import { PersistentForm } from '@/components/ui/PersistentForm'; +import { PRINT_DENSITIES, togglesOf } from '@/domain/settings/preferences'; +import { + savePreferencesAction, + type ActionState, + type Preferences, +} from '@/server/settings/account'; + +const empty: ActionState = {}; + +export function PrintForm({ preferences }: { preferences: Preferences }) { + const [state, action] = useActionState(savePreferencesAction, empty); + const toggles = togglesOf('print'); + + return ( + + {toggles.map((toggle) => ( + + ))} + +
+ Orientation +

+ Une semaine se lit en largeur : le paysage évite de couper le + dimanche sur une seconde page. +

+ {[ + { value: 'landscape', label: 'Paysage' }, + { value: 'portrait', label: 'Portrait' }, + ].map((option) => ( + + ))} +
+ + + + {toggles.map((toggle) => ( +
+ + + {toggle.warning ? ( +

+ {toggle.warning} +

+ ) : null} +
+ ))} + + {state.error} + +
+ Enregistrer + {state.ok ? ( + Réglages enregistrés. + ) : null} +
+
+ ); +} diff --git a/src/app/(app)/reglages/impression/page.tsx b/src/app/(app)/reglages/impression/page.tsx new file mode 100644 index 0000000..6d54f93 --- /dev/null +++ b/src/app/(app)/reglages/impression/page.tsx @@ -0,0 +1,37 @@ +import Link from 'next/link'; + +import { PrintForm } from '@/app/(app)/reglages/impression/PrintForm'; +import { PageBody, PageHeader } from '@/components/shell/PageHeader'; +import { Card, CardHeader } from '@/components/ui/Card'; +import { getPreferences } from '@/server/settings/account'; + +export const metadata = { title: 'Impression · PlanFlow' }; +export const dynamic = 'force-dynamic'; + +export default async function ImpressionReglagesPage() { + const preferences = await getPreferences(); + + return ( + + + + + + + + +

+ Ces réglages gouvernent{' '} + + le planning à afficher + + . Le rendu passe par la boîte d’impression du navigateur, qui sait aussi + enregistrer en PDF : un second moteur de rendu serait un second endroit + où la feuille peut diverger de l’écran. +

+
+ ); +} diff --git a/src/app/(app)/reglages/productivite/ProductivityForm.tsx b/src/app/(app)/reglages/productivite/ProductivityForm.tsx new file mode 100644 index 0000000..e8a40b3 --- /dev/null +++ b/src/app/(app)/reglages/productivite/ProductivityForm.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { useActionState } from 'react'; + +import { Field, FormError, SubmitButton } from '@/components/ui/Form'; +import { PersistentForm } from '@/components/ui/PersistentForm'; +import { + saveProductivityTargetAction, + type ActionState, +} from '@/server/settings/account'; + +const empty: ActionState = {}; + +export function ProductivityForm({ target }: { target: string | null }) { + const [state, action] = useActionState(saveProductivityTargetAction, empty); + + return ( + +
+ + €/h +
+ + {state.error} + +
+ Enregistrer + {state.ok ? ( + Objectif enregistré. + ) : null} +
+
+ ); +} diff --git a/src/app/(app)/reglages/productivite/page.tsx b/src/app/(app)/reglages/productivite/page.tsx new file mode 100644 index 0000000..9f6c866 --- /dev/null +++ b/src/app/(app)/reglages/productivite/page.tsx @@ -0,0 +1,48 @@ +import { ProductivityForm } from '@/app/(app)/reglages/productivite/ProductivityForm'; +import { PageBody, PageHeader } from '@/components/shell/PageHeader'; +import { Badge } from '@/components/ui/Badge'; +import { Card, CardHeader } from '@/components/ui/Card'; +import { getPreferences } from '@/server/settings/account'; + +export const metadata = { title: 'Productivité · PlanFlow' }; +export const dynamic = 'force-dynamic'; + +export default async function ProductivitePage() { + const preferences = await getPreferences(); + const target = preferences.productivityTargetPerHour; + + return ( + + + + + non fixé + } + /> + + + +

+ L’objectif vaut pour l’ensemble des établissements. Vider le champ + l’efface plutôt que de le ramener à zéro : zéro serait un objectif + inatteignable, l’absence de valeur dit qu’aucun objectif n’a été fixé. +

+ +

+ Un objectif de productivité rapporte un chiffre d’affaires à des heures + planifiées. Il éclaire une décision d’organisation ; il ne mesure pas le + travail d’une personne, et n’a pas à être opposé à un salarié. +

+
+ ); +} diff --git a/src/components/planning/CounterStrip.tsx b/src/components/planning/CounterStrip.tsx index be4a338..f73882a 100644 --- a/src/components/planning/CounterStrip.tsx +++ b/src/components/planning/CounterStrip.tsx @@ -27,9 +27,16 @@ function Cell({ label, value }: { label: string; value: string }) { } /** - * Les cinq compteurs d'une ligne salarié : contrat, planifié, absences, écart, - * dimanches et repos. Rendus depuis `computeWeekCounters`, jamais recalculés - * localement. + * Les cinq compteurs d'une ligne salarié — PLAN.md §7.4 : **heures + * contractuelles, planifié, absences, écart, repos compensateur**. + * + * Ce sont ces cinq-là et pas d'autres. Les dimanches travaillés et les jours de + * repos se lisent sur la grille ; le repos compensateur, lui, ne se lit nulle + * part ailleurs — c'est une contrepartie **due**, et l'omettre du bandeau + * masque la seule valeur qui dit qu'une dette existe. + * + * Rendus depuis `computeWeekCounters`, jamais recalculés localement : la même + * implémentation alimente la grille, le rapport d'heures et l'export de paie. */ export function CounterStrip({ counters, @@ -57,7 +64,10 @@ export function CounterStrip({ Forfait jours + {/* Un forfait jours reste soumis aux repos : le RC s'affiche, l'écart + horaire non. */} + ); } @@ -71,6 +81,7 @@ export function CounterStrip({ · + {counters.deltaLabel} - - + ); } diff --git a/src/components/shell/navigation.ts b/src/components/shell/navigation.ts index ee143ec..bec4891 100644 --- a/src/components/shell/navigation.ts +++ b/src/components/shell/navigation.ts @@ -125,6 +125,12 @@ export const NAVIGATION: NavSection[] = [ { id: 'sites', label: 'Établissements', href: '/reglages/etablissements' }, { id: 'preferences', label: 'Préférences', href: '/reglages/preferences' }, { id: 'paie-reglages', label: 'Réglages de paie', href: '/reglages/paie' }, + { id: 'impression', label: 'Impression', href: '/reglages/impression' }, + { + id: 'productivite', + label: 'Productivité', + href: '/reglages/productivite', + }, { id: 'registre', label: 'Registre de paramétrage', href: '/reglages/registre' }, { id: 'email', label: 'Envoi de courrier', href: '/reglages/email' }, { id: 'securite', label: 'Sécurité', href: '/reglages/securite' }, diff --git a/src/domain/counters/week.ts b/src/domain/counters/week.ts index 37cf16c..6199675 100644 --- a/src/domain/counters/week.ts +++ b/src/domain/counters/week.ts @@ -22,6 +22,15 @@ export interface WeekCounters { sundaysWorked: number; /** Jours de repos dans la semaine. */ restDays: number; + /** + * Repos compensateur posé sur la semaine, en minutes. + * + * Distinct de `restDays` : un jour sans créneau est un jour sans travail, + * tandis qu'un repos compensateur est une **contrepartie due** — au dimanche + * travaillé, au dépassement de contingent. Les confondre ferait disparaître + * du bandeau la seule valeur qui dit qu'une dette existe. + */ + compensatoryRestMinutes: number; } export interface WeekCountersView extends WeekCounters { @@ -31,6 +40,7 @@ export interface WeekCountersView extends WeekCounters { plannedLabel: string; absenceLabel: string; deltaLabel: string; + compensatoryRestLabel: string; tone: DeltaTone; } @@ -74,6 +84,7 @@ export function computeWeekCounters(input: WeekCounters): WeekCountersView { plannedLabel: formatMinutes(input.plannedMinutes), absenceLabel: formatMinutes(input.absenceMinutes), deltaLabel: formatDelta(deltaMinutes), + compensatoryRestLabel: formatMinutes(input.compensatoryRestMinutes), tone: deltaTone(deltaMinutes), }; } diff --git a/src/domain/planning/board.ts b/src/domain/planning/board.ts index 00b889f..657bee4 100644 --- a/src/domain/planning/board.ts +++ b/src/domain/planning/board.ts @@ -76,6 +76,19 @@ export interface AbsenceInput { colorKey: string; } +/** + * Repos posé sur la semaine. + * + * Seul le repos **compensateur** alimente le compteur RC : le repos + * hebdomadaire est un droit déjà pris, tandis que le compensateur est une + * contrepartie due, et le bandeau doit dire laquelle des deux est en jeu. + */ +export interface RestInput { + membershipId: string; + restType: 'WEEKLY_REST' | 'COMPENSATORY_REST'; + minutes: number | null; +} + /** * Range les créneaux d'une semaine en lignes × sept colonnes. * @@ -91,9 +104,19 @@ export function buildRows( timeZone: string, isPublished: boolean, absences: AbsenceInput[] = [], + rests: RestInput[] = [], ): { rows: BoardRow[]; unassignedRow: BoardRow | null } { const columnOf = new Map(weekDates.map((date, index) => [date, index])); + const compensatoryByMember = new Map(); + for (const rest of rests) { + if (rest.restType !== 'COMPENSATORY_REST') continue; + compensatoryByMember.set( + rest.membershipId, + (compensatoryByMember.get(rest.membershipId) ?? 0) + (rest.minutes ?? 0), + ); + } + const emptyDays = (): BoardShift[][] => Array.from({ length: 7 }, () => [] as BoardShift[]); @@ -199,6 +222,8 @@ export function buildRows( ), sundaysWorked: sundaysByMember.get(person.membershipId)?.size ?? 0, restDays: 7 - worked, + compensatoryRestMinutes: + compensatoryByMember.get(person.membershipId) ?? 0, }, } satisfies BoardRow; }); @@ -221,6 +246,7 @@ export function buildRows( absenceMinutes: 0, sundaysWorked: 0, restDays: 0, + compensatoryRestMinutes: 0, }, } : null; diff --git a/src/domain/settings/preferences.ts b/src/domain/settings/preferences.ts index 6a976aa..8e419c3 100644 --- a/src/domain/settings/preferences.ts +++ b/src/domain/settings/preferences.ts @@ -8,7 +8,27 @@ * le JSX. */ -export type PreferenceGroup = 'planning' | 'rights' | 'payroll'; +export type PreferenceGroup = 'planning' | 'rights' | 'payroll' | 'print'; + +/** Densités d'impression. Liste fermée : elle décide d'une mise en page. */ +export const PRINT_DENSITIES = [ + { + key: 'large', + label: 'Grande', + hint: 'Moins de salariés par page, lisible à distance sur un mur.', + }, + { + key: 'compact', + label: 'Compacte', + hint: 'Tout l’effectif sur une page, au prix de la lisibilité de loin.', + }, +] as const; + +export type PrintDensity = (typeof PRINT_DENSITIES)[number]['key']; + +export function isPrintDensity(value: string): value is PrintDensity { + return PRINT_DENSITIES.some((density) => density.key === value); +} export interface PreferenceToggle { key: string; @@ -138,6 +158,34 @@ export const PREFERENCE_TOGGLES: PreferenceToggle[] = [ label: 'Générer le matricule automatiquement', hint: 'Décoché, le matricule est saisi à l’embauche — utile quand il doit correspondre à celui du logiciel de paie.', }, + + // --- Impression -------------------------------------------------------- + { + key: 'printContractTotals', + group: 'print', + label: 'Imprimer les totaux contractuels et planifiés', + hint: 'Ajoute une colonne de totaux par salarié.', + }, + { + key: 'printOtherTeams', + group: 'print', + label: 'Imprimer les créneaux des autres équipes', + hint: 'Situe la journée d’ensemble sur la feuille d’une équipe.', + }, + { + key: 'printSunday', + group: 'print', + label: 'Imprimer le dimanche', + hint: 'À décocher pour un établissement qui n’ouvre jamais ce jour-là : la colonne gagnée profite aux six autres.', + }, + { + key: 'printSignatureColumn', + group: 'print', + label: 'Imprimer la colonne « émargements »', + hint: 'Colonne vierge pour signature manuscrite.', + warning: + 'Un émargement papier ne vaut pas décompte du temps de travail : le décompte reste celui de PlanFlow, pas celui de la feuille signée.', + }, ]; export const PREFERENCE_KEYS: string[] = PREFERENCE_TOGGLES.map( diff --git a/src/server/planning/queries.ts b/src/server/planning/queries.ts index fd51489..38dcd66 100644 --- a/src/server/planning/queries.ts +++ b/src/server/planning/queries.ts @@ -147,6 +147,22 @@ export async function getWeekBoard( schedules.map((schedule) => [schedule.teamId, schedule]), ); + // Repos posés sur la semaine. Seul le compensateur alimente le compteur + // RC du bandeau ; le repos hebdomadaire est chargé avec, parce que les + // deux se posent sur la même table et que filtrer ici coûterait une + // seconde requête pour une distinction que le domaine fait déjà. + const rests = await db.rest.findMany({ + where: { + weeklyScheduleId: { in: schedules.map((schedule) => schedule.id) }, + }, + select: { + weeklyScheduleId: true, + membershipId: true, + restType: true, + minutes: true, + }, + }); + const shifts = await db.shift.findMany({ where: { weeklyScheduleId: { in: schedules.map((schedule) => schedule.id) }, @@ -236,6 +252,13 @@ export async function getWeekBoard( location.timezone, isPublished, absenceInputs, + rests + .filter((rest) => rest.weeklyScheduleId === schedule?.id) + .map((rest) => ({ + membershipId: rest.membershipId, + restType: rest.restType, + minutes: rest.minutes, + })), ); const teamAlerts = alerts diff --git a/src/server/settings/account.ts b/src/server/settings/account.ts index 2745215..28f4464 100644 --- a/src/server/settings/account.ts +++ b/src/server/settings/account.ts @@ -5,6 +5,7 @@ import { z } from 'zod'; import { AuthorizationError } from '@/domain/access/authorize'; import { + isPrintDensity, PREFERENCE_KEYS, timeToMinutes, } from '@/domain/settings/preferences'; @@ -52,6 +53,14 @@ export interface Preferences { smoothOvertimeMonthly: boolean; includeRestInNormalHours: boolean; autoEmployeeNumber: boolean; + printLandscape: boolean; + printDensity: string; + printContractTotals: boolean; + printOtherTeams: boolean; + printSunday: boolean; + printSignatureColumn: boolean; + /** Chaîne décimale, ou `null` quand aucun objectif n'est fixé. */ + productivityTargetPerHour: string | null; } const DEFAULTS: Preferences = { @@ -73,6 +82,13 @@ const DEFAULTS: Preferences = { smoothOvertimeMonthly: false, includeRestInNormalHours: false, autoEmployeeNumber: false, + printLandscape: true, + printDensity: 'large', + printContractTotals: true, + printOtherTeams: false, + printSunday: true, + printSignatureColumn: true, + productivityTargetPerHour: null, }; export async function getAccountIdentity(): Promise { @@ -103,8 +119,20 @@ export async function getPreferences(): Promise { }); if (!row) return DEFAULTS; - const { accountId: _ignored, updatedAt: _stamp, ...values } = row; - return values; + const { + accountId: _ignored, + updatedAt: _stamp, + productivityTargetPerHour, + ...values + } = row; + + return { + ...values, + // Un Decimal Prisma ne traverse pas la frontière serveur/client. Une + // chaîne le fait sans perdre de décimale, ce qu'un `number` ne garantit + // pas sur un montant. + productivityTargetPerHour: productivityTargetPerHour?.toString() ?? null, + }; }); } @@ -219,11 +247,27 @@ export async function savePreferencesAction( return { error: 'Formulaire incomplet : aucun réglage à enregistrer.' }; } - const values: Record = {}; + const values: Record = {}; for (const key of scope) { values[key] = formData.get(key) === 'on'; } + // L'orientation et la densité vivent dans le même formulaire que les + // interrupteurs d'impression : elles ne sont lues que si le formulaire les + // porte, sinon un autre bloc les remettrait à leur défaut. + const orientation = formData.get('printOrientation'); + if (typeof orientation === 'string' && orientation !== '') { + values.printLandscape = orientation === 'landscape'; + } + + const density = formData.get('printDensity'); + if (typeof density === 'string' && density !== '') { + if (!isPrintDensity(density)) { + return { error: 'Densité d’impression inconnue.' }; + } + values.printDensity = density; + } + const eveningRaw = formData.get('eveningShiftStart'); if (typeof eveningRaw === 'string' && eveningRaw !== '') { const minutes = timeToMinutes(eveningRaw); @@ -270,5 +314,72 @@ export async function savePreferencesAction( revalidatePath('/reglages/preferences'); revalidatePath('/reglages/paie'); + revalidatePath('/reglages/impression'); + return { ok: true }; +} + +const productivityInput = z.object({ + target: z + .union([ + z.literal(''), + z.coerce + .number() + .min(0, 'Un objectif négatif n’a pas de sens') + .max(100_000), + ]) + .transform((value) => (value === '' ? null : value)), +}); + +/** + * Objectif de productivité, en euros de chiffre d'affaires par heure. + * + * Vider le champ **efface** l'objectif au lieu de le mettre à zéro : zéro est + * un objectif inatteignable, l'absence de valeur dit qu'aucun objectif n'a été + * fixé. Un écran qui afficherait « 0 €/h attendu » pousserait à corriger un + * réglage qui n'existe pas. + */ +export async function saveProductivityTargetAction( + _previous: ActionState, + formData: FormData, +): Promise { + const parsed = productivityInput.safeParse({ + target: formData.get('target') ?? '', + }); + if (!parsed.success) { + return { error: parsed.error.issues[0]?.message ?? 'Formulaire invalide' }; + } + + try { + await mutate('settings.access', async (db, actor) => { + const before = await db.accountPreferences.findUnique({ + where: { accountId: actor.accountId }, + }); + + await db.accountPreferences.upsert({ + where: { accountId: actor.accountId }, + create: { productivityTargetPerHour: parsed.data.target } as never, + update: { productivityTargetPerHour: parsed.data.target }, + }); + + await recordAudit(db, { + actorMembershipId: actor.membershipId, + action: 'account.productivity.update', + entityType: 'AccountPreferences', + entityId: actor.accountId, + before: { + productivityTargetPerHour: + before?.productivityTargetPerHour?.toString() ?? null, + }, + after: { productivityTargetPerHour: parsed.data.target }, + }); + }); + } catch (error) { + if (error instanceof AuthorizationError) { + return { error: "Vous n'avez pas le droit de modifier les réglages." }; + } + throw error; + } + + revalidatePath('/reglages/productivite'); return { ok: true }; } diff --git a/tests/unit/counters.test.ts b/tests/unit/counters.test.ts index 62f2f6f..83d194d 100644 --- a/tests/unit/counters.test.ts +++ b/tests/unit/counters.test.ts @@ -50,6 +50,7 @@ describe('computeWeekCounters', () => { absenceMinutes: h(18), sundaysWorked: 0, restDays: 5, + compensatoryRestMinutes: 0, }); expect(counters.deltaMinutes).toBe(0); @@ -64,6 +65,7 @@ describe('computeWeekCounters', () => { absenceMinutes: 0, sundaysWorked: 2, restDays: 2, + compensatoryRestMinutes: 0, }); expect(counters.deltaLabel).toBe('+1 h 30'); @@ -77,6 +79,7 @@ describe('computeWeekCounters', () => { absenceMinutes: 0, sundaysWorked: 0, restDays: 2, + compensatoryRestMinutes: 0, }); expect(counters.deltaLabel).toBe('−1 h 45'); diff --git a/tests/unit/planning-board.test.ts b/tests/unit/planning-board.test.ts index 239b97f..d89fd22 100644 --- a/tests/unit/planning-board.test.ts +++ b/tests/unit/planning-board.test.ts @@ -194,6 +194,28 @@ describe('buildRows', () => { '14:00–19:00', ]); }); + + it('ne compte au RC que le repos compensateur', () => { + // Le repos hebdomadaire est un droit déjà pris ; le compensateur est une + // contrepartie due. Les additionner ferait disparaître la dette dans un + // total qui ne veut rien dire. + const { rows } = buildRows([person()], [], DATES, TZ, false, [], [ + { membershipId: 'm1', restType: 'COMPENSATORY_REST', minutes: 420 }, + { membershipId: 'm1', restType: 'WEEKLY_REST', minutes: 2100 }, + ]); + + expect(rows[0]?.counters.compensatoryRestMinutes).toBe(420); + }); + + it('tolère un repos sans durée', () => { + // `Rest.minutes` est nullable : un repos posé sans durée vaut zéro minute, + // pas NaN — un NaN se propagerait au bandeau et à l'export. + const { rows } = buildRows([person()], [], DATES, TZ, false, [], [ + { membershipId: 'm1', restType: 'COMPENSATORY_REST', minutes: null }, + ]); + + expect(rows[0]?.counters.compensatoryRestMinutes).toBe(0); + }); }); describe('shiftState', () => { diff --git a/tests/unit/planning-presence.test.ts b/tests/unit/planning-presence.test.ts index 7502572..42bd45b 100644 --- a/tests/unit/planning-presence.test.ts +++ b/tests/unit/planning-presence.test.ts @@ -20,6 +20,7 @@ function row(overrides: Partial = {}): BoardRow { absenceMinutes: 0, sundaysWorked: 0, restDays: 0, + compensatoryRestMinutes: 0, }, days: [[], [], [], [], [], [], []], absences: [], diff --git a/tests/unit/preferences.test.ts b/tests/unit/preferences.test.ts index eb6bcf7..288ff39 100644 --- a/tests/unit/preferences.test.ts +++ b/tests/unit/preferences.test.ts @@ -14,11 +14,13 @@ describe('catalogue de préférences', () => { }); it('range chaque préférence dans un seul groupe', () => { - const grouped = [ - ...togglesOf('planning'), - ...togglesOf('rights'), - ...togglesOf('payroll'), + // Les groupes se déduisent du catalogue plutôt que d'être listés ici : une + // liste tenue à la main laisserait un groupe entier sans écran, et le test + // passerait quand même. + const groups = [ + ...new Set(PREFERENCE_TOGGLES.map((toggle) => toggle.group)), ]; + const grouped = groups.flatMap((group) => togglesOf(group)); expect(grouped).toHaveLength(PREFERENCE_TOGGLES.length); });