Rattraper le référentiel des absences des instances déjà installées

installReferentials() pose les types d'absence depuis WP-02, mais ne tourne
qu'à la création du compte. Une instance installée avant garde zéro type : le
menu « Type » du formulaire de demande reste vide et le module de congés est
inutilisable, sans qu'aucune erreur ne le signale. Déployer le correctif ne
suffisait donc pas.

La migration ne touche que les comptes dont le référentiel est entièrement
vide — un type délibérément archivé ne doit pas réapparaître. Elle lève
FORCE ROW LEVEL SECURITY le temps de la transaction, faute de compte courant.

Le formulaire dit désormais que le référentiel manque et renvoie aux réglages,
plutôt que d'afficher un menu vide qui laisse saisir une demande impossible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5 committed 2026-08-12 15:52:13 +02:00
1 parent dd01fb58f4
commit 49c311423d
2 files changed
+93

No files matched your search

@@ -0,0 +1,77 @@
-- Types d'absence sur les comptes installés avant WP-02 — PLAN.md §7.
--
-- `installReferentials()` pose le référentiel des absences depuis WP-02, mais
-- il ne tourne qu'à la création du compte. Une instance installée **avant**
-- garde donc zéro type d'absence, et le menu « Type » du formulaire de demande
-- reste vide : le module de congés est inutilisable, sans qu'aucune erreur ne
-- le signale. Déployer le correctif ne suffit pas — d'où ce rattrapage.
--
-- Il ne s'applique qu'aux comptes dont le référentiel est **entièrement** vide.
-- Un compte qui a renommé ou archivé ses types garde les siens : personne ne
-- doit voir réapparaître un type qu'il a délibérément retiré.
--
-- La liste double celle de `referentials.ts`, à dessein. Une migration est
-- figée par nature — elle décrit l'état à rattraper au 12 août 2026, pas la
-- liste courante. Les évolutions ultérieures du référentiel se font depuis
-- `/reglages/types-absence`, pas en rééditant ce fichier.
-- `AbsenceType` porte FORCE ROW LEVEL SECURITY : la politique d'insertion vaut
-- pour le propriétaire de la table comme pour tout le monde, et une migration
-- n'a pas de compte courant. La levée tient le temps de la transaction — un
-- échec l'annule, la table ne peut pas rester ouverte.
ALTER TABLE "AbsenceType" NO FORCE ROW LEVEL SECURITY;
INSERT INTO "AbsenceType" (
"id", "accountId", "code", "name", "colorKey",
"isPaid", "countsAsWorkTime", "affectsPaidLeaveAccrual",
"isSocialSecurity", "requiresJustification", "minNoticeDays"
)
SELECT
gen_random_uuid()::text,
a."id",
t."code", t."name", t."colorKey",
t."isPaid", FALSE, t."accrual",
t."social", t."justify", t."notice"
FROM "Account" a
CROSS JOIN (
VALUES
-- Congés et repos
('CP', 'Congé payé', 'cp', TRUE, TRUE, FALSE, FALSE, 30),
('RTT', 'RTT', 'rtt', TRUE, TRUE, FALSE, FALSE, 7),
('RC', 'Repos compensateur', 'rtt', TRUE, TRUE, FALSE, FALSE, 7),
('RC_NUIT', 'Repos compensateur de nuit', 'rtt', TRUE, TRUE, FALSE, FALSE, 7),
('RC_HAB', 'Repos compensateur d''habillement', 'rtt', TRUE, TRUE, FALSE, FALSE, 7),
('RECUP_JF', 'Récupération jour férié', 'rtt', TRUE, TRUE, FALSE, FALSE, 7),
('JF', 'Jour férié', 'ferie', TRUE, TRUE, FALSE, FALSE, NULL),
('CSS', 'Congé sans solde', 'sans-solde', FALSE, FALSE, FALSE, FALSE, 15),
-- Santé et accidents — catégories particulières au sens du RGPD
('MAL', 'Arrêt maladie', 'maladie', FALSE, FALSE, TRUE, TRUE, NULL),
('MP', 'Maladie professionnelle', 'maladie', FALSE, TRUE, TRUE, TRUE, NULL),
('AT', 'Accident du travail', 'maladie', FALSE, TRUE, TRUE, TRUE, NULL),
('AT_TRAJET', 'Accident de trajet', 'maladie', FALSE, TRUE, TRUE, TRUE, NULL),
('VM', 'Visite médicale', 'maladie', TRUE, TRUE, FALSE, TRUE, NULL),
-- Famille
('MAT', 'Congé maternité', 'famille', FALSE, TRUE, TRUE, TRUE, NULL),
('PAT', 'Congé paternité', 'famille', FALSE, TRUE, TRUE, TRUE, NULL),
('PAR', 'Congé parental', 'famille', FALSE, FALSE, FALSE, TRUE, 30),
('NAISS', 'Congé supplémentaire de naissance', 'famille', TRUE, TRUE, FALSE, TRUE, NULL),
('EVT_FAM', 'Évènement familial', 'famille', TRUE, TRUE, FALSE, TRUE, NULL),
-- Formation et obligations
('FORM', 'Formation', 'formation', TRUE, TRUE, FALSE, FALSE, 15),
('EXAM', 'Congé pour examen', 'formation', TRUE, TRUE, FALSE, TRUE, 15),
-- Absences non planifiées et sanctions
('ABS_JUS', 'Absence justifiée', 'sans-solde', FALSE, FALSE, FALSE, TRUE, NULL),
('ABS_INJ', 'Absence injustifiée', 'sans-solde', FALSE, FALSE, FALSE, FALSE, NULL),
('INDISPO', 'Indisponibilité ponctuelle', 'sans-solde', FALSE, FALSE, FALSE, FALSE, NULL),
('MAP_D', 'Mise à pied disciplinaire', 'sanction', FALSE, FALSE, FALSE, TRUE, NULL),
('MAP_C', 'Mise à pied conservatoire', 'sanction', FALSE, FALSE, FALSE, TRUE, NULL)
) AS t ("code", "name", "colorKey", "isPaid", "accrual", "social", "justify", "notice")
WHERE NOT EXISTS (
SELECT 1 FROM "AbsenceType" existing WHERE existing."accountId" = a."id"
);
ALTER TABLE "AbsenceType" FORCE ROW LEVEL SECURITY;
+16
View File
@@ -33,6 +33,22 @@ export function RequestForm({
empty,
);
// Un menu « Type » vide ne se voit pas : le formulaire paraît complet, la
// demande part sans type et échoue au serveur. Dire que le référentiel manque
// coûte moins qu'une saisie qui ne pouvait pas aboutir.
if (absenceTypes.length === 0) {
return (
<p className="rounded-3 border border-line-2 bg-surface-2 p-3 text-sm text-ink-2">
Aucun type d’absence n’est défini : une demande ne peut pas être
qualifiée, donc pas enregistrée. Le référentiel se pose depuis{' '}
<a className="underline" href="/reglages/types-absence">
Réglages · Types d’absence
</a>
.
</p>
);
}
return (
<PersistentForm
resetAfter={state.ok ? state : null}