-- ============================================================================
-- PTA - INSTALLATION PRODUCTION COMPLETE
-- Version applicative cible : V7.7
-- MySQL 8.0.16+
-- ============================================================================
-- Usage : installation sur une BASE VIDE.
--
-- Ce script contient :
--   * toutes les tables nécessaires à PTA V7.7 ;
--   * les clés étrangères, index et contraintes ;
--   * les vues de pilotage ;
--   * les procédures de contrôle / validation ;
--   * uniquement les référentiels techniques indispensables (postes et motifs).
--
-- Il ne contient AUCUN agent, lieu, planning, quota ou jeu de démonstration.
--
-- IMPORTANT - correction MySQL #1832 :
-- creneau_horaire.id_motif est créé directement en INT UNSIGNED NOT NULL,
-- AVANT la création de fk_creneau_motif. Aucun ALTER TABLE ... MODIFY id_motif
-- n'est donc nécessaire sur une installation neuve.
-- ============================================================================

SET NAMES utf8mb4;
SET time_zone = '+00:00';

-- Décommenter si vous souhaitez que le script crée la base lui-même.
-- CREATE DATABASE IF NOT EXISTS pta
--     CHARACTER SET utf8mb4
--     COLLATE utf8mb4_unicode_ci;
-- USE pta;

