Alléger l'annuaire

Le formulaire d'embauche, posé au bas de la liste, occupait plus de place
que l'effectif qu'on vient consulter — une quinzaine de champs déployés en
permanence pour un geste occasionnel. Il s'ouvre désormais en modale,
depuis le bouton d'en-tête.

`<dialog>` natif plutôt qu'un panneau maison : le piège de focus, la
fermeture par Échap et le fond inerte viennent avec, et une `<div>` doit
les réimplémenter sans jamais les tenir tout à fait.

Le tableau perd sa carte et ses fonds : une liste de personnes se lit
mieux sans cadre autour. Les colonnes suivent l'annuaire de référence —
collaborateur, rôle, email, mobile, rattachement, invitation. Le contrat
quitte le tableau, où il doublait le filtre qui le cherche déjà ; son
absence reste signalée là où elle compte, dans le rattachement.

Le décompte ne s'affiche plus que filtré : « 87 salariés » au-dessus de 87
lignes n'apprend rien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5 committed 2026-08-10 15:45:28 +02:00
1 parent 93724b9d66
commit 894e880198
11 files changed
+225 -124

No files matched your search

+108 -118
View File
@@ -1,11 +1,11 @@
import Link from 'next/link';
import { Suspense } from 'react';
import { AddEmployeeForm } from '@/app/(app)/equipe/AddEmployeeForm';
import { AddEmployeeDialog } from '@/app/(app)/equipe/AddEmployeeDialog';
import { DirectoryFilters } from '@/app/(app)/equipe/DirectoryFilters';
import { PageBody, PageHeader } from '@/components/shell/PageHeader';
import { Badge, type Tone } from '@/components/ui/Badge';
import { Card, CardHeader, EmptyState } from '@/components/ui/Card';
import { EmptyState } from '@/components/ui/Card';
import {
listContractLocations,
listEmployees,
@@ -21,8 +21,6 @@ const INVITATION_TONES: Record<string, { label: string; tone: Tone }> = {
REVOKED: { label: 'Révoquée', tone: 'neutral' },
};
const dateFormat = new Intl.DateTimeFormat('fr-FR', { dateStyle: 'medium' });
const one = (value: string | string[] | undefined): string | undefined =>
Array.isArray(value) ? value[0] : value;
@@ -59,25 +57,28 @@ export default async function EquipePage({
<PageBody>
<PageHeader
title="Équipe"
subtitle={
filtered
? `${directory.rows.length} sur ${directory.total} salariés`
: `${directory.total} salarié${directory.total > 1 ? 's' : ''}`
}
// Le décompte n'apparaît que s'il apprend quelque chose : « 87 salariés »
// au-dessus d'une liste de 87 lignes ne dit rien que la liste ne dise.
{...(filtered
? { subtitle: `${directory.rows.length} sur ${directory.total} salariés` }
: {})}
actions={
<Link
href="/reglages/registre"
className="inline-flex h-8 items-center rounded-2 border border-line-3 bg-surface px-3.5 text-sm font-medium text-ink-1 hover:bg-surface-2"
>
Registre unique du personnel
</Link>
<>
<Link
href="/reglages/registre"
className="inline-flex h-8 items-center rounded-2 border border-line-3 bg-surface px-3.5 text-sm font-medium text-ink-1 hover:bg-surface-2"
>
Registre unique du personnel
</Link>
<AddEmployeeDialog locations={hiringLocations} />
</>
}
/>
{/* `useSearchParams` impose une frontière de suspense : sans elle, la
page entière serait rendue à la demande au lieu d'être servie puis
hydratée. */}
<Suspense fallback={<div className="h-[5.5rem]" />}>
<Suspense fallback={<div className="h-[6rem]" />}>
<DirectoryFilters
options={{
locations: directory.locations,
@@ -87,114 +88,103 @@ export default async function EquipePage({
/>
</Suspense>
<Card>
{directory.rows.length === 0 ? (
<EmptyState
title={filtered ? 'Aucun résultat' : 'Aucun salarié'}
description={
filtered
? 'Aucun salarié ne répond à ces critères. Élargissez la recherche.'
: 'Ajoutez un premier salarié pour commencer à planifier.'
}
/>
) : (
<div className="overflow-x-auto">
<table className="w-full min-w-[980px] border-collapse text-sm">
<thead>
<tr className="border-b border-line-2 bg-surface-2 text-left text-micro font-semibold tracking-[0.06em] text-ink-3 uppercase">
<th className="px-4 py-2.5">Collaborateur</th>
<th className="px-4 py-2.5">Rôle</th>
<th className="px-4 py-2.5">Contrat</th>
<th className="px-4 py-2.5">Mobile</th>
<th className="px-4 py-2.5">Rattachement</th>
<th className="px-4 py-2.5">Invitation</th>
</tr>
</thead>
<tbody>
{directory.rows.map((employee) => {
const invitation = employee.invitationState
? INVITATION_TONES[employee.invitationState]
: null;
{directory.rows.length === 0 ? (
<EmptyState
title={filtered ? 'Aucun résultat' : 'Aucun salarié'}
description={
filtered
? 'Aucun salarié ne répond à ces critères. Élargissez la recherche.'
: 'Ajoutez un premier salarié pour commencer à planifier.'
}
/>
) : (
<div className="overflow-x-auto">
<table className="w-full min-w-[940px] border-collapse text-sm">
<thead>
<tr className="border-b border-line-2 text-left text-micro font-semibold tracking-[0.04em] text-ink-3">
<th className="px-3 py-3 font-semibold">Collaborateur</th>
<th className="px-3 py-3 font-semibold">Rôle</th>
<th className="px-3 py-3 font-semibold">Email</th>
<th className="px-3 py-3 font-semibold">Mobile</th>
<th className="px-3 py-3 font-semibold">Rattachement</th>
<th className="px-3 py-3 font-semibold">Invitation</th>
</tr>
</thead>
<tbody>
{directory.rows.map((employee) => {
const invitation = employee.invitationState
? INVITATION_TONES[employee.invitationState]
: null;
return (
<tr
key={employee.id}
className="border-b border-line-1 last:border-b-0 hover:bg-surface-2"
>
<td className="px-4 py-2.5">
<Link
href={`/equipe/${employee.id}`}
className="flex items-center gap-2.5 rounded-2"
return (
<tr
key={employee.id}
className="border-b border-line-1 last:border-b-0 hover:bg-surface-2"
>
<td className="px-3 py-3">
<Link
href={`/equipe/${employee.id}`}
className="flex items-center gap-2.5 rounded-2"
>
<span
aria-hidden
className="flex size-8 flex-none items-center justify-center rounded-full bg-accent-soft text-micro font-semibold text-accent-soft-ink"
>
<span
aria-hidden
className="flex size-7 flex-none items-center justify-center rounded-full bg-accent-soft text-micro font-semibold text-accent-soft-ink"
>
{employee.firstName.charAt(0)}
{employee.lastName.charAt(0)}
{employee.firstName.charAt(0)}
{employee.lastName.charAt(0)}
</span>
<span className="min-w-0">
<span className="block truncate font-medium underline decoration-line-3 underline-offset-2">
{employee.firstName} {employee.lastName}
</span>
<span className="min-w-0">
<span className="block truncate font-medium">
{employee.firstName} {employee.lastName}
</span>
{/* Le matricule ne double la colonne « Email » que
lorsqu'il n'y en a pas : c'est alors la seule
adresse à laquelle on désigne ce salarié. */}
{employee.email ? null : (
<span className="block truncate text-micro text-ink-3">
{employee.email ?? `Matricule ${employee.employeeNumber}`}
Matricule {employee.employeeNumber}
</span>
</span>
</Link>
</td>
<td className="px-4 py-2.5">
<Badge tone="accent">{employee.roleName}</Badge>
</td>
<td className="px-4 py-2.5">
{employee.contract ? (
<span className="flex flex-wrap items-center gap-1.5">
<span>{employee.contract.label}</span>
{employee.contract.forfaitJours ? (
<Badge tone="accent">Forfait jours</Badge>
) : null}
<span className="tnum text-micro text-ink-3">
depuis le{' '}
{dateFormat.format(employee.contract.since)}
</span>
</span>
) : (
<Badge tone="warn">Sans contrat</Badge>
)}
</td>
<td className="tnum px-4 py-2.5 text-ink-2">
{employee.phone ?? (
<span className="text-ink-3">Non renseigné</span>
)}
</td>
<td className="px-4 py-2.5 text-ink-2">
{employee.locationName ?? '—'}
{employee.teamName ? ` / ${employee.teamName}` : ''}
</td>
<td className="px-4 py-2.5">
{invitation ? (
<Badge tone={invitation.tone}>{invitation.label}</Badge>
) : (
<span className="text-micro text-ink-3">
Sans accès applicatif
</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</Card>
<Card>
<CardHeader title="Ajouter un collaborateur" />
<div className="p-4">
<AddEmployeeForm locations={hiringLocations} />
)}
</span>
</Link>
</td>
<td className="px-3 py-3">
<Badge tone="accent">{employee.roleName}</Badge>
</td>
<td className="px-3 py-3 text-ink-2">
{employee.email ?? (
<span className="text-ink-3">Sans compte</span>
)}
</td>
<td className="tnum px-3 py-3 text-ink-2">
{employee.phone ?? (
<span className="text-ink-3">Non renseigné</span>
)}
</td>
<td className="px-3 py-3 text-ink-2">
{employee.locationName ? (
<>
{employee.locationName}
{employee.teamName ? ` / ${employee.teamName}` : ''}
</>
) : (
<Badge tone="warn">Sans contrat</Badge>
)}
</td>
<td className="px-3 py-3">
{invitation ? (
<Badge tone={invitation.tone}>{invitation.label}</Badge>
) : (
<span className="text-micro text-ink-3">—</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</Card>
)}
</PageBody>
);
}