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

1736 lines
56 KiB
SQL

-- ============================================================
-- Base de donnees de test PTA
-- Compatible MySQL 8.0.16+
-- ============================================================
USE nfbhnajdb1;
-- ============================================================
-- 1. REFERENTIEL
-- ============================================================
CREATE TABLE agent (
id_agent INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
matricule VARCHAR(50) NOT NULL UNIQUE,
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_structure INT UNSIGNED NULL,
actif BOOLEAN NOT NULL DEFAULT TRUE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE = InnoDB;
CREATE TABLE structure (
id_structure INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(50) NOT NULL UNIQUE,
nom VARCHAR(150) NOT NULL,
adresse VARCHAR(255) NULL,
type_affectation ENUM('PERISCOLAIRE', 'EXTRASCOLAIRE') NOT NULL DEFAULT 'PERISCOLAIRE',
actif BOOLEAN NOT NULL DEFAULT TRUE
) ENGINE = InnoDB;
ALTER TABLE agent
ADD CONSTRAINT fk_agent_structure_principale
FOREIGN KEY (id_structure) REFERENCES structure(id_structure)
ON UPDATE CASCADE
ON DELETE SET NULL;
CREATE INDEX idx_agent_structure_principale ON agent(id_structure);
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
)
) ENGINE = InnoDB;
CREATE INDEX idx_couverture_structure_jour
ON structure_couverture_horaire(id_structure, jour_semaine, heure_debut, heure_fin);
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;
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),
CONSTRAINT fk_capacite_tranche_age
FOREIGN KEY (id_tranche_age)
REFERENCES tranche_age(id_tranche_age),
CONSTRAINT chk_nombre_places
CHECK (nombre_places > 0)
) ENGINE = InnoDB;
-- ============================================================
-- 2. CONTRATS ET QUOTAS
-- ============================================================
CREATE TABLE contrat_agent (
id_contrat INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
id_agent INT UNSIGNED NOT NULL,
date_debut DATE NOT NULL,
date_fin DATE NULL,
-- Quota total sur la periode du contrat, en minutes.
-- Le stockage en minutes evite les problemes d'arrondis.
quota_minutes INT UNSIGNED NOT NULL,
type_contrat VARCHAR(100) NULL,
actif BOOLEAN NOT NULL DEFAULT TRUE,
CONSTRAINT fk_contrat_agent
FOREIGN KEY (id_agent)
REFERENCES agent(id_agent),
CONSTRAINT chk_dates_contrat
CHECK (date_fin IS NULL OR date_fin >= date_debut),
CONSTRAINT chk_quota_contrat
CHECK (quota_minutes > 0)
) ENGINE = InnoDB;
CREATE INDEX idx_contrat_agent_dates
ON contrat_agent(id_agent, date_debut, date_fin);
-- ============================================================
-- 3. SEMAINES
-- ============================================================
CREATE TABLE semaine (
id_semaine INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
annee SMALLINT UNSIGNED NOT NULL,
numero_semaine TINYINT UNSIGNED NOT NULL,
date_debut DATE NOT NULL,
date_fin DATE NOT NULL,
statut ENUM('OUVERTE', 'VALIDEE', 'VERROUILLEE')
NOT NULL DEFAULT 'OUVERTE',
UNIQUE KEY uk_semaine_annee_numero (annee, numero_semaine),
UNIQUE KEY uk_semaine_dates (date_debut, date_fin),
CONSTRAINT chk_numero_semaine
CHECK (numero_semaine BETWEEN 1 AND 53),
CONSTRAINT chk_dates_semaine
CHECK (date_fin >= date_debut)
) ENGINE = InnoDB;
-- ============================================================
-- 4. PLANNINGS ET CRENEAUX
-- ============================================================
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),
CONSTRAINT fk_planning_semaine
FOREIGN KEY (id_semaine)
REFERENCES semaine(id_semaine),
CONSTRAINT fk_planning_structure
FOREIGN KEY (id_structure)
REFERENCES structure(id_structure),
CONSTRAINT fk_planning_source
FOREIGN KEY (id_planning_source)
REFERENCES planning(id_planning)
ON DELETE SET NULL,
UNIQUE KEY uk_planning_agent_semaine_structure
(id_agent, id_semaine, id_structure)
) ENGINE = InnoDB;
CREATE INDEX idx_planning_agent_statut
ON planning(id_agent, statut);
CREATE TABLE creneau_horaire (
id_creneau INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
id_planning INT UNSIGNED NOT NULL,
date_jour DATE NOT NULL,
heure_debut TIME NOT NULL,
heure_fin TIME NOT NULL,
CONSTRAINT fk_creneau_planning
FOREIGN KEY (id_planning)
REFERENCES planning(id_planning)
ON DELETE CASCADE,
CONSTRAINT chk_creneau_ordre
CHECK (heure_fin > heure_debut),
-- Les heures doivent tomber sur un quart d'heure exact.
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
)
) ENGINE = InnoDB;
CREATE INDEX idx_creneau_planning_date
ON creneau_horaire(id_planning, date_jour, heure_debut, heure_fin);
-- ============================================================
-- 5. VUES DE SUIVI
-- ============================================================
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'
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'
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'
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')
GROUP BY
ca.id_contrat,
ca.id_agent,
a.matricule,
a.prenom,
a.nom,
ca.date_debut,
ca.date_fin,
ca.quota_minutes;
CREATE OR REPLACE VIEW v_planning_agent_semaine AS
SELECT
p.id_planning,
p.id_agent,
a.matricule,
CONCAT(a.prenom, ' ', a.nom) AS agent,
s.annee,
s.numero_semaine,
st.nom AS structure,
p.statut,
ROUND(
COALESCE(
SUM(TIME_TO_SEC(TIMEDIFF(c.heure_fin, c.heure_debut)) / 3600),
0
),
2
) AS heures_planifiees
FROM planning p
JOIN agent a
ON a.id_agent = p.id_agent
JOIN semaine s
ON s.id_semaine = p.id_semaine
JOIN structure st
ON st.id_structure = p.id_structure
LEFT JOIN creneau_horaire c
ON c.id_planning = p.id_planning
GROUP BY
p.id_planning,
p.id_agent,
a.matricule,
a.prenom,
a.nom,
s.annee,
s.numero_semaine,
st.nom,
p.statut;
-- ============================================================
-- 6. PROCEDURES DE CONTROLE ET DE 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;
-- --------------------------------------------------------
-- 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
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
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
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
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$$
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) detectee(s). Premiere 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
SET v_message = CONCAT(
'Validation suspendue : le quota contractuel serait depasse. ',
'Relancer avec p_forcer_depassement_quota = TRUE pour forcer la validation.'
);
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = v_message;
END IF;
UPDATE planning
SET
statut = 'VALIDE',
date_validation = CURRENT_TIMESTAMP
WHERE id_planning = p_id_planning;
SELECT
p.id_planning,
p.statut,
p.date_validation,
'Planning valide avec succes.' AS message
FROM planning p
WHERE p.id_planning = p_id_planning;
END$$
DELIMITER ;
-- ============================================================
-- 7. JEU D'ESSAI
-- ============================================================
INSERT INTO agent (
id_agent,
matricule,
nom,
prenom,
email,
telephone,
adresse,
est_diplome,
diplome_libelle,
date_debut_contrat
)
VALUES
(1, 'AG001', 'MARTIN', 'Alice', 'alice.martin@example.test', '06 10 20 30 40', '12 rue des Lilas, Orléans', TRUE, 'BAFA', '2026-07-01'),
(2, 'AG002', 'DURAND', 'Bruno', 'bruno.durand@example.test', '06 11 21 31 41', '8 avenue de Paris, Orléans', FALSE, NULL, '2026-07-01'),
(3, 'AG003', 'PETIT', 'Chloe', 'chloe.petit@example.test', '06 12 22 32 42', '5 rue Royale, Orléans', TRUE, 'CAP AEPE', '2026-07-22');
INSERT INTO structure (
id_structure,
code,
nom,
adresse,
type_affectation
)
VALUES
(1, 'STR001', 'Creche des Lilas', '10 rue des Lilas, Orleans', 'PERISCOLAIRE'),
(2, 'STR002', 'Accueil Madeleine', '36 rue du Faubourg Madeleine, Orleans', 'EXTRASCOLAIRE');
-- Plages de couverture de test. Elles sont configurables dans l'administration.
INSERT INTO structure_couverture_horaire (id_structure, jour_semaine, heure_debut, heure_fin, minimum_agents)
VALUES
(1, 1, '07:30:00', '09:00:00', 1),
(1, 1, '16:30:00', '18:30:00', 1),
(1, 2, '07:30:00', '09:00:00', 1),
(1, 2, '16:30:00', '18:30:00', 1),
(1, 3, '07:30:00', '09:00:00', 1),
(1, 3, '16:30:00', '18:30:00', 1),
(1, 4, '07:30:00', '09:00:00', 1),
(1, 4, '16:30:00', '18:30:00', 1),
(1, 5, '07:30:00', '09:00:00', 1),
(1, 5, '16:30:00', '18:30:00', 1),
(2, 1, '08:00:00', '18:00:00', 1),
(2, 2, '08:00:00', '18:00:00', 1),
(2, 3, '08:00:00', '18:00:00', 1),
(2, 4, '08:00:00', '18:00:00', 1),
(2, 5, '08:00:00', '18:00:00', 1);
-- Une structure principale par agent.
UPDATE agent SET id_structure = 1 WHERE id_agent IN (1, 2);
UPDATE agent SET id_structure = 2 WHERE id_agent = 3;
INSERT INTO tranche_age (
id_tranche_age,
libelle,
age_min_mois,
age_max_mois
)
VALUES
(1, '0 - 3 ans', 0, 35),
(2, '3 - 6 ans', 36, 71),
(3, '6 - 11 ans', 72, 131);
INSERT INTO structure_capacite (
id_structure,
id_tranche_age,
nombre_places
)
VALUES
(1, 1, 20),
(1, 2, 12),
(2, 2, 18),
(2, 3, 30);
-- Alice : 100 h sur juillet/aout pour permettre plusieurs plannings valides.
-- Bruno : 35 h au total, afin de provoquer un depassement de quota.
-- Chloe : contrat qui commence le 22/07, afin de tester un planning hors contrat.
INSERT INTO contrat_agent (
id_contrat,
id_agent,
date_debut,
date_fin,
quota_minutes,
type_contrat
)
VALUES
(1, 1, '2026-07-01', '2026-08-31', 6000, 'PTA ete - 100 h'),
(2, 2, '2026-07-01', '2026-07-31', 2100, 'PTA juillet - 35 h'),
(3, 3, '2026-07-22', '2026-08-31', 2100, 'PTA ete - 35 h');
INSERT INTO semaine (
id_semaine,
annee,
numero_semaine,
date_debut,
date_fin
)
VALUES
(1, 2026, 29, '2026-07-13', '2026-07-19'),
(2, 2026, 30, '2026-07-20', '2026-07-26'),
(3, 2026, 31, '2026-07-27', '2026-08-02');
-- ------------------------------------------------------------
-- Planning #1 : Alice, semaine 29, 32 h, deja valide
-- ------------------------------------------------------------
INSERT INTO planning (
id_planning,
id_agent,
id_semaine,
id_structure,
statut,
commentaire,
date_validation
)
VALUES
(1, 1, 1, 1, 'VALIDE', 'Planning valide de reference - 32 h', CURRENT_TIMESTAMP);
INSERT INTO creneau_horaire (id_planning, date_jour, heure_debut, heure_fin)
VALUES
(1, '2026-07-13', '08:00:00', '12:00:00'),
(1, '2026-07-13', '13:00:00', '17:00:00'),
(1, '2026-07-14', '08:00:00', '12:00:00'),
(1, '2026-07-14', '13:00:00', '17:00:00'),
(1, '2026-07-15', '08:00:00', '12:00:00'),
(1, '2026-07-15', '13:00:00', '17:00:00'),
(1, '2026-07-16', '08:00:00', '12:00:00'),
(1, '2026-07-16', '13:00:00', '17:00:00');
-- ------------------------------------------------------------
-- Planning #2 : Alice, semaine 30, 32 h, deja valide
-- Sert de planning de reference pour tester les chevauchements.
-- ------------------------------------------------------------
INSERT INTO planning (
id_planning,
id_agent,
id_semaine,
id_structure,
statut,
commentaire,
date_validation
)
VALUES
(2, 1, 2, 1, 'VALIDE', 'Deuxieme planning valide de reference - 32 h', CURRENT_TIMESTAMP);
INSERT INTO creneau_horaire (id_planning, date_jour, heure_debut, heure_fin)
VALUES
(2, '2026-07-20', '08:00:00', '12:00:00'),
(2, '2026-07-20', '13:00:00', '17:00:00'),
(2, '2026-07-21', '08:00:00', '12:00:00'),
(2, '2026-07-21', '13:00:00', '17:00:00'),
(2, '2026-07-22', '08:00:00', '12:00:00'),
(2, '2026-07-22', '13:00:00', '17:00:00'),
(2, '2026-07-23', '08:00:00', '12:00:00'),
(2, '2026-07-23', '13:00:00', '17:00:00');
-- ------------------------------------------------------------
-- Planning #3 : Alice, semaine 30, autre structure
-- ERREUR ATTENDUE : chevauchement avec le planning #2.
-- ------------------------------------------------------------
INSERT INTO planning (
id_planning,
id_agent,
id_semaine,
id_structure,
statut,
commentaire
)
VALUES
(3, 1, 2, 2, 'BROUILLON', 'Test chevauchement entre deux structures');
INSERT INTO creneau_horaire (id_planning, date_jour, heure_debut, heure_fin)
VALUES
(3, '2026-07-20', '10:00:00', '12:00:00');
-- ------------------------------------------------------------
-- Planning #4 : Bruno, semaine 29, 30 h, deja valide
-- ------------------------------------------------------------
INSERT INTO planning (
id_planning,
id_agent,
id_semaine,
id_structure,
statut,
commentaire,
date_validation
)
VALUES
(4, 2, 1, 1, 'VALIDE', 'Planning valide Bruno - 30 h sur quota de 35 h', CURRENT_TIMESTAMP);
INSERT INTO creneau_horaire (id_planning, date_jour, heure_debut, heure_fin)
VALUES
(4, '2026-07-13', '08:00:00', '12:00:00'),
(4, '2026-07-13', '13:00:00', '16:00:00'),
(4, '2026-07-14', '08:00:00', '12:00:00'),
(4, '2026-07-14', '13:00:00', '16:00:00'),
(4, '2026-07-15', '08:00:00', '12:00:00'),
(4, '2026-07-15', '13:00:00', '16:00:00'),
(4, '2026-07-16', '08:00:00', '12:00:00'),
(4, '2026-07-16', '13:00:00', '16:00:00'),
(4, '2026-07-17', '08:00:00', '10:00:00');
-- ------------------------------------------------------------
-- Planning #5 : Bruno, semaine 30, 10 h
-- ALERTE ATTENDUE : quota depasse (30 h deja validees + 10 h = 40 h > 35 h).
-- ------------------------------------------------------------
INSERT INTO planning (
id_planning,
id_agent,
id_semaine,
id_structure,
statut,
commentaire
)
VALUES
(5, 2, 2, 1, 'BROUILLON', 'Test depassement de quota');
INSERT INTO creneau_horaire (id_planning, date_jour, heure_debut, heure_fin)
VALUES
(5, '2026-07-20', '08:00:00', '12:00:00'),
(5, '2026-07-21', '08:00:00', '12:00:00'),
(5, '2026-07-22', '08:00:00', '10:00:00');
-- ------------------------------------------------------------
-- Planning #6 : Chloe, semaine 29
-- ERREUR ATTENDUE : hors contrat, son contrat commence le 22/07.
-- ------------------------------------------------------------
INSERT INTO planning (
id_planning,
id_agent,
id_semaine,
id_structure,
statut,
commentaire
)
VALUES
(6, 3, 1, 1, 'BROUILLON', 'Test creneau avant le debut du contrat');
INSERT INTO creneau_horaire (id_planning, date_jour, heure_debut, heure_fin)
VALUES
(6, '2026-07-15', '08:00:00', '12:00:00');
-- ------------------------------------------------------------
-- Planning #7 : Bruno, semaine 30, deuxieme structure
-- ERREUR ATTENDUE : deux creneaux se chevauchent dans le meme planning.
-- ------------------------------------------------------------
INSERT INTO planning (
id_planning,
id_agent,
id_semaine,
id_structure,
statut,
commentaire
)
VALUES
(7, 2, 2, 2, 'BROUILLON', 'Test chevauchement interne');
INSERT INTO creneau_horaire (id_planning, date_jour, heure_debut, heure_fin)
VALUES
(7, '2026-07-23', '08:00:00', '12:00:00'),
(7, '2026-07-23', '11:00:00', '14:00:00');
-- ------------------------------------------------------------
-- Planning #8 : Alice, semaine 31, 24 h
-- CAS VALIDE ATTENDU : 64 h deja validees + 24 h = 88 h sur quota de 100 h.
-- ------------------------------------------------------------
INSERT INTO planning (
id_planning,
id_agent,
id_semaine,
id_structure,
statut,
commentaire,
id_planning_source
)
VALUES
(8, 1, 3, 1, 'BROUILLON', 'Cas nominal pouvant etre valide', 2);
INSERT INTO creneau_horaire (id_planning, date_jour, heure_debut, heure_fin)
VALUES
(8, '2026-07-27', '08:00:00', '12:00:00'),
(8, '2026-07-27', '13:00:00', '17:00:00'),
(8, '2026-07-28', '08:00:00', '12:00:00'),
(8, '2026-07-28', '13:00:00', '17:00:00'),
(8, '2026-07-29', '08:00:00', '12:00:00'),
(8, '2026-07-29', '13:00:00', '17:00:00');
-- ------------------------------------------------------------
-- Planning #9 : Chloe, semaine 30
-- ERREUR ATTENDUE : le 27/07 appartient a la semaine 31, pas a la semaine 30.
-- Le creneau est volontairement couvert par le contrat pour isoler ce controle.
-- ------------------------------------------------------------
INSERT INTO planning (
id_planning,
id_agent,
id_semaine,
id_structure,
statut,
commentaire
)
VALUES
(9, 3, 2, 1, 'BROUILLON', 'Test date hors de la semaine du planning');
INSERT INTO creneau_horaire (id_planning, date_jour, heure_debut, heure_fin)
VALUES
(9, '2026-07-27', '08:00:00', '12:00:00');
-- ============================================================
-- 8. REQUETES DE TEST A EXECUTER MANUELLEMENT
-- ============================================================
-- CAS 1 : cas nominal, aucune anomalie attendue.
-- CALL sp_controler_planning(8);
-- CAS 2 : validation du cas nominal.
-- CALL sp_valider_planning(8, FALSE);
-- CAS 3 : chevauchement avec un autre planning valide.
-- CALL sp_controler_planning(3);
-- CALL sp_valider_planning(3, FALSE);
-- CAS 4 : depassement de quota.
-- CALL sp_controler_planning(5);
-- La validation ci-dessous doit etre refusee :
-- CALL sp_valider_planning(5, FALSE);
-- La validation peut etre forcee apres confirmation utilisateur :
-- CALL sp_valider_planning(5, TRUE);
-- CAS 5 : creneau hors contrat.
-- CALL sp_controler_planning(6);
-- CALL sp_valider_planning(6, FALSE);
-- CAS 6 : chevauchement interne.
-- CALL sp_controler_planning(7);
-- CALL sp_valider_planning(7, FALSE);
-- CAS 7 : date en dehors de la semaine selectionnee.
-- CALL sp_controler_planning(9);
-- CALL sp_valider_planning(9, FALSE);
-- CAS 8 : controle direct par contrainte SQL des quarts d'heure.
-- L'INSERT suivant doit echouer a cause du CHECK chk_creneau_quart_heure :
-- INSERT INTO creneau_horaire(id_planning, date_jour, heure_debut, heure_fin)
-- VALUES (8, '2026-07-30', '08:10:00', '10:00:00');
-- CAS 9 : consultation du suivi de quota par contrat.
-- SELECT * FROM v_suivi_quota_contrat ORDER BY id_contrat;
-- CAS 10 : synthese des heures par planning.
-- SELECT * FROM v_planning_agent_semaine ORDER BY id_planning;
-- ============================================================
-- 9. LIMITES FONCTIONNELLES VOLONTAIRES DU MVP
-- ============================================================
-- La capacite par tranche d'age est stockee dans structure_capacite,
-- mais elle ne peut pas encore etre controlee automatiquement avec le
-- modele actuel car aucun effectif d'enfants/usagers n'est associe a un
-- planning. Pour controler la capacite, il faudra ajouter une table
-- d'effectif ou d'affectation par structure, date et tranche d'age.
--
-- De meme, les heures ci-dessus sont des heures PLANIFIEES et non des
-- heures reellement EFFECTUEES. Pour distinguer les deux, il faudra
-- ajouter un module de pointage ou de realisation effective.
-- ============================================================
-- Extension de la base PTA pour l'interface de gestion planning
-- À exécuter APRES pta_mysql_test.sql
-- Compatible MySQL 8+
-- ============================================================
USE nfbhnajdb1;
-- 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;
-- Historique initial cohérent avec la structure principale de chaque 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 ;
-- ============================================================
-- PTA - Quotas annuels et vues de pilotage
-- À exécuter APRÈS :
-- 1. pta_mysql_test.sql
-- 2. migration_planning_motifs.sql
-- Compatible MySQL 8+
-- ============================================================
USE nfbhnajdb1;
-- ------------------------------------------------------------
-- 1. Quota annuel d'un agent
-- 1607 h = 96 420 minutes à 100 %.
-- La quotité permet de proratiser automatiquement le quota cible.
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS 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,
quota_cible_minutes INT UNSIGNED NOT NULL,
commentaire VARCHAR(255) NULL,
CONSTRAINT fk_quota_annuel_agent
FOREIGN KEY (id_agent) REFERENCES agent(id_agent),
CONSTRAINT chk_quotite_travail
CHECK (quotite_travail > 0 AND quotite_travail <= 100),
CONSTRAINT chk_quota_annuel_positif
CHECK (quota_cible_minutes > 0),
UNIQUE KEY uk_quota_agent_annee (id_agent, annee)
) ENGINE = InnoDB;
-- Jeu d'essai 2026 : tous les agents existants à 100 %, soit 1607 h.
INSERT INTO quota_agent_annuel (
id_agent,
annee,
quotite_travail,
quota_reference_minutes,
quota_cible_minutes,
commentaire
)
SELECT
a.id_agent,
2026,
100.00,
96420,
96420,
'Quota annuel de référence à 100 %'
FROM agent a
WHERE NOT EXISTS (
SELECT 1
FROM quota_agent_annuel q
WHERE q.id_agent = a.id_agent
AND q.annee = 2026
);
-- Exemple de modification pour un agent à 80 % :
-- UPDATE quota_agent_annuel
-- SET quotite_travail = 80.00,
-- quota_cible_minutes = ROUND(96420 * 0.80)
-- WHERE id_agent = 2 AND annee = 2026;
-- ------------------------------------------------------------
-- 2. Règle de décompte des motifs
-- L'application reste paramétrable : le booléen compte_dans_quota
-- indique si le motif consomme le quota annuel.
-- Selon le besoin exprimé, les motifs principaux ci-dessous sont
-- comptés par défaut. Il est possible de changer cette règle ensuite.
-- ------------------------------------------------------------
UPDATE motif_planning
SET compte_dans_quota = TRUE
WHERE code IN ('TRAVAIL', 'FORMATION', 'CONGE', 'MALADIE');
-- ------------------------------------------------------------
-- 3. Vue annuelle de suivi des quotas
-- Les brouillons et les plannings validés sont distingués.
-- ------------------------------------------------------------
CREATE OR REPLACE VIEW v_suivi_quota_annuel AS
SELECT
q.id_agent,
a.matricule,
a.nom,
a.prenom,
q.annee,
q.quotite_travail,
q.quota_reference_minutes,
q.quota_cible_minutes,
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,
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 YEAR(c.date_jour) = q.annee
LEFT JOIN motif_planning m ON m.id_motif = c.id_motif
GROUP BY
q.id_agent,
a.matricule,
a.nom,
a.prenom,
q.annee,
q.quotite_travail,
q.quota_reference_minutes,
q.quota_cible_minutes;
-- ============================================================
-- PTA V5.9 - Semaines types par agent
-- ============================================================
-- Un modèle mémorise les affectations d'une semaine de référence :
-- jour ouvré, lieu, motif et plage horaire.
CREATE TABLE IF NOT EXISTS 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 DELETE CASCADE,
UNIQUE KEY uk_modele_semaine_agent_nom (id_agent, nom),
KEY idx_modele_semaine_agent (id_agent, date_modification)
) ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS 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 DELETE CASCADE,
CONSTRAINT fk_modele_creneau_structure
FOREIGN KEY (id_structure)
REFERENCES structure(id_structure),
CONSTRAINT fk_modele_creneau_motif
FOREIGN KEY (id_motif)
REFERENCES motif_planning(id_motif),
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
),
KEY idx_modele_creneau_modele_jour (id_modele, jour_semaine, heure_debut)
) ENGINE = InnoDB;
-- ============================================================
-- PTA V5.17 - Périodes de vacances scolaires
-- ============================================================
-- Les dates sont validées manuellement par l'utilisateur avant
-- d'être enregistrées, même lorsqu'elles proviennent de l'API
-- officielle du calendrier scolaire.
--
-- date_fin est stockée de façon INCLUSIVE dans PTA.
-- Pour une proposition issue de l'API, la date de reprise fournie
-- par l'API est convertie en dernier jour de vacances (veille).
CREATE TABLE IF NOT EXISTS 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,
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,
CONSTRAINT chk_periode_vacance_dates
CHECK (date_fin >= date_debut),
UNIQUE KEY uk_periode_vacance (
date_debut,
date_fin,
annee_scolaire,
academie
),
KEY idx_periode_vacance_dates (date_debut, date_fin),
KEY idx_periode_vacance_annee (annee_scolaire)
) ENGINE = InnoDB;
-- ============================================================
-- Historique V5.20 - date de début de contrat
-- IMPORTANT : depuis la V7.5, le quota PTA est suivi sur l'année civile
-- (01/01 -> 31/12). La date de contrat n'est plus l'ancre du quota.
-- ============================================================