Julien Martin 52e40d0001 feat(#131): back placement AM↔enfant + BDD garde/sans_garde
- API PATCH …/fiche, POST/DELETE …/enfants/:enfantId, GET avec amChildren
- Table enfants_assistantes_maternelles (option D, 1 garde active/enfant)
- Statuts enfant: garde/sans_garde remplacent actif (inscription → sans_garde)
- BDD.sql canonique réécrit; migration pour BDD existantes
- Seed test: hash bcrypt corrigé pour mot de passe « password »

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 14:05:39 +02:00

471 lines
19 KiB
SQL

-- ==========================================================
-- P'titsPas — Schéma PostgreSQL (création from scratch)
-- Fichier canonique : toute nouvelle BDD doit partir d'ici.
-- Migrations dans database/migrations/ : BDD existantes uniquement.
-- ==========================================================
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- ==========================================================
-- ENUMS
-- ==========================================================
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'role_type') THEN
CREATE TYPE role_type AS ENUM (
'parent', 'gestionnaire', 'super_admin', 'administrateur', 'assistante_maternelle'
);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'genre_type') THEN
CREATE TYPE genre_type AS ENUM ('H', 'F', 'Autre');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_utilisateur_type') THEN
CREATE TYPE statut_utilisateur_type AS ENUM ('en_attente', 'actif', 'suspendu', 'refuse');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_enfant_type') THEN
CREATE TYPE statut_enfant_type AS ENUM ('a_naitre', 'garde', 'sans_garde', 'scolarise');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_dossier_type') THEN
CREATE TYPE statut_dossier_type AS ENUM ('envoye', 'accepte', 'refuse');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_contrat_type') THEN
CREATE TYPE statut_contrat_type AS ENUM (
'brouillon', 'en_attente_signature', 'valide', 'resilie'
);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_avenant_type') THEN
CREATE TYPE statut_avenant_type AS ENUM ('propose', 'accepte', 'refuse');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'type_evenement_type') THEN
CREATE TYPE type_evenement_type AS ENUM (
'absence_enfant', 'conge_am', 'conge_parent', 'arret_maladie_am', 'evenement_rpe'
);
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'statut_evenement_type') THEN
CREATE TYPE statut_evenement_type AS ENUM ('propose', 'valide', 'refuse');
END IF;
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');
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'situation_familiale_type') THEN
CREATE TYPE situation_familiale_type AS ENUM (
'celibataire', 'marie', 'concubinage', 'pacse', 'separe', 'divorce', 'veuf', 'parent_isole'
);
END IF;
END $$;
-- ==========================================================
-- Table : relais (avant utilisateurs — FK relais_id)
-- ==========================================================
CREATE TABLE relais (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nom VARCHAR(255) NOT NULL,
adresse TEXT NOT NULL,
horaires_ouverture JSONB,
ligne_fixe VARCHAR(20),
actif BOOLEAN DEFAULT true,
notes TEXT,
cree_le TIMESTAMPTZ DEFAULT now(),
modifie_le TIMESTAMPTZ DEFAULT now()
);
-- ==========================================================
-- Table : utilisateurs
-- ==========================================================
CREATE TABLE utilisateurs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
password TEXT,
prenom VARCHAR(100),
nom VARCHAR(100),
genre genre_type,
role role_type NOT NULL,
statut statut_utilisateur_type DEFAULT 'en_attente',
telephone VARCHAR(20),
adresse TEXT,
date_naissance DATE,
lieu_naissance_ville VARCHAR(100),
lieu_naissance_pays VARCHAR(100),
photo_url TEXT,
consentement_photo BOOLEAN DEFAULT false,
date_consentement_photo TIMESTAMPTZ,
token_creation_mdp VARCHAR(255),
token_creation_mdp_expire_le TIMESTAMPTZ,
password_reset_token VARCHAR(255),
password_reset_expires TIMESTAMPTZ,
token_reprise VARCHAR(255),
token_reprise_expire_le TIMESTAMPTZ,
changement_mdp_obligatoire BOOLEAN DEFAULT false,
numero_dossier VARCHAR(20),
cgu_version_acceptee INTEGER,
cgu_acceptee_le TIMESTAMPTZ,
privacy_version_acceptee INTEGER,
privacy_acceptee_le TIMESTAMPTZ,
relais_id UUID REFERENCES relais(id) ON DELETE SET NULL,
cree_le TIMESTAMPTZ DEFAULT now(),
modifie_le TIMESTAMPTZ DEFAULT now(),
ville VARCHAR(150),
code_postal VARCHAR(10),
profession VARCHAR(150),
situation_familiale situation_familiale_type
);
CREATE INDEX idx_utilisateurs_token_creation_mdp
ON utilisateurs(token_creation_mdp)
WHERE token_creation_mdp IS NOT NULL;
CREATE INDEX idx_utilisateurs_password_reset_token
ON utilisateurs(password_reset_token)
WHERE password_reset_token IS NOT NULL;
CREATE INDEX idx_utilisateurs_token_reprise
ON utilisateurs(token_reprise)
WHERE token_reprise IS NOT NULL;
CREATE INDEX idx_utilisateurs_numero_dossier
ON utilisateurs(numero_dossier)
WHERE numero_dossier IS NOT NULL;
-- ==========================================================
-- Table : assistantes_maternelles
-- ==========================================================
CREATE TABLE assistantes_maternelles (
id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE,
numero_agrement VARCHAR(50),
nir_chiffre CHAR(15) NOT NULL,
nb_max_enfants INT,
biographie TEXT,
disponible BOOLEAN DEFAULT true,
ville_residence VARCHAR(100),
date_agrement DATE,
annee_experience SMALLINT,
specialite VARCHAR(100),
place_disponible INT,
numero_dossier VARCHAR(20)
);
CREATE INDEX idx_assistantes_maternelles_numero_dossier
ON assistantes_maternelles(numero_dossier)
WHERE numero_dossier IS NOT NULL;
-- ==========================================================
-- Table : parents
-- ==========================================================
CREATE TABLE parents (
id_utilisateur UUID PRIMARY KEY REFERENCES utilisateurs(id) ON DELETE CASCADE,
id_co_parent UUID REFERENCES utilisateurs(id),
numero_dossier VARCHAR(20)
);
CREATE INDEX idx_parents_numero_dossier
ON parents(numero_dossier)
WHERE numero_dossier IS NOT NULL;
-- ==========================================================
-- Table : enfants
-- ==========================================================
CREATE TABLE enfants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
statut statut_enfant_type,
prenom VARCHAR(100),
nom VARCHAR(100),
genre genre_type NOT NULL,
date_naissance DATE,
date_prevue_naissance DATE,
photo_url TEXT,
consentement_photo BOOLEAN DEFAULT false,
date_consentement_photo TIMESTAMPTZ,
est_multiple BOOLEAN DEFAULT false
);
-- ==========================================================
-- Table : enfants_parents
-- ==========================================================
CREATE TABLE enfants_parents (
id_parent UUID REFERENCES parents(id_utilisateur) ON DELETE CASCADE,
id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE,
PRIMARY KEY (id_parent, id_enfant)
);
-- ==========================================================
-- Table : enfants_assistantes_maternelles (placement AM ↔ enfant — #131)
-- ==========================================================
CREATE TABLE enfants_assistantes_maternelles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_am UUID NOT NULL REFERENCES assistantes_maternelles(id_utilisateur) ON DELETE CASCADE,
id_enfant UUID NOT NULL REFERENCES enfants(id) ON DELETE CASCADE,
date_debut DATE NOT NULL DEFAULT CURRENT_DATE,
date_fin DATE NULL,
cree_le TIMESTAMPTZ NOT NULL DEFAULT now(),
cree_par UUID REFERENCES utilisateurs(id) ON DELETE SET NULL
);
CREATE INDEX idx_eam_am ON enfants_assistantes_maternelles(id_am);
CREATE INDEX idx_eam_enfant ON enfants_assistantes_maternelles(id_enfant);
CREATE UNIQUE INDEX uq_enfant_garde_active
ON enfants_assistantes_maternelles (id_enfant)
WHERE date_fin IS NULL;
-- ==========================================================
-- Table : dossier_famille (inscription parent — ticket #119)
-- ==========================================================
CREATE TABLE dossier_famille (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
numero_dossier VARCHAR(20) NOT NULL,
id_parent UUID NOT NULL REFERENCES parents(id_utilisateur) ON DELETE CASCADE,
presentation TEXT,
statut statut_dossier_type NOT NULL DEFAULT 'envoye',
cree_le TIMESTAMPTZ NOT NULL DEFAULT now(),
modifie_le TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_dossier_famille_numero ON dossier_famille(numero_dossier);
CREATE INDEX idx_dossier_famille_id_parent ON dossier_famille(id_parent);
-- ==========================================================
-- Table : dossier_famille_enfants
-- ==========================================================
CREATE TABLE dossier_famille_enfants (
id_dossier_famille UUID NOT NULL REFERENCES dossier_famille(id) ON DELETE CASCADE,
id_enfant UUID NOT NULL REFERENCES enfants(id) ON DELETE CASCADE,
PRIMARY KEY (id_dossier_famille, id_enfant)
);
CREATE INDEX idx_dossier_famille_enfants_enfant ON dossier_famille_enfants(id_enfant);
-- ==========================================================
-- Table : dossiers
-- ==========================================================
CREATE TABLE dossiers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_parent UUID REFERENCES parents(id_utilisateur) ON DELETE CASCADE,
id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE,
presentation TEXT,
type_contrat VARCHAR(50),
repas BOOLEAN DEFAULT false,
budget NUMERIC(10,2),
planning_souhaite JSONB,
statut statut_dossier_type DEFAULT 'envoye',
cree_le TIMESTAMPTZ DEFAULT now(),
modifie_le TIMESTAMPTZ DEFAULT now()
);
-- ==========================================================
-- Table : messages
-- ==========================================================
CREATE TABLE messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_dossier UUID REFERENCES dossiers(id) ON DELETE CASCADE,
id_expediteur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE,
contenu TEXT,
re_redige_par_ia BOOLEAN DEFAULT false,
cree_le TIMESTAMPTZ DEFAULT now()
);
-- ==========================================================
-- Table : contrats
-- ==========================================================
CREATE TABLE contrats (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_dossier UUID UNIQUE REFERENCES dossiers(id) ON DELETE CASCADE,
planning JSONB,
tarif_horaire NUMERIC(6,2),
indemnites_repas NUMERIC(6,2),
date_debut DATE,
statut statut_contrat_type DEFAULT 'brouillon',
signe_parent BOOLEAN DEFAULT false,
signe_am BOOLEAN DEFAULT false,
finalise_le TIMESTAMPTZ,
cree_le TIMESTAMPTZ DEFAULT now(),
modifie_le TIMESTAMPTZ DEFAULT now()
);
-- ==========================================================
-- Table : avenants_contrats
-- ==========================================================
CREATE TABLE avenants_contrats (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_contrat UUID REFERENCES contrats(id) ON DELETE CASCADE,
modifications JSONB,
initie_par UUID REFERENCES utilisateurs(id),
statut statut_avenant_type DEFAULT 'propose',
cree_le TIMESTAMPTZ DEFAULT now(),
modifie_le TIMESTAMPTZ DEFAULT now()
);
-- ==========================================================
-- Table : evenements
-- ==========================================================
CREATE TABLE evenements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type type_evenement_type,
id_enfant UUID REFERENCES enfants(id) ON DELETE CASCADE,
id_am UUID REFERENCES utilisateurs(id),
id_parent UUID REFERENCES parents(id_utilisateur),
cree_par UUID REFERENCES utilisateurs(id),
date_debut TIMESTAMPTZ,
date_fin TIMESTAMPTZ,
commentaires TEXT,
statut statut_evenement_type DEFAULT 'propose',
delai_grace TIMESTAMPTZ,
urgent BOOLEAN DEFAULT false,
cree_le TIMESTAMPTZ DEFAULT now(),
modifie_le TIMESTAMPTZ DEFAULT now()
);
-- ==========================================================
-- Table : signalements_bugs
-- ==========================================================
CREATE TABLE signalements_bugs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_utilisateur UUID REFERENCES utilisateurs(id),
description TEXT,
cree_le TIMESTAMPTZ DEFAULT now()
);
-- ==========================================================
-- Table : uploads
-- ==========================================================
CREATE TABLE uploads (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE SET NULL,
fichier_url TEXT NOT NULL,
type VARCHAR(50),
cree_le TIMESTAMPTZ DEFAULT now()
);
-- ==========================================================
-- Table : notifications
-- ==========================================================
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE,
contenu TEXT,
lu BOOLEAN DEFAULT false,
cree_le TIMESTAMPTZ DEFAULT now()
);
-- ==========================================================
-- Table : validations
-- ==========================================================
CREATE TABLE validations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE SET NULL,
type VARCHAR(50),
statut statut_validation_type DEFAULT 'en_attente',
commentaire TEXT,
valide_par UUID REFERENCES utilisateurs(id) ON DELETE SET NULL,
cree_le TIMESTAMPTZ DEFAULT now(),
modifie_le TIMESTAMPTZ DEFAULT now()
);
-- ==========================================================
-- Table : configuration
-- ==========================================================
CREATE TABLE configuration (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cle VARCHAR(100) UNIQUE NOT NULL,
valeur TEXT,
type VARCHAR(50) NOT NULL,
categorie VARCHAR(50),
description TEXT,
modifie_le TIMESTAMPTZ DEFAULT now(),
modifie_par UUID REFERENCES utilisateurs(id)
);
CREATE INDEX idx_configuration_cle ON configuration(cle);
CREATE INDEX idx_configuration_categorie ON configuration(categorie);
INSERT INTO configuration (cle, valeur, type, categorie, description) VALUES
('smtp_host', 'localhost', 'string', 'email', 'Serveur SMTP (ex: mail.mairie-bezons.fr, smtp.gmail.com)'),
('smtp_port', '25', 'number', 'email', 'Port SMTP (25, 465, 587)'),
('smtp_secure', 'false', 'boolean', 'email', 'Utiliser SSL/TLS (true pour port 465)'),
('smtp_auth_required', 'false', 'boolean', 'email', 'Authentification SMTP requise'),
('smtp_user', '', 'string', 'email', 'Utilisateur SMTP (si authentification requise)'),
('smtp_password', '', 'encrypted', 'email', 'Mot de passe SMTP (chiffré en AES-256)'),
('email_from_name', 'P''titsPas', 'string', 'email', 'Nom de l''expéditeur affiché dans les emails'),
('email_from_address', 'no-reply@ptits-pas.fr', 'string', 'email', 'Adresse email de l''expéditeur'),
('app_name', 'P''titsPas', 'string', 'app', 'Nom de l''application (affiché dans l''interface)'),
('app_url', 'https://app.ptits-pas.fr', 'string', 'app', 'URL publique de l''application (pour les liens dans emails)'),
('app_logo_url', '/assets/logo.png', 'string', 'app', 'URL du logo de l''application'),
('setup_completed', 'false', 'boolean', 'app', 'Configuration initiale terminée'),
('password_reset_token_expiry_days', '7', 'number', 'security', 'Durée de validité des tokens de création/réinitialisation de mot de passe (en jours)'),
('jwt_expiry_hours', '24', 'number', 'security', 'Durée de validité des sessions JWT (en heures)'),
('max_upload_size_mb', '5', 'number', 'security', 'Taille maximale des fichiers uploadés (en MB)'),
('bcrypt_rounds', '12', 'number', 'security', 'Nombre de rounds bcrypt pour le hachage des mots de passe');
-- ==========================================================
-- Table : documents_legaux
-- ==========================================================
CREATE TABLE documents_legaux (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type VARCHAR(50) NOT NULL,
version INTEGER NOT NULL,
fichier_nom VARCHAR(255) NOT NULL,
fichier_path VARCHAR(500) NOT NULL,
fichier_hash VARCHAR(64) NOT NULL,
actif BOOLEAN DEFAULT false,
televerse_par UUID REFERENCES utilisateurs(id),
televerse_le TIMESTAMPTZ DEFAULT now(),
active_le TIMESTAMPTZ,
UNIQUE(type, version)
);
CREATE INDEX idx_documents_legaux_type_actif ON documents_legaux(type, actif);
CREATE INDEX idx_documents_legaux_version ON documents_legaux(type, version DESC);
-- ==========================================================
-- Table : acceptations_documents
-- ==========================================================
CREATE TABLE acceptations_documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id_utilisateur UUID REFERENCES utilisateurs(id) ON DELETE CASCADE,
id_document UUID REFERENCES documents_legaux(id),
type_document VARCHAR(50) NOT NULL,
version_document INTEGER NOT NULL,
accepte_le TIMESTAMPTZ DEFAULT now(),
ip_address INET,
user_agent TEXT
);
CREATE INDEX idx_acceptations_utilisateur ON acceptations_documents(id_utilisateur);
CREATE INDEX idx_acceptations_document ON acceptations_documents(id_document);
-- ==========================================================
-- Ticket #103 : Numéro de dossier (AAAA-NNNNNN, séquence par année)
-- ==========================================================
CREATE TABLE numero_dossier_sequence (
annee INT PRIMARY KEY,
prochain INT NOT NULL DEFAULT 1
);
-- ==========================================================
-- Seed : Documents légaux génériques v1
-- ==========================================================
INSERT INTO documents_legaux (type, version, fichier_nom, fichier_path, fichier_hash, actif, televerse_le, active_le) VALUES
('cgu', 1, 'cgu_v1_default.pdf', '/documents/legaux/cgu_v1_default.pdf', 'a3f8b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2', true, now(), now()),
('privacy', 1, 'privacy_v1_default.pdf', '/documents/legaux/privacy_v1_default.pdf', 'b4f9c3d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4', true, now(), now());
-- ==========================================================
-- Seed : Super Administrateur par défaut
-- Email: admin@ptits-pas.fr | Mot de passe: 4dm1n1strateur
-- ==========================================================
INSERT INTO utilisateurs (
email,
password,
prenom,
nom,
role,
statut,
changement_mdp_obligatoire
) VALUES (
'admin@ptits-pas.fr',
'$2b$12$plOZCW7lzLFkWgDPcE6p6u10EA4yErQt6Xcp5nyH3Sp/2.6EpNW.6',
'Super',
'Administrateur',
'super_admin',
'actif',
true
);