Files
planflow/src/app/(app)/equipe/AddEmployeeDialog.tsx
T
MichaelandClaude Opus 5 06198ddbce Recueillir à l'embauche ce qu'exige le contrat
Créer un salarié demandait cinq champs. Le contrat, la déclaration et le
registre en réclament une vingtaine, et rien ne permettait de les saisir :
le sexe, le nom de naissance, le pays et le département de naissance, la
situation de famille, les personnes à charge, le téléphone fixe, le
complément d'adresse et l'heure d'embauche n'existaient pas en base.

La migration les ajoute, toutes facultatives : un dossier incomplet doit
pouvoir exister — c'est au registre de signaler ce qui lui manque, pas à
la base de refuser l'embauche. Seul l'envoi des plannings par SMS fait
exception : c'est un consentement, donc faux par défaut, la charge de la
preuve pesant sur l'employeur.

Le pays et le département de naissance sont des colonnes à part et non une
commune saisie librement : la déclaration sociale les demande séparément,
et les rétro-extraire échouerait au premier « Bar-le-Duc (Meuse) ».

Le formulaire devient un panneau latéral : une trentaine de champs posés
au centre masquaient l'annuaire, et on embauche en regardant qui est déjà
là. Le matricule y est proposé à la suite du dernier, attribué dans la
transaction pour que deux embauches simultanées ne tombent pas sur le même
rang. Le nom de naissance vaut le nom de famille quand il n'en diffère
pas, plutôt que d'imposer une ressaisie à l'immense majorité des dossiers.

Le responsable hiérarchique et la politique RTT se posent enfin à
l'embauche : les deux modèles existaient sans qu'aucun écran ne les
alimente.

Le registre unique du personnel gagne sa colonne « sexe » et se tient
désormais au nom de naissance — le nom d'usage peut changer sans que la
personne change.

Migration écrite et validée contre le schéma, non appliquée : aucune base
n'était ouverte ici. À passer avec `pnpm db:migrate`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:28:28 +02:00

79 lines
2.5 KiB
TypeScript

'use client';
import { useEffect, useRef, useState } from 'react';
import { AddEmployeeForm } from '@/app/(app)/equipe/AddEmployeeForm';
import { Button } from '@/components/ui/Button';
import type {
ContractLocation,
HiringOptions,
} from '@/server/employees/queries';
/**
* Embauche, en panneau latéral.
*
* Une trentaine de champs : posés au bas de la liste, ils occupaient plus de
* place que l'effectif qu'on vient consulter. Le panneau glisse par la droite
* et laisse l'annuaire visible — on embauche en regardant qui est déjà là.
*
* `<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.
*/
export function AddEmployeeDialog({
locations,
options,
}: {
locations: ContractLocation[];
options: HiringOptions;
}) {
const dialog = useRef<HTMLDialogElement>(null);
const [open, setOpen] = useState(false);
useEffect(() => {
const element = dialog.current;
if (!element) return;
if (open && !element.open) element.showModal();
if (!open && element.open) element.close();
}, [open]);
return (
<>
<Button variant="primary" onClick={() => setOpen(true)}>
Ajouter un collaborateur
</Button>
<dialog
ref={dialog}
aria-labelledby="titre-embauche"
onClose={() => setOpen(false)}
className="mt-0 mr-0 mb-0 ml-auto h-full max-h-none w-[min(30rem,100vw)] max-w-none border-l border-line-1 bg-surface p-0 text-ink-1 backdrop:bg-[rgb(0_0_0/0.4)]"
>
<div className="flex h-full flex-col">
<header className="flex items-center gap-3 border-b border-line-1 px-5 py-4">
<h2 id="titre-embauche" className="text-md font-semibold">
Nouvel employé
</h2>
<span className="flex-1" />
<Button variant="ghost" onClick={() => setOpen(false)}>
Fermer
</Button>
</header>
<div className="min-h-0 flex-1 overflow-y-auto px-5 pt-5">
{open ? (
// Monté à l'ouverture : un formulaire démonté entre deux usages
// repart vide, sans traîner la saisie abandonnée la fois d'avant.
<AddEmployeeForm
locations={locations}
options={options}
onSaved={() => setOpen(false)}
/>
) : null}
</div>
</div>
</dialog>
</>
);
}