-- ============================================================================
-- 1. LIEUX D'AFFECTATION
-- ============================================================================
CREATE TABLE structure (
    id_structure INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    code VARCHAR(50) NOT NULL,
    nom VARCHAR(150) NOT NULL,
    adresse VARCHAR(255) NULL,
    type_affectation ENUM('PERISCOLAIRE', 'EXTRASCOLAIRE') NOT NULL DEFAULT 'PERISCOLAIRE',
    actif BOOLEAN NOT NULL DEFAULT TRUE,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT uk_structure_code UNIQUE (code),
    INDEX idx_structure_actif_nom (actif, nom)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ============================================================================
-- 2. POSTES PTA - RÉFÉRENTIEL TECHNIQUE
-- ============================================================================
CREATE TABLE poste_agent (
    id_poste INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    code VARCHAR(50) NOT NULL,
    libelle VARCHAR(150) NOT NULL,
    famille ENUM('ANIMATION', 'RESTAURATION_ENTRETIEN') NOT NULL,
    heures_mercredi_minutes SMALLINT UNSIGNED NOT NULL,
    heures_extrascolaire_minutes SMALLINT UNSIGNED NOT NULL,
    autorise_preparation BOOLEAN NOT NULL DEFAULT TRUE,
    peut_assurer_animation BOOLEAN NOT NULL DEFAULT TRUE,
    preparation_lundi_si_100 BOOLEAN NOT NULL DEFAULT FALSE,
    actif BOOLEAN NOT NULL DEFAULT TRUE,
    ordre_affichage SMALLINT UNSIGNED NOT NULL DEFAULT 100,

    CONSTRAINT uk_poste_agent_code UNIQUE (code),
    INDEX idx_poste_agent_actif_ordre (actif, ordre_affichage)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Référentiel indispensable au fonctionnement de l'application.
INSERT INTO poste_agent (
    code, libelle, famille,
    heures_mercredi_minutes, heures_extrascolaire_minutes,
    autorise_preparation, peut_assurer_animation,
    preparation_lundi_si_100, ordre_affichage
) VALUES
    ('RESPONSABLE', 'Responsable d’accueil', 'ANIMATION', 570, 570, TRUE, TRUE, TRUE, 10),
    ('ANIMATION_RELAIS', 'Agent d’animation relais', 'ANIMATION', 570, 570, TRUE, TRUE, TRUE, 20),
    ('ANIMATION', 'Agent d’animation', 'ANIMATION', 570, 570, TRUE, TRUE, TRUE, 30),
    ('RESTAURATION_ENTRETIEN', 'Agent de restauration collective et d’entretien', 'RESTAURATION_ENTRETIEN', 420, 420, FALSE, FALSE, FALSE, 40)
ON DUPLICATE KEY UPDATE
    libelle = VALUES(libelle),
    famille = VALUES(famille),
    heures_mercredi_minutes = VALUES(heures_mercredi_minutes),
    heures_extrascolaire_minutes = VALUES(heures_extrascolaire_minutes),
    autorise_preparation = VALUES(autorise_preparation),
    peut_assurer_animation = VALUES(peut_assurer_animation),
    preparation_lundi_si_100 = VALUES(preparation_lundi_si_100),
    ordre_affichage = VALUES(ordre_affichage),
    actif = TRUE;

-- ============================================================================
-- 3. AGENTS
-- ============================================================================
CREATE TABLE agent (
    id_agent INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    matricule VARCHAR(50) NOT NULL,
    nom VARCHAR(100) NOT NULL,
    prenom VARCHAR(100) NOT NULL,
    email VARCHAR(255) NULL,
    telephone VARCHAR(30) NULL,
    adresse VARCHAR(255) NULL,
    est_diplome BOOLEAN NOT NULL DEFAULT FALSE,
    diplome_libelle VARCHAR(150) NULL,
    date_debut_contrat DATE NULL,
    id_poste INT UNSIGNED NULL,
    type_contrat ENUM('PERMANENT', 'TEMPORAIRE') NOT NULL DEFAULT 'PERMANENT',
    date_fin_contrat DATE NULL,
    formation_repartition_annuelle BOOLEAN NOT NULL DEFAULT FALSE,
    commentaire_pta VARCHAR(1000) NULL,
    id_structure INT UNSIGNED NULL COMMENT 'Lieu principal de rattachement',
    actif BOOLEAN NOT NULL DEFAULT TRUE,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT uk_agent_matricule UNIQUE (matricule),

    CONSTRAINT fk_agent_poste
        FOREIGN KEY (id_poste) REFERENCES poste_agent(id_poste)
        ON UPDATE CASCADE ON DELETE SET NULL,

    CONSTRAINT fk_agent_structure_principale
        FOREIGN KEY (id_structure) REFERENCES structure(id_structure)
        ON UPDATE CASCADE ON DELETE SET NULL,

    CONSTRAINT chk_agent_dates_contrat
        CHECK (
            date_fin_contrat IS NULL
            OR date_debut_contrat IS NULL
            OR date_fin_contrat >= date_debut_contrat
        ),

    INDEX idx_agent_poste (id_poste),
    INDEX idx_agent_structure_principale (id_structure),
    INDEX idx_agent_type_contrat (type_contrat, actif),
    INDEX idx_agent_actif_nom (actif, nom, prenom),
    INDEX idx_agent_date_debut_contrat (date_debut_contrat)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Historique des rattachements de l'agent.
CREATE TABLE agent_structure (
    id_agent_structure INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    id_agent INT UNSIGNED NOT NULL,
    id_structure INT UNSIGNED NOT NULL,
    date_debut DATE NULL,
    date_fin DATE NULL,
    actif BOOLEAN NOT NULL DEFAULT TRUE,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT fk_agent_structure_agent
        FOREIGN KEY (id_agent) REFERENCES agent(id_agent)
        ON UPDATE CASCADE ON DELETE CASCADE,

    CONSTRAINT fk_agent_structure_structure
        FOREIGN KEY (id_structure) REFERENCES structure(id_structure)
        ON UPDATE CASCADE ON DELETE RESTRICT,

    CONSTRAINT chk_agent_structure_dates
        CHECK (date_fin IS NULL OR date_debut IS NULL OR date_fin >= date_debut),

    CONSTRAINT uk_agent_structure_periode
        UNIQUE (id_agent, id_structure, date_debut),

    INDEX idx_agent_structure_agent_actif (id_agent, actif),
    INDEX idx_agent_structure_structure_actif (id_structure, actif)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ============================================================================
-- 4. CAPACITÉS / TRANCHES D'ÂGE
-- ============================================================================
CREATE TABLE tranche_age (
    id_tranche_age INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    libelle VARCHAR(100) NOT NULL,
    age_min_mois INT UNSIGNED NULL,
    age_max_mois INT UNSIGNED NULL,

    CONSTRAINT chk_tranche_age
        CHECK (
            age_min_mois IS NULL
            OR age_max_mois IS NULL
            OR age_max_mois >= age_min_mois
        )
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE structure_capacite (
    id_structure INT UNSIGNED NOT NULL,
    id_tranche_age INT UNSIGNED NOT NULL,
    nombre_places INT UNSIGNED NOT NULL,

    PRIMARY KEY (id_structure, id_tranche_age),

    CONSTRAINT fk_capacite_structure
        FOREIGN KEY (id_structure) REFERENCES structure(id_structure)
        ON UPDATE CASCADE ON DELETE CASCADE,

    CONSTRAINT fk_capacite_tranche_age
        FOREIGN KEY (id_tranche_age) REFERENCES tranche_age(id_tranche_age)
        ON UPDATE CASCADE ON DELETE RESTRICT,

    CONSTRAINT chk_nombre_places CHECK (nombre_places > 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ============================================================================
-- 5. RÈGLES DE COUVERTURE DES LIEUX
-- ============================================================================
CREATE TABLE structure_couverture_horaire (
    id_couverture INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    id_structure INT UNSIGNED NOT NULL,
    jour_semaine TINYINT UNSIGNED NOT NULL COMMENT '1=lundi, 5=vendredi',
    heure_debut TIME NOT NULL,
    heure_fin TIME NOT NULL,
    minimum_agents TINYINT UNSIGNED NOT NULL DEFAULT 1,

    CONSTRAINT fk_couverture_structure
        FOREIGN KEY (id_structure) REFERENCES structure(id_structure)
        ON UPDATE CASCADE ON DELETE CASCADE,

    CONSTRAINT chk_couverture_jour CHECK (jour_semaine BETWEEN 1 AND 5),
    CONSTRAINT chk_couverture_ordre CHECK (heure_fin > heure_debut),
    CONSTRAINT chk_couverture_minimum CHECK (minimum_agents BETWEEN 1 AND 50),
    CONSTRAINT chk_couverture_quart_heure CHECK (
        MOD(MINUTE(heure_debut), 15) = 0
        AND MOD(MINUTE(heure_fin), 15) = 0
        AND SECOND(heure_debut) = 0
        AND SECOND(heure_fin) = 0
    ),

    INDEX idx_couverture_structure_jour
        (id_structure, jour_semaine, heure_debut, heure_fin)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Horaires de référence utilisés pour les automatismes de préparation.
CREATE TABLE structure_creneau_reference (
    id_reference INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    id_structure INT UNSIGNED NOT NULL,
    code_plage ENUM('ALP_MATIN', 'RESTAURATION', 'ALP_SOIR') NOT NULL,
    jour_semaine TINYINT UNSIGNED NOT NULL COMMENT '1=lundi ... 5=vendredi',
    heure_debut TIME NOT NULL,
    heure_fin TIME NOT NULL,
    actif BOOLEAN NOT NULL DEFAULT TRUE,

    CONSTRAINT fk_reference_structure
        FOREIGN KEY (id_structure) REFERENCES structure(id_structure)
        ON UPDATE CASCADE ON DELETE CASCADE,

    CONSTRAINT chk_reference_jour CHECK (jour_semaine BETWEEN 1 AND 5),
    CONSTRAINT chk_reference_ordre CHECK (heure_fin > heure_debut),
    CONSTRAINT uk_reference_structure_plage_jour
        UNIQUE (id_structure, code_plage, jour_semaine),

    INDEX idx_reference_structure_jour (id_structure, jour_semaine, actif)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ============================================================================
-- 6. MOTIFS D'AFFECTATION - RÉFÉRENTIEL TECHNIQUE
-- ============================================================================
CREATE TABLE motif_planning (
    id_motif INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    code VARCHAR(50) NOT NULL,
    libelle VARCHAR(120) NOT NULL,
    compte_dans_quota BOOLEAN NOT NULL DEFAULT FALSE,
    ordre_affichage SMALLINT UNSIGNED NOT NULL DEFAULT 100,
    actif BOOLEAN NOT NULL DEFAULT TRUE,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT uk_motif_planning_code UNIQUE (code),
    INDEX idx_motif_planning_actif_ordre (actif, ordre_affichage, libelle)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Aucun jeu d'essai : uniquement les motifs nécessaires au fonctionnement.
INSERT INTO motif_planning (code, libelle, compte_dans_quota, ordre_affichage, actif)
VALUES
    ('TRAVAIL', 'Heure de travail', TRUE, 5, TRUE),
    ('ALP_MATIN', 'ALP matin', TRUE, 10, TRUE),
    ('RESTAURATION', 'Restauration', TRUE, 20, TRUE),
    ('ALP_SOIR', 'ALP soir', TRUE, 30, TRUE),
    ('MERCREDI_PERISCOLAIRE', 'Mercredi périscolaire', TRUE, 40, TRUE),
    ('EXTRASCOLAIRE', 'Temps extrascolaire', TRUE, 50, TRUE),
    ('FORMATION', 'Formation', TRUE, 60, TRUE),
    ('CONGE', 'Congé annuel (non décompté)', FALSE, 70, TRUE),
    ('JOUR_FRACTIONNEMENT', 'Journée de fractionnement', TRUE, 80, TRUE),
    ('PREPARATION_PERISCOLAIRE', 'Préparation périscolaire', TRUE, 90, TRUE),
    ('PREPARATION_EXTRASCOLAIRE', 'Préparation extrascolaire', TRUE, 100, TRUE),
    ('PREPARATION_LUNDI', 'Préparation du lundi', TRUE, 110, TRUE),
    ('MENAGE_FOND', 'Ménage de fond', TRUE, 120, TRUE),
    ('ABSENCE', 'Absence', TRUE, 130, TRUE),
    ('AUTRE_ABSENCE', 'Autre absence (non décomptée)', FALSE, 140, TRUE),
    ('JNT', 'Journée non travaillée', FALSE, 150, TRUE)
ON DUPLICATE KEY UPDATE
    libelle = VALUES(libelle),
    compte_dans_quota = VALUES(compte_dans_quota),
    ordre_affichage = VALUES(ordre_affichage),
    actif = TRUE;

-- ============================================================================
-- 7. QUOTAS ANNUELS - ANNÉE CIVILE 01/01 -> 31/12
-- ============================================================================
CREATE TABLE quota_agent_annuel (
    id_quota INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    id_agent INT UNSIGNED NOT NULL,
    annee SMALLINT UNSIGNED NOT NULL,
    quotite_travail DECIMAL(5,2) NOT NULL DEFAULT 100.00,
    quota_reference_minutes INT UNSIGNED NOT NULL DEFAULT 96420 COMMENT '1607 h à 100 %',
    quota_cible_minutes INT UNSIGNED NOT NULL,
    commentaire VARCHAR(255) NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_quota_annuel_agent
        FOREIGN KEY (id_agent) REFERENCES agent(id_agent)
        ON UPDATE CASCADE ON DELETE CASCADE,

    CONSTRAINT chk_quotite_travail CHECK (quotite_travail > 0 AND quotite_travail <= 100),
    CONSTRAINT chk_quota_reference_positif CHECK (quota_reference_minutes > 0),
    CONSTRAINT chk_quota_cible_positif CHECK (quota_cible_minutes > 0),
    CONSTRAINT uk_quota_agent_annee UNIQUE (id_agent, annee),

    INDEX idx_quota_agent_annee (id_agent, annee)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE pta_annuel (
    id_pta INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    id_agent INT UNSIGNED NOT NULL,
    date_debut DATE NOT NULL,
    date_fin DATE NOT NULL,
    quotite_travail DECIMAL(5,2) NOT NULL,
    quota_reference_minutes INT UNSIGNED NOT NULL DEFAULT 96420,
    quota_cible_minutes INT UNSIGNED NOT NULL,
    enveloppe_formation_minutes SMALLINT UNSIGNED NOT NULL DEFAULT 840,
    conges_annuels_jours_cible DECIMAL(5,2) NOT NULL DEFAULT 25.00,
    jours_fractionnement_cible TINYINT UNSIGNED NOT NULL DEFAULT 2,
    statut ENUM('BROUILLON', 'A_CONTROLER', 'VALIDE') NOT NULL DEFAULT 'BROUILLON',
    commentaire VARCHAR(1000) NULL,
    date_validation DATETIME NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_pta_annuel_agent
        FOREIGN KEY (id_agent) REFERENCES agent(id_agent)
        ON UPDATE CASCADE ON DELETE CASCADE,

    CONSTRAINT chk_pta_annuel_dates CHECK (date_fin >= date_debut),
    CONSTRAINT chk_pta_annuel_quotite CHECK (quotite_travail > 0 AND quotite_travail <= 100),
    CONSTRAINT chk_pta_annuel_quota CHECK (quota_reference_minutes > 0 AND quota_cible_minutes > 0),
    CONSTRAINT uk_pta_annuel_agent_periode UNIQUE (id_agent, date_debut, date_fin),

    INDEX idx_pta_annuel_agent_statut (id_agent, statut),
    INDEX idx_pta_annuel_periode (date_debut, date_fin)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ============================================================================
-- 8. SEMAINES / PLANNINGS / CRÉNEAUX
-- ============================================================================
CREATE TABLE semaine (
    id_semaine INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    annee SMALLINT UNSIGNED NOT NULL COMMENT 'Année ISO de la semaine',
    numero_semaine TINYINT UNSIGNED NOT NULL,
    date_debut DATE NOT NULL COMMENT 'Lundi',
    date_fin DATE NOT NULL COMMENT 'Dimanche',
    statut ENUM('OUVERTE', 'VALIDEE', 'VERROUILLEE') NOT NULL DEFAULT 'OUVERTE',
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT uk_semaine_annee_numero UNIQUE (annee, numero_semaine),
    CONSTRAINT uk_semaine_dates UNIQUE (date_debut, date_fin),
    CONSTRAINT chk_numero_semaine CHECK (numero_semaine BETWEEN 1 AND 53),
    CONSTRAINT chk_dates_semaine CHECK (date_fin >= date_debut),

    INDEX idx_semaine_dates (date_debut, date_fin)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE planning (
    id_planning INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    id_agent INT UNSIGNED NOT NULL,
    id_semaine INT UNSIGNED NOT NULL,
    id_structure INT UNSIGNED NOT NULL,
    statut ENUM('BROUILLON', 'VALIDE') NOT NULL DEFAULT 'BROUILLON',
    commentaire VARCHAR(500) NULL,
    id_planning_source INT UNSIGNED NULL,
    date_creation DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    date_modification DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    date_validation DATETIME NULL,

    CONSTRAINT fk_planning_agent
        FOREIGN KEY (id_agent) REFERENCES agent(id_agent)
        ON UPDATE CASCADE ON DELETE RESTRICT,

    CONSTRAINT fk_planning_semaine
        FOREIGN KEY (id_semaine) REFERENCES semaine(id_semaine)
        ON UPDATE CASCADE ON DELETE RESTRICT,

    CONSTRAINT fk_planning_structure
        FOREIGN KEY (id_structure) REFERENCES structure(id_structure)
        ON UPDATE CASCADE ON DELETE RESTRICT,

    CONSTRAINT fk_planning_source
        FOREIGN KEY (id_planning_source) REFERENCES planning(id_planning)
        ON UPDATE CASCADE ON DELETE SET NULL,

    CONSTRAINT uk_planning_agent_semaine_structure
        UNIQUE (id_agent, id_semaine, id_structure),

    INDEX idx_planning_agent_statut (id_agent, statut),
    INDEX idx_planning_semaine_structure (id_semaine, id_structure, statut),
    INDEX idx_planning_modification (date_modification)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- CORRECTION #1832 : le type de id_motif est correct dès sa création.
CREATE TABLE creneau_horaire (
    id_creneau INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    id_planning INT UNSIGNED NOT NULL,
    id_motif INT UNSIGNED NOT NULL,
    date_jour DATE NOT NULL,
    heure_debut TIME NOT NULL,
    heure_fin TIME NOT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_creneau_planning
        FOREIGN KEY (id_planning) REFERENCES planning(id_planning)
        ON UPDATE CASCADE ON DELETE CASCADE,

    CONSTRAINT fk_creneau_motif
        FOREIGN KEY (id_motif) REFERENCES motif_planning(id_motif)
        ON UPDATE CASCADE ON DELETE RESTRICT,

    CONSTRAINT chk_creneau_ordre CHECK (heure_fin > heure_debut),
    CONSTRAINT chk_creneau_quart_heure CHECK (
        MOD(MINUTE(heure_debut), 15) = 0
        AND MOD(MINUTE(heure_fin), 15) = 0
        AND SECOND(heure_debut) = 0
        AND SECOND(heure_fin) = 0
    ),

    INDEX idx_creneau_planning_date (id_planning, date_jour, heure_debut, heure_fin),
    INDEX idx_creneau_motif (id_motif),
    INDEX idx_creneau_date (date_jour, heure_debut, heure_fin)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ============================================================================
-- 9. SEMAINES TYPES
-- ============================================================================
CREATE TABLE modele_semaine_agent (
    id_modele INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    id_agent INT UNSIGNED NOT NULL,
    nom VARCHAR(120) NOT NULL,
    date_creation DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    date_modification DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_modele_semaine_agent
        FOREIGN KEY (id_agent) REFERENCES agent(id_agent)
        ON UPDATE CASCADE ON DELETE CASCADE,

    CONSTRAINT uk_modele_semaine_agent_nom UNIQUE (id_agent, nom),
    INDEX idx_modele_semaine_agent (id_agent, date_modification)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE modele_semaine_creneau (
    id_modele_creneau INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    id_modele INT UNSIGNED NOT NULL,
    jour_semaine TINYINT UNSIGNED NOT NULL COMMENT '1=lundi, 5=vendredi',
    id_structure INT UNSIGNED NOT NULL,
    id_motif INT UNSIGNED NOT NULL,
    heure_debut TIME NOT NULL,
    heure_fin TIME NOT NULL,

    CONSTRAINT fk_modele_creneau_modele
        FOREIGN KEY (id_modele) REFERENCES modele_semaine_agent(id_modele)
        ON UPDATE CASCADE ON DELETE CASCADE,

    CONSTRAINT fk_modele_creneau_structure
        FOREIGN KEY (id_structure) REFERENCES structure(id_structure)
        ON UPDATE CASCADE ON DELETE RESTRICT,

    CONSTRAINT fk_modele_creneau_motif
        FOREIGN KEY (id_motif) REFERENCES motif_planning(id_motif)
        ON UPDATE CASCADE ON DELETE RESTRICT,

    CONSTRAINT chk_modele_creneau_jour CHECK (jour_semaine BETWEEN 1 AND 5),
    CONSTRAINT chk_modele_creneau_ordre CHECK (heure_fin > heure_debut),
    CONSTRAINT chk_modele_creneau_quart_heure CHECK (
        MOD(MINUTE(heure_debut), 15) = 0
        AND MOD(MINUTE(heure_fin), 15) = 0
        AND SECOND(heure_debut) = 0
        AND SECOND(heure_fin) = 0
    ),

    INDEX idx_modele_creneau_modele_jour (id_modele, jour_semaine, heure_debut),
    INDEX idx_modele_creneau_structure (id_structure),
    INDEX idx_modele_creneau_motif (id_motif)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ============================================================================
-- 10. VACANCES SCOLAIRES
-- ============================================================================
CREATE TABLE periode_vacance (
    id_periode INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    libelle VARCHAR(150) NOT NULL,
    date_debut DATE NOT NULL,
    date_fin DATE NOT NULL COMMENT 'Date inclusive',
    annee_scolaire VARCHAR(9) NOT NULL,
    academie VARCHAR(100) NOT NULL DEFAULT '',
    zone VARCHAR(50) NOT NULL DEFAULT '',
    source ENUM('MANUEL', 'DATA_GOUV') NOT NULL DEFAULT 'MANUEL',
    actif BOOLEAN NOT NULL DEFAULT TRUE,
    date_creation DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    date_modification DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT chk_periode_vacance_dates CHECK (date_fin >= date_debut),
    CONSTRAINT uk_periode_vacance UNIQUE (date_debut, date_fin, annee_scolaire, academie),

    INDEX idx_periode_vacance_dates (date_debut, date_fin),
    INDEX idx_periode_vacance_annee (annee_scolaire),
    INDEX idx_periode_vacance_actif_dates (actif, date_debut, date_fin)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ============================================================================
-- 11. PRÉFÉRENCES / INTERDICTIONS / CONTRAINTES AGENT
-- ============================================================================
CREATE TABLE agent_structure_souhait (
    id_souhait INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    id_agent INT UNSIGNED NOT NULL,
    id_structure INT UNSIGNED NOT NULL,
    type_souhait ENUM('PREFERENCE', 'INTERDICTION') NOT NULL,
    priorite TINYINT UNSIGNED NOT NULL DEFAULT 1,
    distance_km DECIMAL(7,2) NULL,
    commentaire VARCHAR(500) NULL,
    actif BOOLEAN NOT NULL DEFAULT TRUE,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_souhait_agent
        FOREIGN KEY (id_agent) REFERENCES agent(id_agent)
        ON UPDATE CASCADE ON DELETE CASCADE,

    CONSTRAINT fk_souhait_structure
        FOREIGN KEY (id_structure) REFERENCES structure(id_structure)
        ON UPDATE CASCADE ON DELETE CASCADE,

    CONSTRAINT chk_souhait_priorite CHECK (priorite BETWEEN 1 AND 5),
    CONSTRAINT chk_souhait_distance CHECK (distance_km IS NULL OR distance_km >= 0),
    CONSTRAINT uk_souhait_agent_structure UNIQUE (id_agent, id_structure),

    INDEX idx_souhait_agent_type (id_agent, type_souhait, actif)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE agent_contrainte (
    id_contrainte INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    id_agent INT UNSIGNED NOT NULL,
    type_contrainte ENUM('MEDICALE', 'TEMPS_PARTIEL_THERAPEUTIQUE') NOT NULL,
    date_debut DATE NOT NULL,
    date_fin DATE NULL,
    quotite_temporaire DECIMAL(5,2) NULL,
    maximum_minutes_jour SMALLINT UNSIGNED NULL,
    maximum_minutes_semaine SMALLINT UNSIGNED NULL,
    interdit_matin BOOLEAN NOT NULL DEFAULT FALSE,
    interdit_midi BOOLEAN NOT NULL DEFAULT FALSE,
    interdit_soir BOOLEAN NOT NULL DEFAULT FALSE,
    commentaire VARCHAR(1000) NOT NULL,
    actif BOOLEAN NOT NULL DEFAULT TRUE,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_contrainte_agent
        FOREIGN KEY (id_agent) REFERENCES agent(id_agent)
        ON UPDATE CASCADE ON DELETE CASCADE,

    CONSTRAINT chk_contrainte_dates CHECK (date_fin IS NULL OR date_fin >= date_debut),
    CONSTRAINT chk_contrainte_quotite CHECK (
        quotite_temporaire IS NULL OR (quotite_temporaire > 0 AND quotite_temporaire <= 100)
    ),
    CONSTRAINT chk_contrainte_max_jour CHECK (maximum_minutes_jour IS NULL OR maximum_minutes_jour > 0),
    CONSTRAINT chk_contrainte_max_semaine CHECK (maximum_minutes_semaine IS NULL OR maximum_minutes_semaine > 0),

    INDEX idx_contrainte_agent_dates (id_agent, actif, date_debut, date_fin)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ============================================================================
-- 12. ÉQUIPES PTA
-- ============================================================================
CREATE TABLE equipe_pta (
    id_equipe INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    id_structure INT UNSIGNED NOT NULL,
    type_equipe ENUM('MERCREDI_PERISCOLAIRE', 'EXTRASCOLAIRE', 'PERISCOLAIRE_SEMAINE') NOT NULL,
    libelle VARCHAR(180) NOT NULL,
    date_debut DATE NOT NULL,
    date_fin DATE NOT NULL,
    nombre_enfants_moins_6 SMALLINT UNSIGNED NOT NULL DEFAULT 0,
    [REDACTED] SMALLINT UNSIGNED NOT NULL DEFAULT 0,
    ratio_moins_6 TINYINT UNSIGNED NOT NULL DEFAULT 8,
    ratio_6_plus TINYINT UNSIGNED NOT NULL DEFAULT 12,
    minimum_agents TINYINT UNSIGNED NOT NULL DEFAULT 1,
    pourcentage_diplomes_min DECIMAL(5,2) NOT NULL DEFAULT 50.00,
    statut ENUM('BROUILLON', 'A_CONTROLER', 'VALIDEE') NOT NULL DEFAULT 'BROUILLON',
    commentaire VARCHAR(1000) NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    CONSTRAINT fk_equipe_pta_structure
        FOREIGN KEY (id_structure) REFERENCES structure(id_structure)
        ON UPDATE CASCADE ON DELETE RESTRICT,

    CONSTRAINT chk_equipe_dates CHECK (date_fin >= date_debut),
    CONSTRAINT chk_equipe_ratios CHECK (ratio_moins_6 > 0 AND ratio_6_plus > 0),
    CONSTRAINT chk_equipe_minimum CHECK (minimum_agents > 0),
    CONSTRAINT chk_equipe_diplomes CHECK (pourcentage_diplomes_min BETWEEN 0 AND 100),

    INDEX idx_equipe_pta_type_dates (type_equipe, date_debut, date_fin),
    INDEX idx_equipe_pta_structure_dates (id_structure, date_debut, date_fin)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE equipe_pta_membre (
    id_equipe_membre INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    id_equipe INT UNSIGNED NOT NULL,
    id_agent INT UNSIGNED NOT NULL,
    fonction_equipe ENUM('RESPONSABLE', 'ANIMATION', 'RESTAURATION_ENTRETIEN') NOT NULL DEFAULT 'ANIMATION',
    date_debut DATE NULL,
    date_fin DATE NULL,
    commentaire VARCHAR(500) NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT fk_equipe_membre_equipe
        FOREIGN KEY (id_equipe) REFERENCES equipe_pta(id_equipe)
        ON UPDATE CASCADE ON DELETE CASCADE,

    CONSTRAINT fk_equipe_membre_agent
        FOREIGN KEY (id_agent) REFERENCES agent(id_agent)
        ON UPDATE CASCADE ON DELETE RESTRICT,

    CONSTRAINT chk_equipe_membre_dates
        CHECK (date_fin IS NULL OR date_debut IS NULL OR date_fin >= date_debut),

    CONSTRAINT uk_equipe_membre UNIQUE (id_equipe, id_agent),
    INDEX idx_equipe_membre_agent (id_agent, id_equipe)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ============================================================================
-- 13. VUES DE PILOTAGE
-- ============================================================================

CREATE OR REPLACE VIEW v_planning_agent_semaine AS
SELECT
    p.id_planning,
    p.id_agent,
    a.matricule,
    a.nom,
    a.prenom,
    CONCAT(a.prenom, ' ', a.nom) AS agent,
    p.id_semaine,
    s.annee,
    s.numero_semaine,
    s.date_debut,
    s.date_fin,
    p.id_structure,
    st.code AS code_structure,
    st.nom AS lieu_affectation,
    st.type_affectation,
    p.statut,
    p.date_creation,
    p.date_modification,
    p.date_validation,
    COUNT(c.id_creneau) AS nombre_creneaux,
    COALESCE(SUM(
        TIMESTAMPDIFF(
            MINUTE,
            CONCAT(c.date_jour, ' ', c.heure_debut),
            CONCAT(c.date_jour, ' ', c.heure_fin)
        )
    ), 0) AS minutes_planifiees,
    COALESCE(SUM(
        CASE WHEN m.compte_dans_quota = TRUE THEN
            TIMESTAMPDIFF(
                MINUTE,
                CONCAT(c.date_jour, ' ', c.heure_debut),
                CONCAT(c.date_jour, ' ', c.heure_fin)
            )
        ELSE 0 END
    ), 0) AS minutes_decomptees_quota,
    ROUND(COALESCE(SUM(
        TIMESTAMPDIFF(
            MINUTE,
            CONCAT(c.date_jour, ' ', c.heure_debut),
            CONCAT(c.date_jour, ' ', c.heure_fin)
        )
    ), 0) / 60, 2) AS heures_planifiees
FROM planning p
INNER JOIN agent a ON a.id_agent = p.id_agent
INNER JOIN semaine s ON s.id_semaine = p.id_semaine
INNER JOIN structure st ON st.id_structure = p.id_structure
LEFT JOIN creneau_horaire c ON c.id_planning = p.id_planning
LEFT JOIN motif_planning m ON m.id_motif = c.id_motif
GROUP BY
    p.id_planning, p.id_agent, a.matricule, a.nom, a.prenom,
    p.id_semaine, s.annee, s.numero_semaine, s.date_debut, s.date_fin,
    p.id_structure, st.code, st.nom, st.type_affectation,
    p.statut, p.date_creation, p.date_modification, p.date_validation;

-- Quota annuel = année CIVILE.
CREATE OR REPLACE VIEW v_suivi_quota_annuel AS
SELECT
    q.id_quota,
    q.id_agent,
    a.matricule,
    a.nom,
    a.prenom,
    a.date_debut_contrat,
    a.date_fin_contrat,
    q.annee,
    q.quotite_travail,
    q.quota_reference_minutes,
    q.quota_cible_minutes,
    STR_TO_DATE(CONCAT(q.annee, '-01-01'), '%Y-%m-%d') AS date_debut_periode,
    STR_TO_DATE(CONCAT(q.annee, '-12-31'), '%Y-%m-%d') AS date_fin_periode,
    ROUND(q.quota_reference_minutes / 60, 2) AS quota_reference_heures,
    ROUND(q.quota_cible_minutes / 60, 2) AS quota_cible_heures,

    COALESCE(SUM(CASE
        WHEN p.statut = 'VALIDE' AND m.compte_dans_quota = TRUE THEN
            TIMESTAMPDIFF(
                MINUTE,
                CONCAT(c.date_jour, ' ', c.heure_debut),
                CONCAT(c.date_jour, ' ', c.heure_fin)
            )
        ELSE 0 END
    ), 0) AS minutes_valides,

    COALESCE(SUM(CASE
        WHEN p.statut = 'BROUILLON' AND m.compte_dans_quota = TRUE THEN
            TIMESTAMPDIFF(
                MINUTE,
                CONCAT(c.date_jour, ' ', c.heure_debut),
                CONCAT(c.date_jour, ' ', c.heure_fin)
            )
        ELSE 0 END
    ), 0) AS minutes_brouillon,

    COALESCE(SUM(CASE
        WHEN m.compte_dans_quota = TRUE THEN
            TIMESTAMPDIFF(
                MINUTE,
                CONCAT(c.date_jour, ' ', c.heure_debut),
                CONCAT(c.date_jour, ' ', c.heure_fin)
            )
        ELSE 0 END
    ), 0) AS minutes_affectees,

    q.quota_cible_minutes - COALESCE(SUM(CASE
        WHEN m.compte_dans_quota = TRUE THEN
            TIMESTAMPDIFF(
                MINUTE,
                CONCAT(c.date_jour, ' ', c.heure_debut),
                CONCAT(c.date_jour, ' ', c.heure_fin)
            )
        ELSE 0 END
    ), 0) AS minutes_restantes
FROM quota_agent_annuel q
INNER JOIN agent a ON a.id_agent = q.id_agent
LEFT JOIN planning p ON p.id_agent = q.id_agent
LEFT JOIN creneau_horaire c
    ON c.id_planning = p.id_planning
   AND c.date_jour BETWEEN
        STR_TO_DATE(CONCAT(q.annee, '-01-01'), '%Y-%m-%d')
        AND STR_TO_DATE(CONCAT(q.annee, '-12-31'), '%Y-%m-%d')
LEFT JOIN motif_planning m ON m.id_motif = c.id_motif
GROUP BY
    q.id_quota, q.id_agent, a.matricule, a.nom, a.prenom,
    a.date_debut_contrat, a.date_fin_contrat,
    q.annee, q.quotite_travail,
    q.quota_reference_minutes, q.quota_cible_minutes;

CREATE OR REPLACE VIEW v_plannings_en_attente_validation AS
SELECT
    p.id_planning,
    p.id_agent,
    a.matricule,
    a.nom,
    a.prenom,
    CONCAT(a.prenom, ' ', a.nom) AS agent,
    p.id_structure,
    st.code AS code_structure,
    st.nom AS lieu_affectation,
    p.id_semaine,
    s.annee,
    s.numero_semaine,
    s.date_debut,
    s.date_fin,
    p.date_modification,
    COUNT(c.id_creneau) AS nombre_creneaux,
    COALESCE(SUM(
        TIMESTAMPDIFF(
            MINUTE,
            CONCAT(c.date_jour, ' ', c.heure_debut),
            CONCAT(c.date_jour, ' ', c.heure_fin)
        )
    ), 0) AS minutes_totales,
    COALESCE(SUM(CASE
        WHEN m.compte_dans_quota = TRUE THEN
            TIMESTAMPDIFF(
                MINUTE,
                CONCAT(c.date_jour, ' ', c.heure_debut),
                CONCAT(c.date_jour, ' ', c.heure_fin)
            )
        ELSE 0 END
    ), 0) AS minutes_decomptees
FROM planning p
INNER JOIN agent a ON a.id_agent = p.id_agent
INNER JOIN structure st ON st.id_structure = p.id_structure
INNER JOIN semaine s ON s.id_semaine = p.id_semaine
INNER JOIN creneau_horaire c ON c.id_planning = p.id_planning
INNER JOIN motif_planning m ON m.id_motif = c.id_motif
WHERE p.statut = 'BROUILLON'
GROUP BY
    p.id_planning, p.id_agent, a.matricule, a.nom, a.prenom,
    p.id_structure, st.code, st.nom,
    p.id_semaine, s.annee, s.numero_semaine, s.date_debut, s.date_fin,
    p.date_modification;

-- Uniquement les PTA correspondant à une année civile complète.
CREATE OR REPLACE VIEW v_pta_annuel_synthese AS
SELECT
    pa.id_pta,
    pa.id_agent,
    a.matricule,
    a.nom,
    a.prenom,
    po.code AS poste_code,
    po.libelle AS poste_libelle,
    a.type_contrat,
    pa.date_debut,
    pa.date_fin,
    pa.quotite_travail,
    pa.quota_cible_minutes,
    COALESCE(SUM(CASE
        WHEN mp.compte_dans_quota = TRUE THEN
            TIMESTAMPDIFF(
                MINUTE,
                CONCAT(ch.date_jour, ' ', ch.heure_debut),
                CONCAT(ch.date_jour, ' ', ch.heure_fin)
            )
        ELSE 0 END
    ), 0) AS minutes_decomptees,
    pa.quota_cible_minutes - COALESCE(SUM(CASE
        WHEN mp.compte_dans_quota = TRUE THEN
            TIMESTAMPDIFF(
                MINUTE,
                CONCAT(ch.date_jour, ' ', ch.heure_debut),
                CONCAT(ch.date_jour, ' ', ch.heure_fin)
            )
        ELSE 0 END
    ), 0) AS minutes_restantes,
    pa.statut
FROM pta_annuel pa
INNER JOIN agent a ON a.id_agent = pa.id_agent
LEFT JOIN poste_agent po ON po.id_poste = a.id_poste
LEFT JOIN planning p ON p.id_agent = pa.id_agent
LEFT JOIN creneau_horaire ch
    ON ch.id_planning = p.id_planning
   AND ch.date_jour BETWEEN pa.date_debut AND pa.date_fin
LEFT JOIN motif_planning mp ON mp.id_motif = ch.id_motif
WHERE pa.date_debut = STR_TO_DATE(CONCAT(YEAR(pa.date_debut), '-01-01'), '%Y-%m-%d')
  AND pa.date_fin = STR_TO_DATE(CONCAT(YEAR(pa.date_debut), '-12-31'), '%Y-%m-%d')
GROUP BY
    pa.id_pta, pa.id_agent, a.matricule, a.nom, a.prenom,
    po.code, po.libelle, a.type_contrat,
    pa.date_debut, pa.date_fin, pa.quotite_travail,
    pa.quota_cible_minutes, pa.statut;

CREATE OR REPLACE VIEW v_equipe_pta_controle AS
SELECT
    e.id_equipe,
    e.libelle,
    e.type_equipe,
    e.id_structure,
    s.nom AS structure_nom,
    e.date_debut,
    e.date_fin,
    e.nombre_enfants_moins_6,
    e.nombre_enfants_6_plus,
    GREATEST(
        e.minimum_agents,
        CEIL(e.nombre_enfants_moins_6 / e.ratio_moins_6)
        + CEIL(e.nombre_enfants_6_plus / e.ratio_6_plus)
    ) AS agents_requis,
    COUNT(em.id_agent) AS agents_affectes,
    COALESCE(SUM(CASE WHEN a.est_diplome = TRUE THEN 1 ELSE 0 END), 0) AS agents_diplomes,
    CASE
        WHEN COUNT(em.id_agent) = 0 THEN 0
        ELSE ROUND(
            100 * SUM(CASE WHEN a.est_diplome = TRUE THEN 1 ELSE 0 END)
            / COUNT(em.id_agent),
            2
        )
    END AS pourcentage_diplomes,
    e.pourcentage_diplomes_min,
    COALESCE(SUM(CASE WHEN em.fonction_equipe = 'RESPONSABLE' THEN 1 ELSE 0 END), 0) AS responsables,
    e.statut
FROM equipe_pta e
INNER JOIN structure s ON s.id_structure = e.id_structure
LEFT JOIN equipe_pta_membre em ON em.id_equipe = e.id_equipe
LEFT JOIN agent a ON a.id_agent = em.id_agent
GROUP BY
    e.id_equipe, e.libelle, e.type_equipe, e.id_structure, s.nom,
    e.date_debut, e.date_fin,
    e.nombre_enfants_moins_6, e.nombre_enfants_6_plus,
    e.ratio_moins_6, e.ratio_6_plus, e.minimum_agents,
    e.pourcentage_diplomes_min, e.statut;

-- ============================================================================
-- 14. PROCÉDURES DE CONTRÔLE / VALIDATION
-- ============================================================================
DELIMITER $$

DROP PROCEDURE IF EXISTS sp_preparer_controles_planning$$
CREATE PROCEDURE sp_preparer_controles_planning(
    IN p_id_planning INT UNSIGNED
)
controle: BEGIN
    DECLARE v_planning_existe INT DEFAULT 0;

    DROP TEMPORARY TABLE IF EXISTS tmp_controles_planning;

    CREATE TEMPORARY TABLE tmp_controles_planning (
        id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        niveau ENUM('ERREUR', 'ALERTE', 'INFO') NOT NULL,
        code VARCHAR(100) NOT NULL,
        message VARCHAR(1000) NOT NULL
    );

    SELECT COUNT(*)
    INTO v_planning_existe
    FROM planning
    WHERE id_planning = p_id_planning;

    IF v_planning_existe = 0 THEN
        INSERT INTO tmp_controles_planning(niveau, code, message)
        VALUES (
            'ERREUR',
            'PLANNING_INTROUVABLE',
            CONCAT('Le planning #', p_id_planning, ' n''existe pas.')
        );
        LEAVE controle;
    END IF;

    -- Un planning validable doit contenir au moins un créneau.
    IF NOT EXISTS (
        SELECT 1 FROM creneau_horaire WHERE id_planning = p_id_planning
    ) THEN
        INSERT INTO tmp_controles_planning(niveau, code, message)
        VALUES ('ERREUR', 'AUCUN_CRENEAU', 'Le planning ne contient aucun créneau horaire.');
    END IF;

    -- Créneaux hors de la semaine du planning.
    INSERT INTO tmp_controles_planning(niveau, code, message)
    SELECT
        'ERREUR',
        'CRENEAU_HORS_SEMAINE',
        CONCAT(
            'Le créneau #', c.id_creneau,
            ' du ', DATE_FORMAT(c.date_jour, '%d/%m/%Y'),
            ' est en dehors de la semaine ', s.numero_semaine,
            ' (', DATE_FORMAT(s.date_debut, '%d/%m/%Y'),
            ' au ', DATE_FORMAT(s.date_fin, '%d/%m/%Y'), ').'
        )
    FROM planning p
    INNER JOIN semaine s ON s.id_semaine = p.id_semaine
    INNER JOIN creneau_horaire c ON c.id_planning = p.id_planning
    WHERE p.id_planning = p_id_planning
      AND c.date_jour NOT BETWEEN s.date_debut AND s.date_fin;

    -- Avant le début du contrat.
    INSERT INTO tmp_controles_planning(niveau, code, message)
    SELECT
        'ERREUR',
        'AVANT_DEBUT_CONTRAT',
        CONCAT(
            'Le créneau #', c.id_creneau,
            ' du ', DATE_FORMAT(c.date_jour, '%d/%m/%Y'),
            ' est antérieur au début du contrat de ',
            CONCAT(a.prenom, ' ', a.nom),
            ' (', DATE_FORMAT(a.date_debut_contrat, '%d/%m/%Y'), ').'
        )
    FROM planning p
    INNER JOIN agent a ON a.id_agent = p.id_agent
    INNER JOIN creneau_horaire c ON c.id_planning = p.id_planning
    WHERE p.id_planning = p_id_planning
      AND a.date_debut_contrat IS NOT NULL
      AND c.date_jour < a.date_debut_contrat;

    -- Après la fin du contrat, si elle existe.
    INSERT INTO tmp_controles_planning(niveau, code, message)
    SELECT
        'ERREUR',
        'APRES_FIN_CONTRAT',
        CONCAT(
            'Le créneau #', c.id_creneau,
            ' du ', DATE_FORMAT(c.date_jour, '%d/%m/%Y'),
            ' est postérieur à la fin du contrat de ',
            CONCAT(a.prenom, ' ', a.nom),
            ' (', DATE_FORMAT(a.date_fin_contrat, '%d/%m/%Y'), ').'
        )
    FROM planning p
    INNER JOIN agent a ON a.id_agent = p.id_agent
    INNER JOIN creneau_horaire c ON c.id_planning = p.id_planning
    WHERE p.id_planning = p_id_planning
      AND a.date_fin_contrat IS NOT NULL
      AND c.date_jour > a.date_fin_contrat;

    -- Chevauchements internes.
    INSERT INTO tmp_controles_planning(niveau, code, message)
    SELECT
        'ERREUR',
        'CHEVAUCHEMENT_INTERNE',
        CONCAT(
            'Les créneaux #', c1.id_creneau,
            ' et #', c2.id_creneau,
            ' se chevauchent le ', DATE_FORMAT(c1.date_jour, '%d/%m/%Y'),
            ' (', TIME_FORMAT(c1.heure_debut, '%H:%i'), '-', TIME_FORMAT(c1.heure_fin, '%H:%i'),
            ' et ', TIME_FORMAT(c2.heure_debut, '%H:%i'), '-', TIME_FORMAT(c2.heure_fin, '%H:%i'), ').'
        )
    FROM creneau_horaire c1
    INNER JOIN creneau_horaire c2
        ON c2.id_planning = c1.id_planning
       AND c2.date_jour = c1.date_jour
       AND c2.id_creneau > c1.id_creneau
       AND c1.heure_debut < c2.heure_fin
       AND c1.heure_fin > c2.heure_debut
    WHERE c1.id_planning = p_id_planning;

    -- Double affectation de l'agent, tous lieux et tous statuts confondus.
    INSERT INTO tmp_controles_planning(niveau, code, message)
    SELECT DISTINCT
        'ERREUR',
        'CHEVAUCHEMENT_AUTRE_PLANNING',
        CONCAT(
            'Le créneau #', c1.id_creneau,
            ' chevauche le créneau #', c2.id_creneau,
            ' du planning #', p2.id_planning,
            ' au lieu « ', st2.nom, ' » le ',
            DATE_FORMAT(c1.date_jour, '%d/%m/%Y'),
            ' (', TIME_FORMAT(c2.heure_debut, '%H:%i'), '-', TIME_FORMAT(c2.heure_fin, '%H:%i'), ').'
        )
    FROM planning p1
    INNER JOIN creneau_horaire c1 ON c1.id_planning = p1.id_planning
    INNER JOIN planning p2
        ON p2.id_agent = p1.id_agent
       AND p2.id_planning <> p1.id_planning
    INNER JOIN structure st2 ON st2.id_structure = p2.id_structure
    INNER JOIN creneau_horaire c2
        ON c2.id_planning = p2.id_planning
       AND c2.date_jour = c1.date_jour
       AND c1.heure_debut < c2.heure_fin
       AND c1.heure_fin > c2.heure_debut
    WHERE p1.id_planning = p_id_planning;

    -- Dépassement du quota CIVIL de l'année concernée.
    INSERT INTO tmp_controles_planning(niveau, code, message)
    SELECT DISTINCT
        'ALERTE',
        'QUOTA_DEPASSE',
        CONCAT(
            'Le quota de ', ROUND(v.quota_cible_minutes / 60, 2), ' h ',
            'pour l''année civile ', v.annee,
            ' est dépassé. Total affecté : ', ROUND(v.minutes_affectees / 60, 2), ' h.'
        )
    FROM planning p
    INNER JOIN creneau_horaire cp ON cp.id_planning = p.id_planning
    INNER JOIN v_suivi_quota_annuel v
        ON v.id_agent = p.id_agent
       AND YEAR(cp.date_jour) = v.annee
    WHERE p.id_planning = p_id_planning
      AND v.minutes_restantes < 0;

    -- Aucun quota paramétré pour l'année civile touchée.
    INSERT INTO tmp_controles_planning(niveau, code, message)
    SELECT DISTINCT
        'ALERTE',
        'QUOTA_NON_PARAMETRE',
        CONCAT(
            'Aucun quota n''est paramétré pour l''agent sur l''année civile ',
            YEAR(c.date_jour), '.'
        )
    FROM planning p
    INNER JOIN creneau_horaire c ON c.id_planning = p.id_planning
    WHERE p.id_planning = p_id_planning
      AND NOT EXISTS (
          SELECT 1
          FROM quota_agent_annuel q
          WHERE q.id_agent = p.id_agent
            AND q.annee = YEAR(c.date_jour)
      );

    IF NOT EXISTS (SELECT 1 FROM tmp_controles_planning) THEN
        INSERT INTO tmp_controles_planning(niveau, code, message)
        VALUES ('INFO', 'OK', 'Aucune anomalie détectée. Le planning peut être validé.');
    END IF;
END$$

DROP PROCEDURE IF EXISTS sp_controler_planning$$
CREATE PROCEDURE sp_controler_planning(
    IN p_id_planning INT UNSIGNED
)
BEGIN
    CALL sp_preparer_controles_planning(p_id_planning);

    SELECT niveau, code, message
    FROM tmp_controles_planning
    ORDER BY FIELD(niveau, 'ERREUR', 'ALERTE', 'INFO'), id;
END$$

DROP PROCEDURE IF EXISTS sp_valider_planning$$
CREATE PROCEDURE sp_valider_planning(
    IN p_id_planning INT UNSIGNED,
    IN p_forcer_depassement_quota BOOLEAN
)
BEGIN
    DECLARE v_nb_erreurs INT DEFAULT 0;
    DECLARE v_nb_alertes_quota INT DEFAULT 0;
    DECLARE v_message VARCHAR(1000);

    CALL sp_preparer_controles_planning(p_id_planning);

    SELECT COUNT(*) INTO v_nb_erreurs
    FROM tmp_controles_planning
    WHERE niveau = 'ERREUR';

    IF v_nb_erreurs > 0 THEN
        SELECT message INTO v_message
        FROM tmp_controles_planning
        WHERE niveau = 'ERREUR'
        ORDER BY id
        LIMIT 1;

        SET v_message = CONCAT(
            'Validation impossible. ', v_nb_erreurs,
            ' erreur(s) détectée(s). Première erreur : ', v_message
        );

        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = v_message;
    END IF;

    SELECT COUNT(*) INTO v_nb_alertes_quota
    FROM tmp_controles_planning
    WHERE code = 'QUOTA_DEPASSE';

    IF v_nb_alertes_quota > 0
       AND COALESCE(p_forcer_depassement_quota, FALSE) = FALSE THEN
        SIGNAL SQLSTATE '45000'
            SET MESSAGE_TEXT = 'Validation suspendue : le quota annuel serait dépassé. Relancer en forçant le dépassement après confirmation.';
    END IF;

    UPDATE planning
    SET statut = 'VALIDE',
        date_validation = CURRENT_TIMESTAMP,
        date_modification = CURRENT_TIMESTAMP
    WHERE id_planning = p_id_planning;

    SELECT
        p.id_planning,
        p.statut,
        p.date_validation,
        'Planning validé avec succès.' AS message
    FROM planning p
    WHERE p.id_planning = p_id_planning;
END$$

DELIMITER ;

-- ============================================================================
-- FIN INSTALLATION PRODUCTION PTA V7.7
-- ============================================================================
-- Vérification recommandée après import :
--
-- SHOW CREATE TABLE creneau_horaire;
--
-- Vous devez voir :
--   id_motif int unsigned NOT NULL
--   CONSTRAINT fk_creneau_motif FOREIGN KEY (id_motif)
--       REFERENCES motif_planning(id_motif)
--
-- Aucun changement de type de id_motif n'est exécuté après création de la FK.
-- ============================================================================
