-- ============================================================ -- Extension de la base PTA pour l'interface de gestion planning -- À exécuter APRES pta_mysql_test.sql -- Compatible MySQL 8+ -- ============================================================ USE pta_test; -- 1. Motifs d'affectation CREATE TABLE IF NOT EXISTS motif_planning ( id_motif INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, code VARCHAR(50) NOT NULL UNIQUE, 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 ) ENGINE = InnoDB; INSERT INTO motif_planning (code, libelle, compte_dans_quota, ordre_affichage) VALUES ('TRAVAIL', 'Heure de travail', TRUE, 10), ('CONGE', 'Congé', FALSE, 20), ('MALADIE', 'Absence', TRUE, 30), ('FORMATION', 'Formation', TRUE, 40), ('AUTRE', 'Autre absence (non décomptée)', FALSE, 100) ON DUPLICATE KEY UPDATE libelle = VALUES(libelle), compte_dans_quota = VALUES(compte_dans_quota), ordre_affichage = VALUES(ordre_affichage), actif = TRUE; -- 2. Rattachement des agents aux structures CREATE TABLE IF NOT EXISTS 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, CONSTRAINT fk_agent_structure_agent FOREIGN KEY (id_agent) REFERENCES agent(id_agent), CONSTRAINT fk_agent_structure_structure FOREIGN KEY (id_structure) REFERENCES structure(id_structure), CONSTRAINT chk_agent_structure_dates CHECK (date_fin IS NULL OR date_debut IS NULL OR date_fin >= date_debut), UNIQUE KEY uk_agent_structure_periode (id_agent, id_structure, date_debut) ) ENGINE = InnoDB; -- Jeu d'essai : un seul rattachement principal par agent. INSERT INTO agent_structure (id_agent, id_structure, date_debut, date_fin, actif) VALUES (1, 1, '2026-01-01', NULL, TRUE), (2, 1, '2026-01-01', NULL, TRUE), (3, 2, '2026-01-01', NULL, TRUE) ON DUPLICATE KEY UPDATE date_fin = VALUES(date_fin), actif = VALUES(actif); -- 3. Ajout du motif à chaque créneau existant SET @travail_id = ( SELECT id_motif FROM motif_planning WHERE code = 'TRAVAIL' LIMIT 1 ); SET @col_exists = ( SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'creneau_horaire' AND COLUMN_NAME = 'id_motif' ); SET @sql = IF( @col_exists = 0, 'ALTER TABLE creneau_horaire ADD COLUMN id_motif INT UNSIGNED NULL AFTER id_planning', 'SELECT 1' ); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; UPDATE creneau_horaire SET id_motif = @travail_id WHERE id_motif IS NULL; SET @fk_exists = ( SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND TABLE_NAME = 'creneau_horaire' AND CONSTRAINT_NAME = 'fk_creneau_motif' AND CONSTRAINT_TYPE = 'FOREIGN KEY' ); SET @sql = IF( @fk_exists = 0, 'ALTER TABLE creneau_horaire ADD CONSTRAINT fk_creneau_motif FOREIGN KEY (id_motif) REFERENCES motif_planning(id_motif)', 'SELECT 1' ); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; ALTER TABLE creneau_horaire MODIFY id_motif INT UNSIGNED NOT NULL; SET @idx_exists = ( SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'creneau_horaire' AND INDEX_NAME = 'idx_creneau_motif' ); SET @sql = IF( @idx_exists = 0, 'CREATE INDEX idx_creneau_motif ON creneau_horaire(id_motif)', 'SELECT 1' ); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 4. Vue de synthèse par agent en tenant compte du motif CREATE OR REPLACE VIEW v_suivi_quota_contrat AS SELECT ca.id_contrat, ca.id_agent, a.matricule, CONCAT(a.prenom, ' ', a.nom) AS agent, ca.date_debut, ca.date_fin, ca.quota_minutes, ROUND(ca.quota_minutes / 60, 2) AS quota_heures, COALESCE( SUM( CASE WHEN p.statut = 'VALIDE' AND m.compte_dans_quota = TRUE THEN TIME_TO_SEC(TIMEDIFF(c.heure_fin, c.heure_debut)) / 60 ELSE 0 END ), 0 ) AS minutes_planifiees_validees, ROUND( COALESCE( SUM( CASE WHEN p.statut = 'VALIDE' AND m.compte_dans_quota = TRUE THEN TIME_TO_SEC(TIMEDIFF(c.heure_fin, c.heure_debut)) / 60 ELSE 0 END ), 0 ) / 60, 2 ) AS heures_planifiees_validees, ROUND( ( ca.quota_minutes - COALESCE( SUM( CASE WHEN p.statut = 'VALIDE' AND m.compte_dans_quota = TRUE THEN TIME_TO_SEC(TIMEDIFF(c.heure_fin, c.heure_debut)) / 60 ELSE 0 END ), 0 ) ) / 60, 2 ) AS heures_restantes FROM contrat_agent ca JOIN agent a ON a.id_agent = ca.id_agent LEFT JOIN planning p ON p.id_agent = ca.id_agent LEFT JOIN creneau_horaire c ON c.id_planning = p.id_planning AND c.date_jour BETWEEN ca.date_debut AND COALESCE(ca.date_fin, '9999-12-31') LEFT JOIN motif_planning m ON m.id_motif = c.id_motif GROUP BY ca.id_contrat, ca.id_agent, a.matricule, a.prenom, a.nom, ca.date_debut, ca.date_fin, ca.quota_minutes; -- 5. Mise à jour du contrôle de quota : seuls les motifs marqués -- compte_dans_quota = TRUE sont additionnés. 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; -- -------------------------------------------------------- -- Controle 1 : le planning doit contenir au moins un creneau -- -------------------------------------------------------- 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 creneau horaire.' ); END IF; -- -------------------------------------------------------- -- Controle 2 : chaque creneau doit etre dans la semaine -- -------------------------------------------------------- INSERT INTO tmp_controles_planning(niveau, code, message) SELECT 'ERREUR', 'CRENEAU_HORS_SEMAINE', CONCAT( 'Le creneau #', 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 JOIN semaine s ON s.id_semaine = p.id_semaine 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; -- -------------------------------------------------------- -- Controle 3 : chaque creneau doit etre couvert par un contrat -- -------------------------------------------------------- INSERT INTO tmp_controles_planning(niveau, code, message) SELECT 'ERREUR', 'HORS_CONTRAT', CONCAT( 'Le creneau #', c.id_creneau, ' du ', DATE_FORMAT(c.date_jour, '%d/%m/%Y'), ' n''est couvert par aucun contrat actif pour cet agent.' ) FROM planning p JOIN creneau_horaire c ON c.id_planning = p.id_planning WHERE p.id_planning = p_id_planning AND NOT EXISTS ( SELECT 1 FROM contrat_agent ca WHERE ca.id_agent = p.id_agent AND ca.actif = TRUE AND c.date_jour BETWEEN ca.date_debut AND COALESCE(ca.date_fin, '9999-12-31') ); -- -------------------------------------------------------- -- Controle 4 : chevauchements dans le meme planning -- -------------------------------------------------------- INSERT INTO tmp_controles_planning(niveau, code, message) SELECT 'ERREUR', 'CHEVAUCHEMENT_INTERNE', CONCAT( 'Les creneaux #', 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 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; -- -------------------------------------------------------- -- Controle 5 : chevauchement avec un autre planning valide -- pour le meme agent, meme si la structure est differente. -- -------------------------------------------------------- INSERT INTO tmp_controles_planning(niveau, code, message) SELECT 'ERREUR', 'CHEVAUCHEMENT_AUTRE_PLANNING', CONCAT( 'Le creneau #', c1.id_creneau, ' chevauche le creneau #', c2.id_creneau, ' du planning valide #', p2.id_planning, ' le ', DATE_FORMAT(c1.date_jour, '%d/%m/%Y'), '.' ) FROM planning p1 JOIN creneau_horaire c1 ON c1.id_planning = p1.id_planning JOIN planning p2 ON p2.id_agent = p1.id_agent AND p2.id_planning <> p1.id_planning AND p2.statut = 'VALIDE' 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; -- -------------------------------------------------------- -- Controle 6 : depassement du quota du contrat -- Le quota est ici un quota total sur la periode du contrat. -- Il s'agit d'une ALERTE et non d'une erreur bloquante. -- -------------------------------------------------------- INSERT INTO tmp_controles_planning(niveau, code, message) SELECT 'ALERTE', 'QUOTA_DEPASSE', CONCAT( 'Le contrat #', ca.id_contrat, ' prevoit ', ROUND(ca.quota_minutes / 60, 2), ' h. ', 'Apres validation, ', ROUND( ( -- Minutes deja validees, hors planning courant COALESCE(( SELECT SUM( TIME_TO_SEC(TIMEDIFF(cv.heure_fin, cv.heure_debut)) / 60 ) FROM planning pv JOIN creneau_horaire cv ON cv.id_planning = pv.id_planning JOIN motif_planning mv ON mv.id_motif = cv.id_motif AND mv.compte_dans_quota = TRUE WHERE pv.id_agent = ca.id_agent AND pv.statut = 'VALIDE' AND pv.id_planning <> p_id_planning AND cv.date_jour BETWEEN ca.date_debut AND COALESCE(ca.date_fin, '9999-12-31') ), 0) + -- Minutes du planning courant appartenant a ce contrat COALESCE(( SELECT SUM( TIME_TO_SEC(TIMEDIFF(cc.heure_fin, cc.heure_debut)) / 60 ) FROM creneau_horaire cc JOIN motif_planning mc ON mc.id_motif = cc.id_motif AND mc.compte_dans_quota = TRUE WHERE cc.id_planning = p_id_planning AND cc.date_jour BETWEEN ca.date_debut AND COALESCE(ca.date_fin, '9999-12-31') ), 0) ) / 60, 2 ), ' h seraient planifiees.' ) FROM planning p JOIN contrat_agent ca ON ca.id_agent = p.id_agent AND ca.actif = TRUE WHERE p.id_planning = p_id_planning AND EXISTS ( SELECT 1 FROM creneau_horaire cc WHERE cc.id_planning = p_id_planning AND cc.date_jour BETWEEN ca.date_debut AND COALESCE(ca.date_fin, '9999-12-31') ) AND ( COALESCE(( SELECT SUM( TIME_TO_SEC(TIMEDIFF(cv.heure_fin, cv.heure_debut)) / 60 ) FROM planning pv JOIN creneau_horaire cv ON cv.id_planning = pv.id_planning JOIN motif_planning mv ON mv.id_motif = cv.id_motif AND mv.compte_dans_quota = TRUE WHERE pv.id_agent = ca.id_agent AND pv.statut = 'VALIDE' AND pv.id_planning <> p_id_planning AND cv.date_jour BETWEEN ca.date_debut AND COALESCE(ca.date_fin, '9999-12-31') ), 0) + COALESCE(( SELECT SUM( TIME_TO_SEC(TIMEDIFF(cc.heure_fin, cc.heure_debut)) / 60 ) FROM creneau_horaire cc JOIN motif_planning mc ON mc.id_motif = cc.id_motif AND mc.compte_dans_quota = TRUE WHERE cc.id_planning = p_id_planning AND cc.date_jour BETWEEN ca.date_debut AND COALESCE(ca.date_fin, '9999-12-31') ), 0) ) > ca.quota_minutes; -- Si aucun probleme n'a ete detecte, on retourne explicitement OK. IF NOT EXISTS ( SELECT 1 FROM tmp_controles_planning ) THEN INSERT INTO tmp_controles_planning(niveau, code, message) VALUES ( 'INFO', 'OK', 'Aucune anomalie detectee. Le planning peut etre valide.' ); END IF; END$$ DELIMITER ;