Compare commits

...
Author SHA1 Message Date
jmartinandCursor d7783badca feat(#193): table absences_garde + drop evenements.
Vérité métier absences/congés/arrêt par placement (périodes, expire_at).
Entité TypeORM, BDD.sql, docs enums/FK, seed et verify alignés.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-24 11:09:39 +02:00
8 changed files with 238 additions and 152 deletions
@@ -0,0 +1,88 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { AmChildren } from './am_children.entity';
import { Users } from './users.entity';
export enum TypeAbsenceGardeType {
ABSENCE_ENFANT = 'absence_enfant',
CONGE_AM = 'conge_am',
ARRET_MALADIE_AM = 'arret_maladie_am',
}
export enum StatutAbsenceGardeType {
EN_ATTENTE = 'en_attente',
ACCEPTE = 'accepte',
REFUSE = 'refuse',
}
/**
* Vérité métier des absences / congés / arrêts par couple AM↔enfant.
* Les cartes (module ultérieur) collectent ; cette table stocke.
* Ticket #193.
*/
@Entity('absences_garde', { schema: 'public' })
export class AbsencesGarde {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'id_placement', type: 'uuid' })
id_placement: string;
@ManyToOne(() => AmChildren, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'id_placement', referencedColumnName: 'id' })
placement: AmChildren;
@Column({
type: 'enum',
enum: TypeAbsenceGardeType,
enumName: 'type_absence_garde_type',
name: 'type',
})
type: TypeAbsenceGardeType;
@Column({ name: 'date_debut', type: 'date' })
date_debut: string;
@Column({ name: 'date_fin', type: 'date' })
date_fin: string;
@Column({
type: 'enum',
enum: StatutAbsenceGardeType,
enumName: 'statut_absence_garde_type',
name: 'statut',
default: StatutAbsenceGardeType.EN_ATTENTE,
})
statut: StatutAbsenceGardeType;
/** Purge auto pour en_attente/refuse ; accepte → infinity. */
@Column({ name: 'expire_at', type: 'timestamptz' })
expire_at: Date;
@Column({ name: 'cree_par', type: 'uuid', nullable: true })
cree_par?: string;
@ManyToOne(() => Users, { nullable: true, onDelete: 'SET NULL' })
@JoinColumn({ name: 'cree_par', referencedColumnName: 'id' })
createdBy?: Users;
/** Réf. carte de collecte (FK quand module Cartes existera). */
@Column({ name: 'id_card_instance', type: 'uuid', nullable: true })
id_card_instance?: string;
@Column({ name: 'motif', type: 'text', nullable: true })
motif?: string;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
cree_le: Date;
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
modifie_le: Date;
}
-79
View File
@@ -1,79 +0,0 @@
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { Children } from "./children.entity";
import { Users } from "./users.entity";
import { Parents } from "./parents.entity";
export enum TypeEvenementType {
ABSENCE_ENFANT = 'absence_enfant',
CONGE_AM = 'conge_am',
CONGE_PARENT = 'conge_parent',
ARRET_MALADIE_AM = 'arret_maladie_am',
EVENEMENT_RPE = 'evenement_rpe',
}
export enum StatutEvenementType {
PROPOSE = 'propose',
VALIDE = 'valide',
REFUSE = 'refuse',
}
@Entity('evenements')
export class Evenement {
// Define your columns and relationships here
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({
type: 'enum',
enum: TypeEvenementType,
enumName: 'type_evenement_type',
name: 'type'
})
type: TypeEvenementType;
@ManyToOne(() => Children, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'id_enfant', referencedColumnName: 'id' })
child?: Children;
@ManyToOne(() => Users, { nullable: true })
@JoinColumn({ name: 'id_am', referencedColumnName: 'id' })
assistanteMaternelle?: Users;
@ManyToOne(() => Parents, { nullable: true })
@JoinColumn({ name: 'id_parent', referencedColumnName: 'user_id' })
parent?: Parents;
@ManyToOne(() => Users, { nullable: true })
@JoinColumn({ name: 'cree_par', referencedColumnName: 'id' })
created_by?: Users;
@Column({ type: 'timestamptz', nullable: true, name: 'date_debut' })
start_date?: Date;
@Column({ type: 'timestamptz', nullable: true, name: 'date_fin' })
end_date?: Date;
@Column({ type: 'text', nullable: true, name: 'commentaires' })
comments?: string;
@Column({
type: 'enum',
enum: StatutEvenementType,
enumName: 'statut_evenement_type',
name: 'statut',
default: StatutEvenementType.PROPOSE
})
status: StatutEvenementType;
@Column({type: 'timestamptz', nullable: true, name: 'delai_grace'})
grace_deadline?: Date;
@Column({type: 'boolean', default: false, name: 'urgent'})
urgent: boolean;
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
created_at: Date;
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
updated_at: Date;
}
+31 -20
View File
@@ -35,13 +35,15 @@ DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_avenant_type') THEN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_avenant_type') THEN
CREATE TYPE statut_avenant_type AS ENUM ('propose', 'accepte', 'refuse'); CREATE TYPE statut_avenant_type AS ENUM ('propose', 'accepte', 'refuse');
END IF; END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'type_evenement_type') THEN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'type_absence_garde_type') THEN
CREATE TYPE type_evenement_type AS ENUM ( CREATE TYPE type_absence_garde_type AS ENUM (
'absence_enfant', 'conge_am', 'conge_parent', 'arret_maladie_am', 'evenement_rpe' 'absence_enfant', 'conge_am', 'arret_maladie_am'
); );
END IF; END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_evenement_type') THEN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_absence_garde_type') THEN
CREATE TYPE statut_evenement_type AS ENUM ('propose', 'valide', 'refuse'); CREATE TYPE statut_absence_garde_type AS ENUM (
'en_attente', 'accepte', 'refuse'
);
END IF; END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_validation_type') THEN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_validation_type') THEN
CREATE TYPE statut_validation_type AS ENUM ('en_attente', 'valide', 'refuse'); CREATE TYPE statut_validation_type AS ENUM ('en_attente', 'valide', 'refuse');
@@ -307,25 +309,34 @@ CREATE TABLE avenants_contrats (
); );
-- ========================================================== -- ==========================================================
-- Table : evenements -- Table : absences_garde (vérité métier absences/congés/arrêt)
-- 1 ligne = 1 période ; rattachement = placement AM↔enfant (#193)
-- ========================================================== -- ==========================================================
CREATE TABLE evenements ( CREATE TABLE absences_garde (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type type_evenement_type, id_placement UUID NOT NULL
id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE, REFERENCES enfants_assistantes_maternelles(id) ON DELETE CASCADE,
id_am UUID REFERENCES utilisateurs(id), type type_absence_garde_type NOT NULL,
id_parent UUID REFERENCES parents(id_utilisateur), date_debut DATE NOT NULL,
cree_par UUID REFERENCES utilisateurs(id), date_fin DATE NOT NULL,
date_debut TIMESTAMPTZ, statut statut_absence_garde_type NOT NULL DEFAULT 'en_attente',
date_fin TIMESTAMPTZ, expire_at TIMESTAMPTZ NOT NULL,
commentaires TEXT, cree_par UUID REFERENCES utilisateurs(id) ON DELETE SET NULL,
statut statut_evenement_type DEFAULT 'propose', id_card_instance UUID,
delai_grace TIMESTAMPTZ, motif TEXT,
urgent BOOLEAN DEFAULT false, cree_le TIMESTAMPTZ NOT NULL DEFAULT now(),
cree_le TIMESTAMPTZ DEFAULT now(), modifie_le TIMESTAMPTZ NOT NULL DEFAULT now(),
modifie_le TIMESTAMPTZ DEFAULT now() CONSTRAINT chk_absences_garde_dates CHECK (date_fin >= date_debut)
); );
CREATE INDEX idx_absences_garde_placement_dates
ON absences_garde (id_placement, date_debut, date_fin);
CREATE INDEX idx_absences_garde_placement_type_statut
ON absences_garde (id_placement, type, statut);
CREATE INDEX idx_absences_garde_expire_at
ON absences_garde (expire_at)
WHERE statut IN ('en_attente', 'refuse');
-- ========================================================== -- ==========================================================
-- Table : signalements_bugs -- Table : signalements_bugs
-- ========================================================== -- ==========================================================
+10 -10
View File
@@ -104,31 +104,31 @@ Ce document recense **toutes les valeurs énumérées** utilisées dans la base
--- ---
## 7) Type dévénement — `type` ## 7) Type dabsence de garde — `type`
**Tables/colonnes** : `evenements.type` **Tables/colonnes** : `absences_garde.type`
**Valeurs autorisées** : **Valeurs autorisées** :
| Valeur | Description | | Valeur | Description |
|---|---| |---|---|
| `absence_enfant` | Enfant absent | | `absence_enfant` | Absence déclarée par un parent |
| `conge_am` | Congé de lassistante maternelle | | `conge_am` | Congé de lassistante maternelle |
| `conge_parent` | Congé du parent |
| `arret_maladie_am` | Arrêt maladie AM | | `arret_maladie_am` | Arrêt maladie AM |
| `evenement_rpe` | Événement RPE |
> Remplace lancien enum `type_evenement_type` / table `evenements` (supprimés ticket #193).
--- ---
## 8) Statut dévénement — `statut` ## 8) Statut dabsence de garde — `statut`
**Tables/colonnes** : `evenements.statut` **Tables/colonnes** : `absences_garde.statut`
**Valeurs autorisées** : **Valeurs autorisées** :
| Valeur | Description | | Valeur | Description |
|---|---| |---|---|
| `propose` | Événement proposé | | `en_attente` | En attente de validation / ack |
| `valide` | Événement validé | | `accepte` | Accepté / acté (conservé en historique métier) |
| `rejete` | Événement refusé | | `refuse` | Refusé (temporaire ; purge TTL) |
--- ---
+6 -10
View File
@@ -42,10 +42,8 @@ Documenter, de façon unique et partagée, les règles de suppression/mise à jo
| **contrats(id_dossier)**`dossiers(id)` | **CASCADE** | 1:1, contrat détruit si dossier supprimé | | **contrats(id_dossier)**`dossiers(id)` | **CASCADE** | 1:1, contrat détruit si dossier supprimé |
| **avenants_contrats(id_contrat)**`contrats(id)` | **CASCADE** | Avenants détruits avec le contrat | | **avenants_contrats(id_contrat)**`contrats(id)` | **CASCADE** | Avenants détruits avec le contrat |
| **avenants_contrats(initie_par)**`utilisateurs(id)` | **SET NULL** | Historiser lavenant sans bloquer | | **avenants_contrats(initie_par)**`utilisateurs(id)` | **SET NULL** | Historiser lavenant sans bloquer |
| **evenements(id_enfant)**`enfants(id)` | **CASCADE** | Événements nont plus de sens | | **absences_garde(id_placement)**`enfants_assistantes_maternelles(id)` | **CASCADE** | Plus de couple = plus dabsences |
| **evenements(id_am)**`utilisateurs(id)` | **SET NULL** | Garder la trace même si AM supprimée | | **absences_garde(cree_par)**`utilisateurs(id)` | **SET NULL** | Conserver la période si user supprimé |
| **evenements(id_parent)**`parents(id_utilisateur)` | **SET NULL** | Garder la trace si parent supprimé |
| **evenements(cree_par)**`utilisateurs(id)` | **SET NULL** | Conserver lhistorique de création |
| **signalements_bugs(id_utilisateur)**`utilisateurs(id)` | **SET NULL** | Conserver le ticket même si compte supprimé | | **signalements_bugs(id_utilisateur)**`utilisateurs(id)` | **SET NULL** | Conserver le ticket même si compte supprimé |
| **uploads(id_utilisateur)**`utilisateurs(id)` | **SET NULL** | Fichier reste référencé sans lauteur | | **uploads(id_utilisateur)**`utilisateurs(id)` | **SET NULL** | Fichier reste référencé sans lauteur |
| **notifications(id_utilisateur)**`utilisateurs(id)` | **CASCADE** | Notifications propres à lutilisateur | | **notifications(id_utilisateur)**`utilisateurs(id)` | **CASCADE** | Notifications propres à lutilisateur |
@@ -79,10 +77,8 @@ Documenter, de façon unique et partagée, les règles de suppression/mise à jo
- `avenants_contrats.initie_par`**SET NULL** - `avenants_contrats.initie_par`**SET NULL**
### Événements ### Événements
- `evenements.id_enfant`**CASCADE** - `absences_garde.id_placement`**CASCADE**
- `evenements.id_am`**SET NULL** - `absences_garde.cree_par`**SET NULL**
- `evenements.id_parent`**SET NULL**
- `evenements.cree_par`**SET NULL**
### Divers ### Divers
- `signalements_bugs.id_utilisateur`**SET NULL** - `signalements_bugs.id_utilisateur`**SET NULL**
@@ -109,11 +105,11 @@ Documenter, de façon unique et partagée, les règles de suppression/mise à jo
4. **Suppression dun enfant** 4. **Suppression dun enfant**
- Supprimer `enfants(id=childA)` - Supprimer `enfants(id=childA)`
- Attendu : `enfants_parents` (CASCADE), `dossiers` du childA (CASCADE), `evenements` du childA (CASCADE). - Attendu : `enfants_parents` (CASCADE), `dossiers` du childA (CASCADE), `absences_garde` des placements du childA (CASCADE via EAM).
5. **Suppression dun utilisateur AM** 5. **Suppression dun utilisateur AM**
- Supprimer `utilisateurs(id=amB)` - Supprimer `utilisateurs(id=amB)`
- Attendu : `evenements.id_am` devient **NULL** (historique conservé). - Attendu : absences liées via placement AM (CASCADE si le lien EAM est retiré).
--- ---
@@ -0,0 +1,85 @@
-- Ticket #193 — Back métier absences / congés / arrêt (périodes par placement)
-- Idempotent : safe à rejouer.
-- Remplace la table legacy `evenements` (mal conçue, non utilisée en API).
-- ---------------------------------------------------------------------------
-- Enums
-- ---------------------------------------------------------------------------
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'type_absence_garde_type') THEN
CREATE TYPE type_absence_garde_type AS ENUM (
'absence_enfant',
'conge_am',
'arret_maladie_am'
);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_absence_garde_type') THEN
CREATE TYPE statut_absence_garde_type AS ENUM (
'en_attente',
'accepte',
'refuse'
);
END IF;
END $$;
-- ---------------------------------------------------------------------------
-- Table absences_garde (1 ligne = 1 période)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS absences_garde (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_placement UUID NOT NULL
REFERENCES enfants_assistantes_maternelles(id) ON DELETE CASCADE,
type type_absence_garde_type NOT NULL,
date_debut DATE NOT NULL,
date_fin DATE NOT NULL,
statut statut_absence_garde_type NOT NULL DEFAULT 'en_attente',
-- TTL purge : obligatoire ; pour statut=accepte utiliser 'infinity' (pas de purge métier)
expire_at TIMESTAMPTZ NOT NULL,
cree_par UUID REFERENCES utilisateurs(id) ON DELETE SET NULL,
id_card_instance UUID NULL,
motif TEXT,
cree_le TIMESTAMPTZ NOT NULL DEFAULT now(),
modifie_le TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT chk_absences_garde_dates CHECK (date_fin >= date_debut)
);
CREATE INDEX IF NOT EXISTS idx_absences_garde_placement_dates
ON absences_garde (id_placement, date_debut, date_fin);
CREATE INDEX IF NOT EXISTS idx_absences_garde_placement_type_statut
ON absences_garde (id_placement, type, statut);
CREATE INDEX IF NOT EXISTS idx_absences_garde_expire_at
ON absences_garde (expire_at)
WHERE statut IN ('en_attente', 'refuse');
COMMENT ON TABLE absences_garde IS
'Vérité métier des absences/congés/arrêts par couple AM↔enfant (placement). Les cartes collectent ; cette table stocke.';
COMMENT ON COLUMN absences_garde.expire_at IS
'Purge auto (job ultérieur) pour en_attente/refuse. accepte → infinity.';
COMMENT ON COLUMN absences_garde.id_card_instance IS
'Réf. carte de collecte (FK cards quand le module Cartes existera).';
-- ---------------------------------------------------------------------------
-- Drop legacy evenements (+ enums si plus référencés)
-- ---------------------------------------------------------------------------
DROP TABLE IF EXISTS evenements CASCADE;
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'type_evenement_type')
AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE udt_name = 'type_evenement_type'
) THEN
DROP TYPE type_evenement_type;
END IF;
IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_evenement_type')
AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE udt_name = 'statut_evenement_type'
) THEN
DROP TYPE statut_evenement_type;
END IF;
END $$;
+2 -16
View File
@@ -161,23 +161,9 @@ VALUES (
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
-- ------------------------------------------------------------ -- ------------------------------------------------------------
-- Événement (absence enfant) -- Absences_garde : seeds quand placements EAM présents (API / ticket peuplement)
-- Legacy evenements supprimée (#193).
-- ------------------------------------------------------------ -- ------------------------------------------------------------
INSERT INTO evenements (id, type, id_enfant, id_am, id_parent, cree_par, date_debut, date_fin, commentaires, statut, urgence)
VALUES (
'e0000000-0000-0000-0000-000000000001',
'absence_enfant',
'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'66666666-6666-6666-6666-666666666666',
'33333333-3333-3333-3333-333333333333',
'33333333-3333-3333-3333-333333333333',
'2025-09-12',
'2025-09-12',
'Enfant malade (rhume).',
'propose',
false
)
ON CONFLICT (id) DO NOTHING;
-- ------------------------------------------------------------ -- ------------------------------------------------------------
-- Upload (justificatif lié au dossier) -- Upload (justificatif lié au dossier)
+16 -17
View File
@@ -79,12 +79,11 @@ LEFT JOIN avenants_contrats a ON a.id_contrat = c.id
GROUP BY c.id, c.id_dossier, c.statut GROUP BY c.id, c.id_dossier, c.statut
ORDER BY c.cree_le DESC; ORDER BY c.cree_le DESC;
\echo '=== 9) Evénements par enfant (30 derniers jours) ==============' \echo '=== 9) Absences_garde (30 derniers jours) ====================='
SELECT ev.id, ev.type, ev.id_enfant, e.prenom AS enfant, ev.date_debut, ev.date_fin, ev.statut SELECT ag.id, ag.type, ag.id_placement, ag.date_debut, ag.date_fin, ag.statut, ag.expire_at
FROM evenements ev FROM absences_garde ag
JOIN enfants e ON e.id = ev.id_enfant WHERE ag.date_debut >= (NOW()::date - INTERVAL '30 days')
WHERE ev.date_debut >= (NOW()::date - INTERVAL '30 days') ORDER BY ag.date_debut DESC;
ORDER BY ev.date_debut DESC;
\echo '=== 10) Uploads & notifications récentes =======================' \echo '=== 10) Uploads & notifications récentes ======================='
SELECT u.courriel, up.fichier_url, up.type_fichier, up.cree_le SELECT u.courriel, up.fichier_url, up.type_fichier, up.cree_le
@@ -135,11 +134,11 @@ FROM avenants_contrats a
LEFT JOIN contrats c ON c.id = a.id_contrat LEFT JOIN contrats c ON c.id = a.id_contrat
WHERE c.id IS NULL; WHERE c.id IS NULL;
-- Evénements sans enfant -- Absences sans placement
SELECT ev.* SELECT ag.*
FROM evenements ev FROM absences_garde ag
LEFT JOIN enfants e ON e.id = ev.id_enfant LEFT JOIN enfants_assistantes_maternelles eam ON eam.id = ag.id_placement
WHERE e.id IS NULL; WHERE eam.id IS NULL;
\echo '=== 13) Performance : EXPLAIN sur requêtes clés ===============' \echo '=== 13) Performance : EXPLAIN sur requêtes clés ==============='
@@ -151,12 +150,12 @@ WHERE m.id_dossier = 'dddddddd-dddd-dddd-dddd-dddddddddddd'
ORDER BY m.cree_le DESC ORDER BY m.cree_le DESC
LIMIT 20; LIMIT 20;
-- Evénements par enfant et période (idx_evenements_id_enfant_date_debut) -- Absences par placement et période (idx_absences_garde_placement_dates)
EXPLAIN ANALYZE EXPLAIN ANALYZE
SELECT ev.* SELECT ag.*
FROM evenements ev FROM absences_garde ag
WHERE ev.id_enfant = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' WHERE ag.id_placement IS NOT NULL
AND ev.date_debut >= '2025-01-01'; AND ag.date_debut >= '2025-01-01';
-- Notifications non lues (idx_notifications_user_lu_cree_le) -- Notifications non lues (idx_notifications_user_lu_cree_le)
EXPLAIN ANALYZE EXPLAIN ANALYZE
@@ -199,7 +198,7 @@ SELECT
(SELECT COUNT(*) FROM messages) AS nb_messages, (SELECT COUNT(*) FROM messages) AS nb_messages,
(SELECT COUNT(*) FROM contrats) AS nb_contrats, (SELECT COUNT(*) FROM contrats) AS nb_contrats,
(SELECT COUNT(*) FROM avenants_contrats) AS nb_avenants, (SELECT COUNT(*) FROM avenants_contrats) AS nb_avenants,
(SELECT COUNT(*) FROM evenements) AS nb_evenements, (SELECT COUNT(*) FROM absences_garde) AS nb_absences_garde,
(SELECT COUNT(*) FROM uploads) AS nb_uploads, (SELECT COUNT(*) FROM uploads) AS nb_uploads,
(SELECT COUNT(*) FROM notifications) AS nb_notifications, (SELECT COUNT(*) FROM notifications) AS nb_notifications,
(SELECT COUNT(*) FROM validations) AS nb_validations; (SELECT COUNT(*) FROM validations) AS nb_validations;