Files
pta/database/migration_planning_motifs.sql
Loic Masi 5fbf76868f pour prod
2026-08-07 16:13:43 +02:00

450 lines
15 KiB
SQL

-- ============================================================
-- 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 ;