Donner aux indicateurs RH leur profondeur temporelle

Le tableau de bord montrait un mois. Un effectif, un taux de rotation ou un
absentéisme lus sur un seul mois ne disent rien : c'est la pente qui informe, et
elle demande une série.

Trois analyses sur douze mois glissants — effectifs, heures, absences — servies
par une seule requête. C'est ce qui garantit que l'effectif moyen affiché en
« effectifs » soit exactement le dénominateur du taux d'absentéisme affiché en
« absences ». Trois calculs séparés finiraient par diverger d'un arrondi, et
l'écart se plaide mal devant un CSE.

Les mois se découpent en mémoire après une lecture par nature, pas en douze
requêtes par indicateur : sur une année glissante, la seconde approche rend
l'écran inutilisable.

Le graphique est en HTML. Douze barres ne justifient pas 90 ko de dépendance, et
la CSP interdit de toute façon toute origine tierce. La hauteur ne porte jamais
l'information seule — chaque barre affiche sa valeur, et le tableau reste la
source. Une valeur non calculable s'y dessine en gris plutôt qu'à zéro : « 0 %
de rotation » sur un établissement vide est une affirmation fausse, pas une
absence de mouvement.

Le mois de chaque ligne mène à ses lignes sources. Un indicateur qu'on ne peut
pas ouvrir ne se corrige pas, il se conteste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5 committed 2026-08-11 12:35:08 +02:00
1 parent 1519b6bf9d
commit 5b60e7ddb6
7 files changed
+720

No files matched your search

+100
View File
@@ -0,0 +1,100 @@
import Link from 'next/link';
import {
AnalysisShell,
shortMonth,
} from '@/app/(app)/rh/analyses/AnalysisShell';
import { SeriesChart } from '@/components/hr/SeriesChart';
import { Card, CardHeader } from '@/components/ui/Card';
import { monthOf } from '@/domain/planning/month';
import { getAnalyticsSeries } from '@/server/hr/analytics';
export const metadata = { title: 'Analyse des absences · PlanFlow' };
export const dynamic = 'force-dynamic';
interface PageProps {
searchParams: Promise<{ etablissement?: string }>;
}
export default async function AbsencesAnalysePage({ searchParams }: PageProps) {
const { etablissement } = await searchParams;
const series = await getAnalyticsSeries(monthOf(new Date()), 12, etablissement);
const last = series.points[series.points.length - 1];
return (
<AnalysisShell
current="absences"
title="Absences"
subtitle={
last?.absenteeismRate === null || last === undefined
? 'Taux non calculable'
: `${last.absenteeismRate} % d’absentéisme ce mois`
}
series={series}
footnote="Le taux rapporte les jours d’absence aux jours théoriquement travaillés, pas aux jours calendaires : rapporter à trente donnerait un taux artificiellement bas et ferait passer un problème réel pour du bruit. La colonne « dont arrêts » agrège les absences relevant de la Sécurité sociale sans jamais nommer de salarié — un taux ne doit pas devenir un moyen d’identifier qui est malade."
>
<Card>
<CardHeader title="Taux d’absentéisme mensuel" />
<SeriesChart
tone="warn"
points={series.points.map((point) => ({
label: point.label,
short: shortMonth(point.label),
value: point.absenteeismRate,
}))}
format={(value) => `${value} %`}
/>
</Card>
<Card>
<CardHeader title="Détail mensuel" />
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-line-1 text-left">
{[
'Mois',
'Jours d’absence',
'dont arrêts',
'Jours théoriques',
'Taux',
].map((heading) => (
<th
key={heading}
className="px-4 py-2 text-micro font-semibold tracking-[0.06em] text-ink-3 uppercase"
>
{heading}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-line-1">
{[...series.points].reverse().map((point) => (
<tr key={point.param}>
<td className="px-4 py-2">
<Link
href={`/?mois=${point.param}&liste=absences${series.location ? `&etablissement=${series.location.id}` : ''}`}
className="hover:underline"
>
{point.label}
</Link>
</td>
<td className="px-4 py-2 tnum">{point.absenceDays}</td>
<td className="px-4 py-2 tnum">{point.sickDays}</td>
<td className="px-4 py-2 tnum">
{point.theoreticalWorkedDays}
</td>
<td className="px-4 py-2 tnum">
{point.absenteeismRate === null
? '—'
: `${point.absenteeismRate} %`}
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</AnalysisShell>
);
}