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>
175 lines
5.7 KiB
TypeScript
175 lines
5.7 KiB
TypeScript
import 'server-only';
|
|
|
|
import { cache } from 'react';
|
|
|
|
import { genderShort } from '@/domain/hr/civil-status';
|
|
import type { RupPerson } from '@/domain/legal/rup';
|
|
import { peopleWithGaps } from '@/domain/legal/rup';
|
|
import { query } from '@/server/context';
|
|
import { contractLabel } from '@/server/employees/queries';
|
|
|
|
/**
|
|
* Registre unique du personnel — lecture.
|
|
*
|
|
* Le registre porte **tous** ceux qui ont été employés dans l'établissement,
|
|
* y compris les partis : c'est précisément l'historique que l'inspection vient
|
|
* chercher. Filtrer sur les présents en ferait un trombinoscope.
|
|
*/
|
|
|
|
export interface RupData {
|
|
locationId: string;
|
|
locationName: string;
|
|
siret: string | null;
|
|
people: RupPerson[];
|
|
/** Nombre de salariés à qui il manque au moins une mention obligatoire. */
|
|
incomplete: number;
|
|
}
|
|
|
|
export const getRegisterData = cache(async function getRegisterData(
|
|
locationId: string,
|
|
): Promise<RupData | null> {
|
|
return query('members.register.export', async (db) => {
|
|
const location = await db.location.findUnique({
|
|
where: { id: locationId },
|
|
select: { id: true, name: true, siret: true },
|
|
});
|
|
if (!location) return null;
|
|
|
|
const contracts = await db.userContract.findMany({
|
|
where: { locationId },
|
|
orderBy: { startDate: 'asc' },
|
|
include: {
|
|
membership: {
|
|
select: {
|
|
profile: {
|
|
select: {
|
|
firstName: true,
|
|
lastName: true,
|
|
birthName: true,
|
|
gender: true,
|
|
nationality: true,
|
|
birthDate: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const jobTitleIds = [
|
|
...new Set(
|
|
contracts
|
|
.map((contract) => contract.jobTitleId)
|
|
.filter((id): id is string => Boolean(id)),
|
|
),
|
|
];
|
|
const jobTitles = await db.jobTitle.findMany({
|
|
where: { id: { in: jobTitleIds } },
|
|
select: { id: true, name: true },
|
|
});
|
|
const jobTitleNames = new Map(
|
|
jobTitles.map((entry) => [entry.id, entry.name]),
|
|
);
|
|
|
|
// Une ligne par contrat, pas par personne : un salarié réembauché a deux
|
|
// entrées et deux sorties, et les fondre effacerait l'interruption.
|
|
const people: RupPerson[] = contracts.map((contract) => ({
|
|
// Le registre se tient au nom de naissance : c'est celui de l'état civil,
|
|
// et le nom d'usage peut changer sans que la personne change.
|
|
lastName:
|
|
contract.membership.profile?.birthName ??
|
|
contract.membership.profile?.lastName ??
|
|
'',
|
|
firstName: contract.membership.profile?.firstName ?? '',
|
|
sex: genderShort(contract.membership.profile?.gender ?? null),
|
|
nationality: contract.membership.profile?.nationality ?? null,
|
|
birthDate: contract.membership.profile?.birthDate ?? null,
|
|
jobTitle: contract.jobTitleId
|
|
? (jobTitleNames.get(contract.jobTitleId) ?? null)
|
|
: null,
|
|
qualification: contract.classification,
|
|
contractLabel: contractLabel(contract.contractType),
|
|
entryDate: contract.startDate,
|
|
exitDate: contract.endDate,
|
|
}));
|
|
|
|
return {
|
|
locationId: location.id,
|
|
locationName: location.name,
|
|
siret: location.siret,
|
|
people,
|
|
incomplete: peopleWithGaps(people),
|
|
};
|
|
});
|
|
});
|
|
|
|
/** Établissements pour lesquels un registre peut être tiré. */
|
|
export const listRegisterLocations = cache(
|
|
async function listRegisterLocations(): Promise<
|
|
Array<{ id: string; name: string; incomplete: number; total: number }>
|
|
> {
|
|
return query('members.register.export', async (db) => {
|
|
const locations = await db.location.findMany({
|
|
where: { archivedAt: null },
|
|
select: { id: true, name: true },
|
|
orderBy: { name: 'asc' },
|
|
});
|
|
|
|
// Le décompte des manques est calculé ici pour que l'écran puisse
|
|
// avertir **avant** le téléchargement : découvrir un registre incomplet
|
|
// en l'ouvrant, c'est le découvrir devant l'inspection.
|
|
return Promise.all(
|
|
locations.map(async (location) => {
|
|
const contracts = await db.userContract.findMany({
|
|
where: { locationId: location.id },
|
|
select: {
|
|
classification: true,
|
|
jobTitleId: true,
|
|
contractType: true,
|
|
startDate: true,
|
|
endDate: true,
|
|
membership: {
|
|
select: {
|
|
profile: {
|
|
select: {
|
|
firstName: true,
|
|
lastName: true,
|
|
birthName: true,
|
|
gender: true,
|
|
nationality: true,
|
|
birthDate: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const people: RupPerson[] = contracts.map((contract) => ({
|
|
lastName:
|
|
contract.membership.profile?.birthName ??
|
|
contract.membership.profile?.lastName ??
|
|
'',
|
|
firstName: contract.membership.profile?.firstName ?? '',
|
|
sex: genderShort(contract.membership.profile?.gender ?? null),
|
|
nationality: contract.membership.profile?.nationality ?? null,
|
|
birthDate: contract.membership.profile?.birthDate ?? null,
|
|
jobTitle: contract.jobTitleId ? 'x' : null,
|
|
qualification: contract.classification,
|
|
contractLabel: contractLabel(contract.contractType),
|
|
entryDate: contract.startDate,
|
|
exitDate: contract.endDate,
|
|
}));
|
|
|
|
return {
|
|
id: location.id,
|
|
name: location.name,
|
|
incomplete: peopleWithGaps(people),
|
|
total: people.length,
|
|
};
|
|
}),
|
|
);
|
|
});
|
|
},
|
|
);
|