diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0f3a3a4 --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# Application +APP_BIND=0.0.0.0 +APP_PORT=8080 +TZ=Europe/Paris + +# MariaDB +DB_NAME=pta +DB_USER=pta +DB_PASSWORD=pta +DB_ROOT_PASSWORD=pta + +# phpMyAdmin +# 127.0.0.1 = uniquement accessible depuis le serveur lui-même. +PMA_BIND=127.0.0.1 +PMA_PORT=8081 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5a9333b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +# syntax=docker/dockerfile:1 + +# ========================================================== +# PHP-FPM +# ========================================================== +FROM php:8.3-fpm-bookworm AS php + +ENV TZ=Europe/Paris + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + libcurl4-openssl-dev \ + libonig-dev \ + && docker-php-ext-install -j"$(nproc)" \ + pdo_mysql \ + mysqli \ + mbstring \ + curl \ + opcache \ + && apt-get purge -y --auto-remove libcurl4-openssl-dev libonig-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /var/www/html + +COPY docker/php/php.ini /usr/local/etc/php/conf.d/99-pta.ini +COPY . /var/www/html + +RUN chown -R www-data:www-data /var/www/html + +USER www-data + +EXPOSE 9000 + +CMD ["php-fpm"] + +# ========================================================== +# NGINX +# ========================================================== +FROM nginx:1.27-alpine AS nginx + +COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf +COPY --from=php /var/www/html /var/www/html + +EXPOSE 80 diff --git a/README_DOCKER.md b/README_DOCKER.md new file mode 100644 index 0000000..6f314ef --- /dev/null +++ b/README_DOCKER.md @@ -0,0 +1,96 @@ +# PTA - Docker / Nginx / PHP / MariaDB / phpMyAdmin + +## Installation + +Copier les fichiers de ce kit à la racine du projet PTA : + +- `Dockerfile` +- `docker-compose.yml` +- `.env.example` +- `docker/nginx/default.conf` +- `docker/php/php.ini` +- `database/pta_prod_schema_mariadb.sql` + +Puis : + +```bash +cp .env.example .env +nano .env +``` + +Changer impérativement `DB_PASSWORD` et `DB_ROOT_PASSWORD`. + +Démarrer : + +```bash +docker compose up -d --build +``` + +Application : + +```text +http://IP_DU_SERVEUR:8080 +``` + +phpMyAdmin est volontairement publié sur `127.0.0.1:8081` par défaut. +Sur le serveur lui-même : + +```text +http://127.0.0.1:8081 +``` + +Pour y accéder depuis un poste distant, le plus sûr est un tunnel SSH : + +```bash +ssh -L 8081:127.0.0.1:8081 utilisateur@serveur +``` + +Puis ouvrir localement : + +```text +http://127.0.0.1:8081 +``` + +## Base de données + +Le schéma `database/pta_prod_schema_mariadb.sql` est exécuté automatiquement uniquement +lorsque le volume MariaDB est créé pour la première fois. + +Il ne sera PAS rejoué à chaque démarrage du conteneur. + +Si la base existe déjà, ne supprimez pas le volume pour appliquer une migration. Exécutez +les migrations explicitement sur la base existante. + +## Commandes utiles + +```bash +# Etat +docker compose ps + +# Logs +docker compose logs -f nginx php db + +# Rebuild après modification du PHP / JS / CSS +docker compose up -d --build php nginx + +# Arrêt +docker compose down + +# Arrêt SANS supprimer les données +docker compose down + +# ATTENTION : supprime la base MariaDB persistée +docker compose down -v +``` + +## Connexion de l'application + +Le `config.php` PTA utilise déjà les variables : + +- `PTA_DB_HOST` +- `PTA_DB_PORT` +- `PTA_DB_NAME` +- `PTA_DB_USER` +- `PTA_DB_PASS` + +Le compose les fournit automatiquement au conteneur PHP. diff --git a/database/install_complet.sql b/database/install_complet.sql deleted file mode 100644 index df94c5b..0000000 --- a/database/install_complet.sql +++ /dev/null @@ -1,1735 +0,0 @@ --- ============================================================ --- 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. --- ============================================================ diff --git a/database/migration_agent_structure_principale.sql b/database/migration_agent_structure_principale.sql deleted file mode 100644 index 64d2df1..0000000 --- a/database/migration_agent_structure_principale.sql +++ /dev/null @@ -1,70 +0,0 @@ --- ============================================================ --- PTA V4.5 - Une structure principale par agent --- MySQL 8+ --- ============================================================ - -SET @col_exists = ( - SELECT COUNT(*) - FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'agent' - AND COLUMN_NAME = 'id_structure' -); - -SET @sql = IF( - @col_exists = 0, - 'ALTER TABLE agent ADD COLUMN id_structure INT UNSIGNED NULL AFTER email', - 'SELECT 1' -); -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; - -SET @fk_exists = ( - SELECT COUNT(*) - FROM information_schema.REFERENTIAL_CONSTRAINTS - WHERE CONSTRAINT_SCHEMA = DATABASE() - AND TABLE_NAME = 'agent' - AND CONSTRAINT_NAME = 'fk_agent_structure_principale' -); - -SET @sql = IF( - @fk_exists = 0, - 'ALTER TABLE agent ADD CONSTRAINT fk_agent_structure_principale FOREIGN KEY (id_structure) REFERENCES structure(id_structure) ON UPDATE CASCADE ON DELETE SET NULL', - 'SELECT 1' -); -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; - -SET @idx_exists = ( - SELECT COUNT(*) - FROM information_schema.STATISTICS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'agent' - AND INDEX_NAME = 'idx_agent_structure_principale' -); -SET @sql = IF( - @idx_exists = 0, - 'CREATE INDEX idx_agent_structure_principale ON agent(id_structure)', - 'SELECT 1' -); -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; - --- Reprise automatique uniquement lorsqu'un agent n'a qu'un seul rattachement actif. -UPDATE agent a -JOIN ( - SELECT id_agent, MIN(id_structure) AS id_structure - FROM agent_structure - WHERE actif = TRUE - AND (date_fin IS NULL OR date_fin >= CURRENT_DATE) - GROUP BY id_agent - HAVING COUNT(DISTINCT id_structure) = 1 -) x ON x.id_agent = a.id_agent -SET a.id_structure = x.id_structure -WHERE a.id_structure IS NULL; - --- Les agents encore sans structure doivent être affectés depuis l'onglet --- "Gestion des agents" de l'application. diff --git a/database/migration_planning_motifs.sql b/database/migration_planning_motifs.sql deleted file mode 100644 index 2411d4b..0000000 --- a/database/migration_planning_motifs.sql +++ /dev/null @@ -1,449 +0,0 @@ --- ============================================================ --- 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 ; diff --git a/database/migration_quota_annuel_vues.sql b/database/migration_quota_annuel_vues.sql deleted file mode 100644 index 0d6f5cb..0000000 --- a/database/migration_quota_annuel_vues.sql +++ /dev/null @@ -1,145 +0,0 @@ --- ============================================================ --- 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 pta_test; - --- ------------------------------------------------------------ --- 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; diff --git a/database/migration_v4_9_type_affectation.sql b/database/migration_v4_9_type_affectation.sql deleted file mode 100644 index 674c891..0000000 --- a/database/migration_v4_9_type_affectation.sql +++ /dev/null @@ -1,10 +0,0 @@ --- PTA V4.9 - Ajout du type d’affectation des agents --- À exécuter une seule fois sur une base existante V4.8. - -ALTER TABLE agent - ADD COLUMN type_affectation ENUM('PERISCOLAIRE', 'EXTRASCOLAIRE') - NOT NULL DEFAULT 'PERISCOLAIRE' - AFTER id_structure; - --- Valeur par défaut : les agents existants sont considérés périscolaires. --- Modifiez ensuite individuellement les agents extrascolaires depuis l’écran Administration. diff --git a/database/migration_v5_13_couverture_structures.sql b/database/migration_v5_13_couverture_structures.sql deleted file mode 100644 index 247e384..0000000 --- a/database/migration_v5_13_couverture_structures.sql +++ /dev/null @@ -1,39 +0,0 @@ --- ============================================================ --- PTA V5.13 - Contrôle de couverture des lieux d'affectation --- MySQL 8+ --- ============================================================ - -CREATE TABLE IF NOT EXISTS 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); diff --git a/database/migration_v5_16_informations_agents.sql b/database/migration_v5_16_informations_agents.sql deleted file mode 100644 index 4032445..0000000 --- a/database/migration_v5_16_informations_agents.sql +++ /dev/null @@ -1,15 +0,0 @@ --- ============================================================ --- PTA V5.16 - Diplôme et coordonnées des agents --- MySQL 8+ --- ============================================================ --- À exécuter une seule fois sur une base V5.15. - -ALTER TABLE agent - ADD COLUMN telephone VARCHAR(30) NULL AFTER email, - ADD COLUMN adresse VARCHAR(255) NULL AFTER telephone, - ADD COLUMN est_diplome BOOLEAN NOT NULL DEFAULT FALSE AFTER adresse, - ADD COLUMN diplome_libelle VARCHAR(150) NULL AFTER est_diplome; - --- Exemples de mise à jour : --- UPDATE agent SET est_diplome = TRUE, diplome_libelle = 'BAFA' WHERE id_agent = 1; --- UPDATE agent SET telephone = '06 00 00 00 00', adresse = '1 rue Exemple, Orléans' WHERE id_agent = 1; diff --git a/database/migration_v5_17_vacances_scolaires.sql b/database/migration_v5_17_vacances_scolaires.sql deleted file mode 100644 index 7fb3b78..0000000 --- a/database/migration_v5_17_vacances_scolaires.sql +++ /dev/null @@ -1,36 +0,0 @@ --- ============================================================ --- 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; diff --git a/database/migration_v5_20_annee_contractuelle.sql b/database/migration_v5_20_annee_contractuelle.sql deleted file mode 100644 index 864d491..0000000 --- a/database/migration_v5_20_annee_contractuelle.sql +++ /dev/null @@ -1,27 +0,0 @@ --- ============================================================ --- PTA V5.20 - Année contractuelle pour le calcul des quotas --- MySQL 8+ --- ============================================================ --- À exécuter une seule fois sur une base V5.19. --- --- Le quota annuel (1607 h à 100 %) n'est plus consommé sur la --- période 01/01 -> 31/12. La période de référence commence à la --- date anniversaire du contrat de chaque agent. --- --- Exemple : date_debut_contrat = 2026-09-01 --- période 1 : 2026-09-01 -> 2027-08-31 --- période 2 : 2027-09-01 -> 2028-08-31 --- --- La migration ne devine volontairement PAS la date de contrat --- des agents existants. Il faut la renseigner depuis Administration --- > Modifier l'agent. Tant qu'elle est NULL, l'application conserve --- temporairement le calcul par année civile et signale le fallback. - -ALTER TABLE agent - ADD COLUMN date_debut_contrat DATE NULL AFTER diplome_libelle; - -CREATE INDEX idx_agent_date_debut_contrat ON agent(date_debut_contrat); - --- Exemples de mise à jour manuelle : --- UPDATE agent SET date_debut_contrat = '2024-09-01' WHERE id_agent = 1; --- UPDATE agent SET date_debut_contrat = '2026-01-15' WHERE id_agent = 2; diff --git a/database/migration_v5_5_motifs.sql b/database/migration_v5_5_motifs.sql deleted file mode 100644 index 00441e1..0000000 --- a/database/migration_v5_5_motifs.sql +++ /dev/null @@ -1,14 +0,0 @@ --- PTA V5.5 - Mise à jour des motifs d'affectation --- Compatible avec une base existante. - -UPDATE motif_planning -SET libelle = 'Absence', - compte_dans_quota = TRUE, - actif = TRUE -WHERE code = 'MALADIE'; - -UPDATE motif_planning -SET libelle = 'Autre absence (non décomptée)', - compte_dans_quota = FALSE, - actif = TRUE -WHERE code = 'AUTRE'; diff --git a/database/migration_v5_7_type_affectation_structure.sql b/database/migration_v5_7_type_affectation_structure.sql deleted file mode 100644 index edf94ed..0000000 --- a/database/migration_v5_7_type_affectation_structure.sql +++ /dev/null @@ -1,33 +0,0 @@ --- ============================================================ --- PTA V5.7 - Le type d'affectation appartient au lieu --- ============================================================ --- A exécuter une seule fois sur une base V5.6. --- La migration reprend, quand c'est possible, le type majoritaire --- des agents actuellement rattachés au lieu puis supprime la colonne --- type_affectation de la table agent. - -ALTER TABLE structure - ADD COLUMN type_affectation ENUM('PERISCOLAIRE', 'EXTRASCOLAIRE') - NOT NULL DEFAULT 'PERISCOLAIRE' - AFTER adresse; - -UPDATE structure s -LEFT JOIN ( - SELECT - id_structure, - CASE - WHEN SUM(type_affectation = 'EXTRASCOLAIRE') > SUM(type_affectation = 'PERISCOLAIRE') - THEN 'EXTRASCOLAIRE' - ELSE 'PERISCOLAIRE' - END AS type_affectation_migre - FROM agent - WHERE id_structure IS NOT NULL - GROUP BY id_structure -) source ON source.id_structure = s.id_structure -SET s.type_affectation = COALESCE(source.type_affectation_migre, 'PERISCOLAIRE'); - -ALTER TABLE agent - DROP COLUMN type_affectation; - --- Vérification conseillée après migration : --- SELECT id_structure, code, nom, type_affectation FROM structure ORDER BY nom; diff --git a/database/migration_v5_9_semaines_types.sql b/database/migration_v5_9_semaines_types.sql deleted file mode 100644 index 3d5aa59..0000000 --- a/database/migration_v5_9_semaines_types.sql +++ /dev/null @@ -1,60 +0,0 @@ --- ============================================================ --- 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; diff --git a/database/migration_v7_5_quota_annee_civile.sql b/database/migration_v7_5_quota_annee_civile.sql deleted file mode 100644 index d9e5c7f..0000000 --- a/database/migration_v7_5_quota_annee_civile.sql +++ /dev/null @@ -1,95 +0,0 @@ --- ============================================================================ --- PTA V7.5 - Quota PTA sur l'année civile --- MySQL 8+ --- À exécuter après la V7 / V7.4. --- ============================================================================ --- Règle métier : --- * 1607 h à 100 % sont planifiées sur l'année civile : 01/01 -> 31/12. --- * la date de début du contrat reste une information RH et ne décale plus --- la période annuelle du PTA ; --- * un agent peut participer à des équipes périscolaires ET extrascolaires. --- Aucun type périscolaire/extrascolaire n'est porté par l'agent. --- --- Cette migration ne supprime pas les anciens PTA construits sur une période --- contractuelle. Elle crée, si besoin, une ligne civile équivalente afin de --- conserver l'historique et d'éviter une migration destructive. --- ============================================================================ - -START TRANSACTION; - -UPDATE quota_agent_annuel -SET commentaire = 'Quota PTA de référence - année civile' -WHERE commentaire IS NULL - OR commentaire LIKE '%contrat%' - OR commentaire LIKE '%période%'; - --- Recrée une ligne PTA civile à partir des anciennes lignes non civiles. --- INSERT IGNORE évite tout conflit si la ligne annuelle existe déjà. -INSERT IGNORE INTO pta_annuel ( - id_agent, - date_debut, - date_fin, - quotite_travail, - quota_reference_minutes, - quota_cible_minutes, - enveloppe_formation_minutes, - conges_annuels_jours_cible, - jours_fractionnement_cible, - statut, - commentaire, - date_validation -) -SELECT - pa.id_agent, - STR_TO_DATE(CONCAT(YEAR(pa.date_debut), '-01-01'), '%Y-%m-%d') AS date_debut, - STR_TO_DATE(CONCAT(YEAR(pa.date_debut), '-12-31'), '%Y-%m-%d') AS date_fin, - pa.quotite_travail, - pa.quota_reference_minutes, - pa.quota_cible_minutes, - pa.enveloppe_formation_minutes, - pa.conges_annuels_jours_cible, - pa.jours_fractionnement_cible, - pa.statut, - CONCAT( - COALESCE(NULLIF(pa.commentaire, ''), ''), - CASE WHEN COALESCE(NULLIF(pa.commentaire, ''), '') = '' THEN '' ELSE ' - ' END, - 'Période convertie en année civile par migration V7.5' - ), - pa.date_validation -FROM pta_annuel pa -WHERE pa.date_debut <> STR_TO_DATE(CONCAT(YEAR(pa.date_debut), '-01-01'), '%Y-%m-%d') - OR pa.date_fin <> STR_TO_DATE(CONCAT(YEAR(pa.date_debut), '-12-31'), '%Y-%m-%d'); - --- La vue de synthèse ne présente désormais que les PTA civils. -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 -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; - -COMMIT; diff --git a/database/migration_v7_pta_annuel_equipes.sql b/database/migration_v7_pta_annuel_equipes.sql deleted file mode 100644 index 65dced2..0000000 --- a/database/migration_v7_pta_annuel_equipes.sql +++ /dev/null @@ -1,313 +0,0 @@ --- ============================================================================ --- PTA V7 - Socle métier annuel, postes, contraintes et équipes --- MySQL 8.0.16+ --- À exécuter après le schéma V5.20 / V6. --- ============================================================================ - -START TRANSACTION; - --- -------------------------------------------------------------------------- --- 1. Postes éligibles au PTA --- -------------------------------------------------------------------------- -CREATE TABLE IF NOT EXISTS 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) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; - -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; - --- -------------------------------------------------------------------------- --- 2. Compléments métier de l’agent --- -------------------------------------------------------------------------- -ALTER TABLE agent - ADD COLUMN id_poste INT UNSIGNED NULL AFTER date_debut_contrat, - ADD COLUMN type_contrat ENUM('PERMANENT', 'TEMPORAIRE') NOT NULL DEFAULT 'PERMANENT' AFTER id_poste, - ADD COLUMN date_fin_contrat DATE NULL AFTER type_contrat, - ADD COLUMN formation_repartition_annuelle BOOLEAN NOT NULL DEFAULT FALSE AFTER date_fin_contrat, - ADD COLUMN commentaire_pta VARCHAR(1000) NULL AFTER formation_repartition_annuelle; - -ALTER TABLE agent - ADD CONSTRAINT fk_agent_poste - FOREIGN KEY (id_poste) REFERENCES poste_agent(id_poste) - ON UPDATE CASCADE ON DELETE SET NULL, - ADD CONSTRAINT chk_agent_dates_contrat_v7 - CHECK (date_fin_contrat IS NULL OR date_debut_contrat IS NULL OR date_fin_contrat >= date_debut_contrat); - -CREATE INDEX idx_agent_poste ON agent(id_poste); -CREATE INDEX idx_agent_type_contrat ON agent(type_contrat, actif); - --- -------------------------------------------------------------------------- --- 3. PTA annuel de l’agent --- -------------------------------------------------------------------------- -CREATE TABLE IF NOT EXISTS 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_0900_ai_ci; - --- -------------------------------------------------------------------------- --- 4. Préférences, interdictions et proximité des lieux --- -------------------------------------------------------------------------- -CREATE TABLE IF NOT EXISTS 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_0900_ai_ci; - --- -------------------------------------------------------------------------- --- 5. Contraintes médicales et temps partiels thérapeutiques --- -------------------------------------------------------------------------- -CREATE TABLE IF NOT EXISTS 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_0900_ai_ci; - --- -------------------------------------------------------------------------- --- 6. Équipes périscolaires des mercredis et extrascolaires --- -------------------------------------------------------------------------- -CREATE TABLE IF NOT EXISTS 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, - nombre_enfants_6_plus 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_0900_ai_ci; - -CREATE TABLE IF NOT EXISTS 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_0900_ai_ci; - --- -------------------------------------------------------------------------- --- 7. Références horaires des lieux pour les automatismes de préparation --- -------------------------------------------------------------------------- -CREATE TABLE IF NOT EXISTS 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_0900_ai_ci; - --- -------------------------------------------------------------------------- --- 8. Motifs détaillés du PTA --- -------------------------------------------------------------------------- -INSERT INTO motif_planning (code, libelle, compte_dans_quota, ordre_affichage, actif) -VALUES - ('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), - ('JNT', 'Journée non travaillée', FALSE, 130, TRUE) -ON DUPLICATE KEY UPDATE - libelle = VALUES(libelle), - compte_dans_quota = VALUES(compte_dans_quota), - ordre_affichage = VALUES(ordre_affichage), - actif = TRUE; - --- La règle métier communiquée précise que les congés annuels ne sont pas --- redécomptés du quota puisqu'ils sont déjà intégrés dans le calcul initial. -UPDATE motif_planning -SET libelle = 'Congé annuel (non décompté)', compte_dans_quota = FALSE -WHERE code = 'CONGE'; - --- -------------------------------------------------------------------------- --- 9. Vues de synthèse --- -------------------------------------------------------------------------- -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 -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 -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, - SUM(CASE WHEN a.est_diplome = TRUE THEN 1 ELSE 0 END) 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, - SUM(CASE WHEN em.fonction_equipe = 'RESPONSABLE' THEN 1 ELSE 0 END) AS responsables, - e.statut -FROM equipe_pta e -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; - -COMMIT; diff --git a/database/pta_mysql_test.sql b/database/pta_mysql_test.sql deleted file mode 100644 index aa85e84..0000000 --- a/database/pta_mysql_test.sql +++ /dev/null @@ -1,1739 +0,0 @@ --- ============================================================ --- Base de donnees de test PTA --- Compatible MySQL 8.0.16+ --- ============================================================ - -DROP DATABASE IF EXISTS pta_test; -CREATE DATABASE pta_test - CHARACTER SET utf8mb4 - COLLATE utf8mb4_0900_ai_ci; - -USE pta_test; - --- ============================================================ --- 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 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; - --- 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 pta_test; - --- ------------------------------------------------------------ --- 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. --- ============================================================ diff --git a/database/pta_prod_schema_mariadb.sql b/database/pta_prod_schema_mariadb.sql new file mode 100644 index 0000000..4b4bf36 --- /dev/null +++ b/database/pta_prod_schema_mariadb.sql @@ -0,0 +1,1180 @@ +-- ============================================================================ +-- 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, + nombre_enfants_6_plus 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. +-- ============================================================================ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..25fa9b4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,87 @@ +services: + db: + image: mariadb:11.4 + container_name: pta-db + restart: unless-stopped + environment: + MARIADB_DATABASE: ${DB_NAME:-pta} + MARIADB_USER: ${DB_USER:-pta} + MARIADB_PASSWORD: ${DB_PASSWORD:-pta} + MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-pta} + TZ: ${TZ:-Europe/Paris} + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + volumes: + - mariadb_data:/var/lib/mysql + # Ce fichier n'est exécuté QUE lors de l'initialisation d'un volume vide. + - ./database/pta_prod_schema_mariadb.sql:/docker-entrypoint-initdb.d/01-pta-schema.sql:ro + healthcheck: + test: ["CMD-SHELL", "mariadb-admin ping -h 127.0.0.1 -uroot -p\"$${MARIADB_ROOT_PASSWORD}\" --silent"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s + networks: + - pta + + php: + build: + context: . + dockerfile: Dockerfile + target: php + container_name: pta-php + restart: unless-stopped + environment: + PTA_DB_HOST: db + PTA_DB_PORT: 3306 + PTA_DB_NAME: ${DB_NAME:-pta} + PTA_DB_USER: ${DB_USER:-pta} + PTA_DB_PASS: ${DB_PASSWORD:-change_me} + TZ: ${TZ:-Europe/Paris} + depends_on: + db: + condition: service_healthy + networks: + - pta + + nginx: + build: + context: . + dockerfile: Dockerfile + target: nginx + container_name: pta-nginx + restart: unless-stopped + ports: + - "${APP_BIND:-0.0.0.0}:${APP_PORT:-8080}:80" + depends_on: + - php + networks: + - pta + + phpmyadmin: + image: phpmyadmin:5.2.2 + container_name: pta-phpmyadmin + restart: unless-stopped + environment: + PMA_HOST: db + PMA_PORT: 3306 + PMA_USER: ${DB_USER:-pta} + PMA_PASSWORD: ${DB_PASSWORD:-change_me} + UPLOAD_LIMIT: 64M + TZ: ${TZ:-Europe/Paris} + ports: + # Par défaut phpMyAdmin n'est accessible que depuis la machine hôte. + - "${PMA_BIND:-127.0.0.1}:${PMA_PORT:-8081}:80" + depends_on: + db: + condition: service_healthy + networks: + - pta + +networks: + pta: + driver: bridge + +volumes: + mariadb_data: diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf new file mode 100644 index 0000000..7302de1 --- /dev/null +++ b/docker/nginx/default.conf @@ -0,0 +1,55 @@ +server { + listen 80; + server_name _; + + root /var/www/html; + index index.php; + + charset utf-8; + client_max_body_size 64M; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + try_files $uri =404; + + include fastcgi_params; + fastcgi_pass php:9000; + fastcgi_index index.php; + + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT $document_root; + fastcgi_param HTTP_PROXY ""; + + fastcgi_read_timeout 120s; + } + + # Les classes, modèles et fichiers SQL ne doivent jamais être servis directement. + location ^~ /app/ { + deny all; + return 404; + } + + location ^~ /database/ { + deny all; + return 404; + } + + location ~* ^/(config\.php|db\.php|page_bootstrap\.php|README[^/]*)$ { + deny all; + return 404; + } + + location ~ /\. { + deny all; + return 404; + } + + location ~* \.(?:css|js|png|jpg|jpeg|gif|svg|ico|webp)$ { + expires 7d; + add_header Cache-Control "public, max-age=604800"; + try_files $uri =404; + } +} diff --git a/docker/php/php.ini b/docker/php/php.ini new file mode 100644 index 0000000..30c46e2 --- /dev/null +++ b/docker/php/php.ini @@ -0,0 +1,24 @@ +date.timezone = Europe/Paris +memory_limit = 256M +max_execution_time = 120 +max_input_time = 120 +post_max_size = 64M +upload_max_filesize = 64M + +session.cookie_httponly = 1 +session.cookie_samesite = Lax +session.use_strict_mode = 1 + +expose_php = Off + +display_errors = Off +display_startup_errors = Off +log_errors = On +error_reporting = E_ALL + +opcache.enable = 1 +opcache.enable_cli = 0 +opcache.memory_consumption = 128 +opcache.interned_strings_buffer = 16 +opcache.max_accelerated_files = 10000 +opcache.validate_timestamps = 0