Droit à l'enveloppe de formation par agent, et motif « Formation » filtré à la saisie
Nouvelle colonne `agent.droit_enveloppe_formation`, cochée par défaut, saisie à la création de l'agent et modifiable depuis sa fiche. Un agent sans ce droit : - ne voit plus le motif « Formation » dans le sélecteur de motif de l'éditeur de planning ni dans le modal de la vue individuelle ; - se voit refuser toute affectation de ce motif par le serveur, avec un message qui nomme le motif et l'agent (save_planning.php et update_agent_entry.php) ; - a une enveloppe de formation de 0 min au PTA annuel, et une ALERTE FORMATION_SANS_DROIT si des heures de formation subsistent dans son planning. À ne pas confondre avec `formation_repartition_annuelle`, qui existait déjà : celle-ci dit que la formation est déjà comptée dans la répartition annuelle — l'agent y a droit, l'enveloppe complémentaire n'est simplement pas ajoutée. Les deux règles sont désormais rassemblées dans AgentModel::trainingEnvelopeMinutes(), qui remplace les deux calculs « 840 ou 0 » jusque-là dupliqués. Le motif interdit reste affiché lorsqu'il est déjà celui de la plage en cours d'édition : le retirer de la liste changerait silencieusement le motif d'une saisie existante. C'est le contrôle de saisie, puis le serveur, qui demandent alors de corriger la plage. La migration est nommée v7_9b et non v7_10 : les fichiers sont joués dans l'ordre alphabétique, où « v7_10 » passerait avant « v7_8 ». Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -362,6 +362,9 @@ final class AgentController
|
|||||||
$postId = filter_var($payload['id_poste'] ?? null, FILTER_VALIDATE_INT);
|
$postId = filter_var($payload['id_poste'] ?? null, FILTER_VALIDATE_INT);
|
||||||
$contractType = strtoupper(trim((string) ($payload['type_contrat'] ?? 'PERMANENT')));
|
$contractType = strtoupper(trim((string) ($payload['type_contrat'] ?? 'PERMANENT')));
|
||||||
$trainingPlanned = filter_var($payload['formation_repartition_annuelle'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
$trainingPlanned = filter_var($payload['formation_repartition_annuelle'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||||
|
// Le droit à l'enveloppe de formation est acquis par défaut : un
|
||||||
|
// formulaire ancien ou une intégration tierce ne doit pas le retirer.
|
||||||
|
$trainingRight = filter_var($payload['droit_enveloppe_formation'] ?? true, FILTER_VALIDATE_BOOLEAN);
|
||||||
$ptaComment = trim((string) ($payload['commentaire_pta'] ?? ''));
|
$ptaComment = trim((string) ($payload['commentaire_pta'] ?? ''));
|
||||||
$validPostIds = array_map(static fn(array $post): int => (int) $post['id_poste'], $this->agentModel->allPosts());
|
$validPostIds = array_map(static fn(array $post): int => (int) $post['id_poste'], $this->agentModel->allPosts());
|
||||||
|
|
||||||
@@ -406,6 +409,7 @@ final class AgentController
|
|||||||
'id_poste' => (int) $postId,
|
'id_poste' => (int) $postId,
|
||||||
'type_contrat' => $contractType,
|
'type_contrat' => $contractType,
|
||||||
'formation_repartition_annuelle' => $trainingPlanned,
|
'formation_repartition_annuelle' => $trainingPlanned,
|
||||||
|
'droit_enveloppe_formation' => $trainingRight,
|
||||||
'commentaire_pta' => $ptaComment !== '' ? $ptaComment : null,
|
'commentaire_pta' => $ptaComment !== '' ? $ptaComment : null,
|
||||||
]);
|
]);
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
@@ -437,6 +441,7 @@ final class AgentController
|
|||||||
'poste_libelle' => (string) (array_values(array_filter($this->agentModel->allPosts(), static fn(array $post): bool => (int) $post['id_poste'] === (int) $postId))[0]['libelle'] ?? ''),
|
'poste_libelle' => (string) (array_values(array_filter($this->agentModel->allPosts(), static fn(array $post): bool => (int) $post['id_poste'] === (int) $postId))[0]['libelle'] ?? ''),
|
||||||
'type_contrat' => $contractType,
|
'type_contrat' => $contractType,
|
||||||
'formation_repartition_annuelle' => $trainingPlanned ? 1 : 0,
|
'formation_repartition_annuelle' => $trainingPlanned ? 1 : 0,
|
||||||
|
'droit_enveloppe_formation' => $trainingRight ? 1 : 0,
|
||||||
'commentaire_pta' => $ptaComment !== '' ? $ptaComment : null,
|
'commentaire_pta' => $ptaComment !== '' ? $ptaComment : null,
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
@@ -462,6 +467,9 @@ final class AgentController
|
|||||||
$postId = filter_var($payload['id_poste'] ?? null, FILTER_VALIDATE_INT);
|
$postId = filter_var($payload['id_poste'] ?? null, FILTER_VALIDATE_INT);
|
||||||
$contractType = strtoupper(trim((string) ($payload['type_contrat'] ?? 'PERMANENT')));
|
$contractType = strtoupper(trim((string) ($payload['type_contrat'] ?? 'PERMANENT')));
|
||||||
$trainingPlanned = filter_var($payload['formation_repartition_annuelle'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
$trainingPlanned = filter_var($payload['formation_repartition_annuelle'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||||
|
// Le droit à l'enveloppe de formation est acquis par défaut : un
|
||||||
|
// formulaire ancien ou une intégration tierce ne doit pas le retirer.
|
||||||
|
$trainingRight = filter_var($payload['droit_enveloppe_formation'] ?? true, FILTER_VALIDATE_BOOLEAN);
|
||||||
$ptaComment = trim((string) ($payload['commentaire_pta'] ?? ''));
|
$ptaComment = trim((string) ($payload['commentaire_pta'] ?? ''));
|
||||||
$validPostIds = array_map(static fn(array $post): int => (int) $post['id_poste'], $this->agentModel->allPosts());
|
$validPostIds = array_map(static fn(array $post): int => (int) $post['id_poste'], $this->agentModel->allPosts());
|
||||||
$rawStructure = $payload['structure_id'] ?? null;
|
$rawStructure = $payload['structure_id'] ?? null;
|
||||||
@@ -503,6 +511,7 @@ final class AgentController
|
|||||||
'id_poste' => (int) $postId,
|
'id_poste' => (int) $postId,
|
||||||
'type_contrat' => $contractType,
|
'type_contrat' => $contractType,
|
||||||
'formation_repartition_annuelle' => $trainingPlanned,
|
'formation_repartition_annuelle' => $trainingPlanned,
|
||||||
|
'droit_enveloppe_formation' => $trainingRight,
|
||||||
'commentaire_pta' => $ptaComment !== '' ? $ptaComment : null,
|
'commentaire_pta' => $ptaComment !== '' ? $ptaComment : null,
|
||||||
'structure_id' => $structureId !== null ? (int) $structureId : null,
|
'structure_id' => $structureId !== null ? (int) $structureId : null,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace App\Controllers;
|
|||||||
use App\Core\Access;
|
use App\Core\Access;
|
||||||
use App\Core\Csrf;
|
use App\Core\Csrf;
|
||||||
use App\Core\JsonResponse;
|
use App\Core\JsonResponse;
|
||||||
|
use App\Core\MotifCategories;
|
||||||
use App\Core\Request;
|
use App\Core\Request;
|
||||||
use App\Models\AgentModel;
|
use App\Models\AgentModel;
|
||||||
use App\Models\MotifModel;
|
use App\Models\MotifModel;
|
||||||
@@ -98,7 +99,8 @@ final class PlanningController
|
|||||||
if (!is_array($rawEntries)) {
|
if (!is_array($rawEntries)) {
|
||||||
JsonResponse::send(422, ['error' => 'La liste des affectations est invalide.']);
|
JsonResponse::send(422, ['error' => 'La liste des affectations est invalide.']);
|
||||||
}
|
}
|
||||||
if ($this->agentModel->findActive((int) $agentId) === null) {
|
$agent = $this->agentModel->findActive((int) $agentId);
|
||||||
|
if ($agent === null) {
|
||||||
JsonResponse::send(422, ['error' => 'Agent invalide ou inactif.']);
|
JsonResponse::send(422, ['error' => 'Agent invalide ou inactif.']);
|
||||||
}
|
}
|
||||||
if ($this->structureModel->findActive((int) $structureId) === null) {
|
if ($this->structureModel->findActive((int) $structureId) === null) {
|
||||||
@@ -107,7 +109,10 @@ final class PlanningController
|
|||||||
|
|
||||||
[$year, $weekNumber] = $week;
|
[$year, $weekNumber] = $week;
|
||||||
$weekStartDate = (new DateTimeImmutable())->setISODate($year, $weekNumber, 1)->format('Y-m-d');
|
$weekStartDate = (new DateTimeImmutable())->setISODate($year, $weekNumber, 1)->format('Y-m-d');
|
||||||
$entries = $this->normalizeEntries($rawEntries, $year, $weekNumber, $this->motifModel->activeIds());
|
// Les motifs autorisés dépendent de l'agent : « Formation » n'est pas
|
||||||
|
// affectable à un agent sans droit à l'enveloppe de formation.
|
||||||
|
$this->assertMotifsAllowedForAgent($rawEntries, $agent);
|
||||||
|
$entries = $this->normalizeEntries($rawEntries, $year, $weekNumber, $this->motifModel->activeIdsForAgent($agent));
|
||||||
$this->assertNoInternalOverlap($entries);
|
$this->assertNoInternalOverlap($entries);
|
||||||
$this->assertNoOtherLocationOverlap(
|
$this->assertNoOtherLocationOverlap(
|
||||||
$entries,
|
$entries,
|
||||||
@@ -169,7 +174,12 @@ final class PlanningController
|
|||||||
if ($this->structureModel->findActive((int) $structureId) === null) {
|
if ($this->structureModel->findActive((int) $structureId) === null) {
|
||||||
JsonResponse::send(422, ['error' => 'Lieu d’affectation invalide ou inactif.']);
|
JsonResponse::send(422, ['error' => 'Lieu d’affectation invalide ou inactif.']);
|
||||||
}
|
}
|
||||||
if (!in_array((int) $motifId, $this->motifModel->activeIds(), true)) {
|
$entryAgent = $this->agentModel->findActive((int) $entry['id_agent']);
|
||||||
|
if ($entryAgent === null) {
|
||||||
|
JsonResponse::send(422, ['error' => 'Agent invalide ou inactif.']);
|
||||||
|
}
|
||||||
|
$this->assertMotifsAllowedForAgent([['motif_id' => $motifId]], $entryAgent);
|
||||||
|
if (!in_array((int) $motifId, $this->motifModel->activeIdsForAgent($entryAgent), true)) {
|
||||||
JsonResponse::send(422, ['error' => 'Motif invalide ou inactif.']);
|
JsonResponse::send(422, ['error' => 'Motif invalide ou inactif.']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,6 +560,48 @@ final class PlanningController
|
|||||||
return [$year, $week];
|
return [$year, $week];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refuse les affectations portant un motif fermé à l'agent, avec un message
|
||||||
|
* qui nomme le motif : normalizeEntries() rejetterait bien la ligne, mais
|
||||||
|
* sans dire pourquoi. L'interface masque déjà ces motifs — reste les
|
||||||
|
* plannings saisis avant le retrait du droit, et les appels directs.
|
||||||
|
*
|
||||||
|
* @param array<int, mixed> $rawEntries
|
||||||
|
*/
|
||||||
|
private function assertMotifsAllowedForAgent(array $rawEntries, array $agent): void
|
||||||
|
{
|
||||||
|
if (AgentModel::hasTrainingRight($agent)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$restricted = [];
|
||||||
|
foreach ($this->motifModel->allActive() as $motif) {
|
||||||
|
if (MotifCategories::requiresTrainingRight((string) $motif['code'])) {
|
||||||
|
$restricted[(int) $motif['id_motif']] = (string) $motif['libelle'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($restricted === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rawEntries as $entry) {
|
||||||
|
if (!is_array($entry)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$motifId = (int) filter_var($entry['motif_id'] ?? 0, FILTER_VALIDATE_INT);
|
||||||
|
if (isset($restricted[$motifId])) {
|
||||||
|
JsonResponse::send(422, [
|
||||||
|
'error' => sprintf(
|
||||||
|
'Le motif « %s » ne peut pas être affecté à %s %s : cet agent n’a pas droit à l’enveloppe de formation. Retirez ou remplacez ces plages avant d’enregistrer.',
|
||||||
|
$restricted[$motifId],
|
||||||
|
(string) ($agent['prenom'] ?? ''),
|
||||||
|
(string) ($agent['nom'] ?? '')
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function normalizeEntries(array $entries, int $year, int $weekNumber, array $validMotifs): array
|
private function normalizeEntries(array $entries, int $year, int $weekNumber, array $validMotifs): array
|
||||||
{
|
{
|
||||||
$weekStart = (new DateTimeImmutable())->setISODate($year, $weekNumber, 1)->setTime(0, 0);
|
$weekStart = (new DateTimeImmutable())->setISODate($year, $weekNumber, 1)->setTime(0, 0);
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ final class MotifCategories
|
|||||||
return strtoupper(trim((string) $code));
|
return strtoupper(trim((string) $code));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Motif dont l'affectation dépend d'un droit porté par l'agent : la formation
|
||||||
|
* n'est ouverte qu'aux agents bénéficiant de l'enveloppe annuelle de 14 h
|
||||||
|
* (colonne `droit_enveloppe_formation`, voir AgentModel::hasTrainingRight()).
|
||||||
|
*/
|
||||||
|
public static function requiresTrainingRight(?string $code): bool
|
||||||
|
{
|
||||||
|
return self::normalizeCode($code) === self::FORMATION;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Un motif inconnu du référentiel est rattaché au temps de travail dès lors qu'il
|
* Un motif inconnu du référentiel est rattaché au temps de travail dès lors qu'il
|
||||||
* décompte le quota : c'est le comportement le moins surprenant pour un motif ajouté
|
* décompte le quota : c'est le comportement le moins surprenant pour un motif ajouté
|
||||||
|
|||||||
@@ -10,11 +10,40 @@ use Throwable;
|
|||||||
|
|
||||||
final class AgentModel extends BaseModel
|
final class AgentModel extends BaseModel
|
||||||
{
|
{
|
||||||
|
/** Enveloppe annuelle complémentaire de formation : 14 h. */
|
||||||
|
public const ENVELOPPE_FORMATION_MINUTES = 840;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Un agent sans droit à l'enveloppe de formation ne peut pas se voir
|
||||||
|
* affecter le motif « Formation ». Le droit vaut par défaut lorsque
|
||||||
|
* l'information n'est pas projetée : ne jamais retirer un droit sur une
|
||||||
|
* donnée absente.
|
||||||
|
*/
|
||||||
|
public static function hasTrainingRight(array $agent): bool
|
||||||
|
{
|
||||||
|
return (int) ($agent['droit_enveloppe_formation'] ?? 1) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minutes de formation ouvertes à l'agent sur l'année : rien s'il n'y a pas
|
||||||
|
* droit, rien non plus si sa formation est déjà comptée dans la répartition
|
||||||
|
* annuelle, l'enveloppe complémentaire sinon.
|
||||||
|
*/
|
||||||
|
public static function trainingEnvelopeMinutes(array $agent): int
|
||||||
|
{
|
||||||
|
if (!self::hasTrainingRight($agent) || !empty($agent['formation_repartition_annuelle'])) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::ENVELOPPE_FORMATION_MINUTES;
|
||||||
|
}
|
||||||
|
|
||||||
private const AGENT_SELECT = <<<'SQL'
|
private const AGENT_SELECT = <<<'SQL'
|
||||||
SELECT a.id_agent, a.matricule, a.nom, a.prenom, a.email,
|
SELECT a.id_agent, a.matricule, a.nom, a.prenom, a.email,
|
||||||
a.telephone, a.adresse, a.est_diplome, a.diplome_libelle,
|
a.telephone, a.adresse, a.est_diplome, a.diplome_libelle,
|
||||||
a.date_debut_contrat, a.date_fin_contrat, a.type_contrat,
|
a.date_debut_contrat, a.date_fin_contrat, a.type_contrat,
|
||||||
a.formation_repartition_annuelle, a.commentaire_pta,
|
a.formation_repartition_annuelle, a.droit_enveloppe_formation,
|
||||||
|
a.commentaire_pta,
|
||||||
a.id_poste, po.code AS poste_code, po.libelle AS poste_libelle,
|
a.id_poste, po.code AS poste_code, po.libelle AS poste_libelle,
|
||||||
po.famille AS poste_famille, po.heures_mercredi_minutes,
|
po.famille AS poste_famille, po.heures_mercredi_minutes,
|
||||||
po.heures_extrascolaire_minutes, po.autorise_preparation,
|
po.heures_extrascolaire_minutes, po.autorise_preparation,
|
||||||
@@ -129,7 +158,10 @@ final class AgentModel extends BaseModel
|
|||||||
$periodEnd = new DateTimeImmutable(sprintf('%04d-12-31', $year));
|
$periodEnd = new DateTimeImmutable(sprintf('%04d-12-31', $year));
|
||||||
$quotaReference = QuotaModel::REFERENCE_MINUTES;
|
$quotaReference = QuotaModel::REFERENCE_MINUTES;
|
||||||
$quotaTarget = (int) round($quotaReference * ((float) $data['quotite_travail'] / 100));
|
$quotaTarget = (int) round($quotaReference * ((float) $data['quotite_travail'] / 100));
|
||||||
$trainingEnvelope = !empty($data['formation_repartition_annuelle']) ? 0 : 840;
|
$trainingEnvelope = self::trainingEnvelopeMinutes([
|
||||||
|
'droit_enveloppe_formation' => !empty($data['droit_enveloppe_formation']) ? 1 : 0,
|
||||||
|
'formation_repartition_annuelle' => $data['formation_repartition_annuelle'],
|
||||||
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->pdo->beginTransaction();
|
$this->pdo->beginTransaction();
|
||||||
@@ -139,14 +171,14 @@ final class AgentModel extends BaseModel
|
|||||||
matricule, nom, prenom, email, telephone, adresse,
|
matricule, nom, prenom, email, telephone, adresse,
|
||||||
est_diplome, diplome_libelle, date_debut_contrat,
|
est_diplome, diplome_libelle, date_debut_contrat,
|
||||||
id_poste, type_contrat, date_fin_contrat,
|
id_poste, type_contrat, date_fin_contrat,
|
||||||
formation_repartition_annuelle, commentaire_pta,
|
formation_repartition_annuelle, droit_enveloppe_formation,
|
||||||
id_structure, actif
|
commentaire_pta, id_structure, actif
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:matricule, :nom, :prenom, :email, :telephone, :adresse,
|
:matricule, :nom, :prenom, :email, :telephone, :adresse,
|
||||||
:est_diplome, :diplome_libelle, :date_debut_contrat,
|
:est_diplome, :diplome_libelle, :date_debut_contrat,
|
||||||
:id_poste, :type_contrat, :date_fin_contrat,
|
:id_poste, :type_contrat, :date_fin_contrat,
|
||||||
:formation_repartition_annuelle, :commentaire_pta,
|
:formation_repartition_annuelle, :droit_enveloppe_formation,
|
||||||
:structure_id, TRUE
|
:commentaire_pta, :structure_id, TRUE
|
||||||
)'
|
)'
|
||||||
);
|
);
|
||||||
$insertAgent->execute([
|
$insertAgent->execute([
|
||||||
@@ -163,6 +195,7 @@ final class AgentModel extends BaseModel
|
|||||||
'type_contrat' => $data['type_contrat'],
|
'type_contrat' => $data['type_contrat'],
|
||||||
'date_fin_contrat' => $contractEnd?->format('Y-m-d'),
|
'date_fin_contrat' => $contractEnd?->format('Y-m-d'),
|
||||||
'formation_repartition_annuelle' => $data['formation_repartition_annuelle'] ? 1 : 0,
|
'formation_repartition_annuelle' => $data['formation_repartition_annuelle'] ? 1 : 0,
|
||||||
|
'droit_enveloppe_formation' => !empty($data['droit_enveloppe_formation']) ? 1 : 0,
|
||||||
'commentaire_pta' => $data['commentaire_pta'],
|
'commentaire_pta' => $data['commentaire_pta'],
|
||||||
'structure_id' => $data['structure_id'],
|
'structure_id' => $data['structure_id'],
|
||||||
]);
|
]);
|
||||||
@@ -261,6 +294,7 @@ final class AgentModel extends BaseModel
|
|||||||
type_contrat = :type_contrat,
|
type_contrat = :type_contrat,
|
||||||
date_fin_contrat = :date_fin_contrat,
|
date_fin_contrat = :date_fin_contrat,
|
||||||
formation_repartition_annuelle = :formation_repartition_annuelle,
|
formation_repartition_annuelle = :formation_repartition_annuelle,
|
||||||
|
droit_enveloppe_formation = :droit_enveloppe_formation,
|
||||||
commentaire_pta = :commentaire_pta,
|
commentaire_pta = :commentaire_pta,
|
||||||
id_structure = :structure_id
|
id_structure = :structure_id
|
||||||
WHERE id_agent = :agent_id'
|
WHERE id_agent = :agent_id'
|
||||||
@@ -276,6 +310,7 @@ final class AgentModel extends BaseModel
|
|||||||
$update->bindValue(':type_contrat', $data['type_contrat']);
|
$update->bindValue(':type_contrat', $data['type_contrat']);
|
||||||
$update->bindValue(':date_fin_contrat', $data['date_fin_contrat']);
|
$update->bindValue(':date_fin_contrat', $data['date_fin_contrat']);
|
||||||
$update->bindValue(':formation_repartition_annuelle', $data['formation_repartition_annuelle'] ? 1 : 0, PDO::PARAM_INT);
|
$update->bindValue(':formation_repartition_annuelle', $data['formation_repartition_annuelle'] ? 1 : 0, PDO::PARAM_INT);
|
||||||
|
$update->bindValue(':droit_enveloppe_formation', !empty($data['droit_enveloppe_formation']) ? 1 : 0, PDO::PARAM_INT);
|
||||||
$update->bindValue(':commentaire_pta', $data['commentaire_pta']);
|
$update->bindValue(':commentaire_pta', $data['commentaire_pta']);
|
||||||
if ($newStructureId === null) {
|
if ($newStructureId === null) {
|
||||||
$update->bindValue(':structure_id', null, PDO::PARAM_NULL);
|
$update->bindValue(':structure_id', null, PDO::PARAM_NULL);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Core\MotifCategories;
|
||||||
|
|
||||||
final class MotifModel extends BaseModel
|
final class MotifModel extends BaseModel
|
||||||
{
|
{
|
||||||
public function allActive(): array
|
public function allActive(): array
|
||||||
@@ -23,4 +25,29 @@ final class MotifModel extends BaseModel
|
|||||||
$this->pdo->query('SELECT id_motif FROM motif_planning WHERE actif = TRUE')->fetchAll(\PDO::FETCH_COLUMN)
|
$this->pdo->query('SELECT id_motif FROM motif_planning WHERE actif = TRUE')->fetchAll(\PDO::FETCH_COLUMN)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Motifs réellement affectables à un agent : « Formation » disparaît de la
|
||||||
|
* liste dès lors que l'agent n'a pas droit à l'enveloppe de formation.
|
||||||
|
*/
|
||||||
|
public function allActiveForAgent(array $agent): array
|
||||||
|
{
|
||||||
|
if (AgentModel::hasTrainingRight($agent)) {
|
||||||
|
return $this->allActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_filter(
|
||||||
|
$this->allActive(),
|
||||||
|
static fn(array $motif): bool => !MotifCategories::requiresTrainingRight((string) $motif['code'])
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return int[] */
|
||||||
|
public function activeIdsForAgent(array $agent): array
|
||||||
|
{
|
||||||
|
return array_map(
|
||||||
|
static fn(array $motif): int => (int) $motif['id_motif'],
|
||||||
|
$this->allActiveForAgent($agent)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ final class PtaModel extends BaseModel
|
|||||||
return $pta;
|
return $pta;
|
||||||
}
|
}
|
||||||
|
|
||||||
$training = !empty($agent['formation_repartition_annuelle']) ? 0 : 840;
|
$training = AgentModel::trainingEnvelopeMinutes($agent);
|
||||||
$insert = $this->pdo->prepare(
|
$insert = $this->pdo->prepare(
|
||||||
"INSERT INTO pta_annuel (
|
"INSERT INTO pta_annuel (
|
||||||
id_agent, date_debut, date_fin, quotite_travail,
|
id_agent, date_debut, date_fin, quotite_travail,
|
||||||
@@ -378,7 +378,13 @@ final class PtaModel extends BaseModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
$formationMinutes = $categories['FORMATION'];
|
$formationMinutes = $categories['FORMATION'];
|
||||||
$formationTarget = !empty($agent['formation_repartition_annuelle']) ? 0 : 840;
|
$formationTarget = AgentModel::trainingEnvelopeMinutes($agent);
|
||||||
|
if (!AgentModel::hasTrainingRight($agent) && $formationMinutes > 0) {
|
||||||
|
$controls[] = $this->control('ALERTE', 'FORMATION_SANS_DROIT', sprintf(
|
||||||
|
'Formation : %s planifiées alors que l’agent n’a pas droit à l’enveloppe de formation. Ces plages sont à retirer.',
|
||||||
|
$this->formatMinutes($formationMinutes)
|
||||||
|
));
|
||||||
|
}
|
||||||
if ($formationTarget > 0 && $formationMinutes > $formationTarget) {
|
if ($formationTarget > 0 && $formationMinutes > $formationTarget) {
|
||||||
$controls[] = $this->control('ALERTE', 'FORMATION_DEPASSEE', sprintf(
|
$controls[] = $this->control('ALERTE', 'FORMATION_DEPASSEE', sprintf(
|
||||||
'Formation : %s planifiées pour une enveloppe complémentaire de 14 h.',
|
'Formation : %s planifiées pour une enveloppe complémentaire de 14 h.',
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ final class SchemaModel extends BaseModel
|
|||||||
$requiredColumns = [
|
$requiredColumns = [
|
||||||
'creneau_horaire' => ['id_motif'],
|
'creneau_horaire' => ['id_motif'],
|
||||||
'planning' => ['date_validation'],
|
'planning' => ['date_validation'],
|
||||||
'agent' => ['email', 'telephone', 'adresse', 'est_diplome', 'diplome_libelle', 'id_structure', 'id_poste', 'type_contrat', 'date_debut_contrat', 'date_fin_contrat', 'formation_repartition_annuelle', 'commentaire_pta'],
|
'agent' => ['email', 'telephone', 'adresse', 'est_diplome', 'diplome_libelle', 'id_structure', 'id_poste', 'type_contrat', 'date_debut_contrat', 'date_fin_contrat', 'formation_repartition_annuelle', 'droit_enveloppe_formation', 'commentaire_pta'],
|
||||||
'structure' => ['type_affectation'],
|
'structure' => ['type_affectation'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ window.PTA_PAGE_CONFIG = <?= json_encode([
|
|||||||
'params' => $pageConfig,
|
'params' => $pageConfig,
|
||||||
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE) ?>;
|
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE) ?>;
|
||||||
</script>
|
</script>
|
||||||
<script src="assets/js/core.js?v=7.11"></script>
|
<script src="assets/js/core.js?v=7.12"></script>
|
||||||
<?php foreach ($scripts as $script): ?>
|
<?php foreach ($scripts as $script): ?>
|
||||||
<script src="assets/js/<?= htmlspecialchars($script) ?>?v=7.11"></script>
|
<script src="assets/js/<?= htmlspecialchars($script) ?>?v=7.12"></script>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<script src="assets/js/page-bootstrap.js?v=7.11"></script>
|
<script src="assets/js/page-bootstrap.js?v=7.12"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ $showGroupLabels = $visiblePageCount > 4;
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title><?= htmlspecialchars($pageTitle) ?> — PTA</title>
|
<title><?= htmlspecialchars($pageTitle) ?> — PTA</title>
|
||||||
<link rel="stylesheet" href="assets/style.css?v=7.11">
|
<link rel="stylesheet" href="assets/style.css?v=7.12">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="app-shell">
|
<main class="app-shell">
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Choisir votre accès — PTA</title>
|
<title>Choisir votre accès — PTA</title>
|
||||||
<link rel="stylesheet" href="assets/style.css?v=7.11">
|
<link rel="stylesheet" href="assets/style.css?v=7.12">
|
||||||
</head>
|
</head>
|
||||||
<body class="role-page-body">
|
<body class="role-page-body">
|
||||||
<main class="role-page-shell">
|
<main class="role-page-shell">
|
||||||
|
|||||||
@@ -122,6 +122,10 @@
|
|||||||
<span>Date de fin du contrat</span>
|
<span>Date de fin du contrat</span>
|
||||||
<input id="new-agent-contract-end" name="date_fin_contrat" type="date">
|
<input id="new-agent-contract-end" name="date_fin_contrat" type="date">
|
||||||
</label>
|
</label>
|
||||||
|
<label class="check-field admin-field-wide">
|
||||||
|
<input id="new-agent-training-right" type="checkbox" checked>
|
||||||
|
<span>Droit à l’enveloppe de formation de 14 h. Décoché, le motif « Formation » ne pourra pas être affecté à cet agent lors de la saisie des horaires.</span>
|
||||||
|
</label>
|
||||||
<label class="check-field admin-field-wide">
|
<label class="check-field admin-field-wide">
|
||||||
<input id="new-agent-training-planned" type="checkbox">
|
<input id="new-agent-training-planned" type="checkbox">
|
||||||
<span>Une formation est déjà prévue dans la répartition annuelle : ne pas ajouter l’enveloppe complémentaire de 14 h</span>
|
<span>Une formation est déjà prévue dans la répartition annuelle : ne pas ajouter l’enveloppe complémentaire de 14 h</span>
|
||||||
@@ -243,6 +247,7 @@
|
|||||||
data-id-poste="<?= (int) ($agent['id_poste'] ?? 0) ?>"
|
data-id-poste="<?= (int) ($agent['id_poste'] ?? 0) ?>"
|
||||||
data-type-contrat="<?= htmlspecialchars((string) ($agent['type_contrat'] ?? 'PERMANENT'), ENT_QUOTES) ?>"
|
data-type-contrat="<?= htmlspecialchars((string) ($agent['type_contrat'] ?? 'PERMANENT'), ENT_QUOTES) ?>"
|
||||||
data-formation-repartition-annuelle="<?= !empty($agent['formation_repartition_annuelle']) ? '1' : '0' ?>"
|
data-formation-repartition-annuelle="<?= !empty($agent['formation_repartition_annuelle']) ? '1' : '0' ?>"
|
||||||
|
data-droit-enveloppe-formation="<?= !isset($agent['droit_enveloppe_formation']) || !empty($agent['droit_enveloppe_formation']) ? '1' : '0' ?>"
|
||||||
data-commentaire-pta="<?= htmlspecialchars((string) ($agent['commentaire_pta'] ?? ''), ENT_QUOTES) ?>"
|
data-commentaire-pta="<?= htmlspecialchars((string) ($agent['commentaire_pta'] ?? ''), ENT_QUOTES) ?>"
|
||||||
data-structure-id="<?= (int) ($agent['id_structure'] ?? 0) ?>"
|
data-structure-id="<?= (int) ($agent['id_structure'] ?? 0) ?>"
|
||||||
>
|
>
|
||||||
@@ -325,6 +330,10 @@
|
|||||||
<span>Date de fin du contrat</span>
|
<span>Date de fin du contrat</span>
|
||||||
<input id="edit-agent-profile-contract-end" type="date">
|
<input id="edit-agent-profile-contract-end" type="date">
|
||||||
</label>
|
</label>
|
||||||
|
<label class="check-field admin-field-wide">
|
||||||
|
<input id="edit-agent-profile-training-right" type="checkbox">
|
||||||
|
<span>Droit à l’enveloppe de formation de 14 h</span>
|
||||||
|
</label>
|
||||||
<label class="check-field admin-field-wide">
|
<label class="check-field admin-field-wide">
|
||||||
<input id="edit-agent-profile-training-planned" type="checkbox">
|
<input id="edit-agent-profile-training-planned" type="checkbox">
|
||||||
<span>Formation déjà intégrée dans la répartition annuelle</span>
|
<span>Formation déjà intégrée dans la répartition annuelle</span>
|
||||||
|
|||||||
@@ -212,6 +212,7 @@
|
|||||||
row.dataset.idPoste = agent.id_poste ? String(Number(agent.id_poste)) : '';
|
row.dataset.idPoste = agent.id_poste ? String(Number(agent.id_poste)) : '';
|
||||||
row.dataset.typeContrat = agent.type_contrat || 'PERMANENT';
|
row.dataset.typeContrat = agent.type_contrat || 'PERMANENT';
|
||||||
row.dataset.formationRepartitionAnnuelle = Number(agent.formation_repartition_annuelle) ? '1' : '0';
|
row.dataset.formationRepartitionAnnuelle = Number(agent.formation_repartition_annuelle) ? '1' : '0';
|
||||||
|
row.dataset.droitEnveloppeFormation = Number(agent.droit_enveloppe_formation ?? 1) ? '1' : '0';
|
||||||
row.dataset.commentairePta = agent.commentaire_pta || '';
|
row.dataset.commentairePta = agent.commentaire_pta || '';
|
||||||
row.dataset.structureId = agent.id_structure ? String(Number(agent.id_structure)) : '';
|
row.dataset.structureId = agent.id_structure ? String(Number(agent.id_structure)) : '';
|
||||||
}
|
}
|
||||||
@@ -256,6 +257,7 @@
|
|||||||
document.querySelector('#edit-agent-profile-post').value = row.dataset.idPoste || '';
|
document.querySelector('#edit-agent-profile-post').value = row.dataset.idPoste || '';
|
||||||
document.querySelector('#edit-agent-profile-contract-type').value = row.dataset.typeContrat || 'PERMANENT';
|
document.querySelector('#edit-agent-profile-contract-type').value = row.dataset.typeContrat || 'PERMANENT';
|
||||||
document.querySelector('#edit-agent-profile-training-planned').checked = row.dataset.formationRepartitionAnnuelle === '1';
|
document.querySelector('#edit-agent-profile-training-planned').checked = row.dataset.formationRepartitionAnnuelle === '1';
|
||||||
|
document.querySelector('#edit-agent-profile-training-right').checked = row.dataset.droitEnveloppeFormation !== '0';
|
||||||
document.querySelector('#edit-agent-profile-pta-comment').value = row.dataset.commentairePta || '';
|
document.querySelector('#edit-agent-profile-pta-comment').value = row.dataset.commentairePta || '';
|
||||||
document.querySelector('#edit-agent-profile-structure').value = row.dataset.structureId || '';
|
document.querySelector('#edit-agent-profile-structure').value = row.dataset.structureId || '';
|
||||||
toggleDiplomaField(document.querySelector('#edit-agent-profile-qualified'), document.querySelector('#edit-agent-profile-diploma'));
|
toggleDiplomaField(document.querySelector('#edit-agent-profile-qualified'), document.querySelector('#edit-agent-profile-diploma'));
|
||||||
@@ -303,6 +305,7 @@
|
|||||||
id_poste: Number(document.querySelector('#edit-agent-profile-post').value),
|
id_poste: Number(document.querySelector('#edit-agent-profile-post').value),
|
||||||
type_contrat: document.querySelector('#edit-agent-profile-contract-type').value,
|
type_contrat: document.querySelector('#edit-agent-profile-contract-type').value,
|
||||||
formation_repartition_annuelle: document.querySelector('#edit-agent-profile-training-planned').checked,
|
formation_repartition_annuelle: document.querySelector('#edit-agent-profile-training-planned').checked,
|
||||||
|
droit_enveloppe_formation: document.querySelector('#edit-agent-profile-training-right').checked,
|
||||||
commentaire_pta: document.querySelector('#edit-agent-profile-pta-comment').value,
|
commentaire_pta: document.querySelector('#edit-agent-profile-pta-comment').value,
|
||||||
structure_id: structureValue ? Number(structureValue) : null,
|
structure_id: structureValue ? Number(structureValue) : null,
|
||||||
}),
|
}),
|
||||||
@@ -408,6 +411,7 @@
|
|||||||
id_poste: Number(document.querySelector('#new-agent-post').value),
|
id_poste: Number(document.querySelector('#new-agent-post').value),
|
||||||
type_contrat: document.querySelector('#new-agent-contract-type').value,
|
type_contrat: document.querySelector('#new-agent-contract-type').value,
|
||||||
formation_repartition_annuelle: document.querySelector('#new-agent-training-planned').checked,
|
formation_repartition_annuelle: document.querySelector('#new-agent-training-planned').checked,
|
||||||
|
droit_enveloppe_formation: document.querySelector('#new-agent-training-right').checked,
|
||||||
commentaire_pta: document.querySelector('#new-agent-pta-comment').value,
|
commentaire_pta: document.querySelector('#new-agent-pta-comment').value,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -254,6 +254,28 @@
|
|||||||
setMessage(message);
|
setMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Le sélecteur de motif est rendu côté serveur avec tous les motifs actifs :
|
||||||
|
* on le reconstruit à l'ouverture pour retirer « Formation » aux agents sans
|
||||||
|
* droit à l'enveloppe de formation. Le motif déjà porté par le créneau reste
|
||||||
|
* proposé, sans quoi le modal changerait silencieusement de motif.
|
||||||
|
*/
|
||||||
|
function renderMotifOptions(selectedMotifId) {
|
||||||
|
const select = document.querySelector('#edit-agent-entry-motif');
|
||||||
|
const motifs = window.PTA_CONFIG?.motifs || [];
|
||||||
|
if (!select || motifs.length === 0) return;
|
||||||
|
|
||||||
|
const agent = shared.lastAgentOverviewData?.agent;
|
||||||
|
const hasTrainingRight = Number(agent?.droit_enveloppe_formation ?? 1) !== 0;
|
||||||
|
|
||||||
|
select.innerHTML = motifs
|
||||||
|
.filter((motif) => hasTrainingRight
|
||||||
|
|| String(motif.code).toUpperCase() !== 'FORMATION'
|
||||||
|
|| Number(motif.id) === Number(selectedMotifId))
|
||||||
|
.map((motif) => `<option value="${Number(motif.id)}">${escapeHtml(motif.label)}</option>`)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
function openModal(entryId) {
|
function openModal(entryId) {
|
||||||
const entry = findEntry(entryId);
|
const entry = findEntry(entryId);
|
||||||
if (!entry || !canEditEntry(entry)) return;
|
if (!entry || !canEditEntry(entry)) return;
|
||||||
@@ -263,6 +285,7 @@
|
|||||||
document.querySelector('#edit-agent-entry-id').value = String(Number(entry.id_creneau));
|
document.querySelector('#edit-agent-entry-id').value = String(Number(entry.id_creneau));
|
||||||
document.querySelector('#edit-agent-entry-date').value = entry.date_jour;
|
document.querySelector('#edit-agent-entry-date').value = entry.date_jour;
|
||||||
document.querySelector('#edit-agent-entry-structure').value = String(Number(entry.id_structure));
|
document.querySelector('#edit-agent-entry-structure').value = String(Number(entry.id_structure));
|
||||||
|
renderMotifOptions(Number(entry.id_motif));
|
||||||
document.querySelector('#edit-agent-entry-motif').value = String(Number(entry.id_motif));
|
document.querySelector('#edit-agent-entry-motif').value = String(Number(entry.id_motif));
|
||||||
document.querySelector('#edit-agent-entry-start').value = entry.heure_debut;
|
document.querySelector('#edit-agent-entry-start').value = entry.heure_debut;
|
||||||
document.querySelector('#edit-agent-entry-end').value = entry.heure_fin;
|
document.querySelector('#edit-agent-entry-end').value = entry.heure_fin;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
entries: [],
|
entries: [],
|
||||||
otherStructureEntries: [],
|
otherStructureEntries: [],
|
||||||
currentStructure: null,
|
currentStructure: null,
|
||||||
|
currentAgent: null,
|
||||||
planningId: null,
|
planningId: null,
|
||||||
planningStatus: null,
|
planningStatus: null,
|
||||||
weekDays: [],
|
weekDays: [],
|
||||||
|
|||||||
@@ -123,8 +123,24 @@
|
|||||||
return motif ? Number(motif.id) : null;
|
return motif ? Number(motif.id) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Un agent sans droit à l'enveloppe de formation ne peut pas recevoir le motif
|
||||||
|
* « Formation ». Le droit vaut tant que l'agent n'est pas chargé : on ne retire
|
||||||
|
* jamais une possibilité sur une donnée absente.
|
||||||
|
*/
|
||||||
|
function agentHasTrainingRight() {
|
||||||
|
return Number(state.currentAgent?.droit_enveloppe_formation ?? 1) !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMotifAllowedForAgent(motif) {
|
||||||
|
return agentHasTrainingRight() || String(motif.code).toUpperCase() !== 'FORMATION';
|
||||||
|
}
|
||||||
|
|
||||||
function motifOptionsHtml(selectedMotifId = null) {
|
function motifOptionsHtml(selectedMotifId = null) {
|
||||||
return (window.PTA_CONFIG?.motifs || [])
|
return (window.PTA_CONFIG?.motifs || [])
|
||||||
|
// Un motif interdit reste proposé s'il est déjà celui de la plage :
|
||||||
|
// le retirer changerait silencieusement le motif d'une saisie existante.
|
||||||
|
.filter((motif) => isMotifAllowedForAgent(motif) || Number(motif.id) === Number(selectedMotifId))
|
||||||
.map((motif) => {
|
.map((motif) => {
|
||||||
const selected = Number(motif.id) === Number(selectedMotifId) ? ' selected' : '';
|
const selected = Number(motif.id) === Number(selectedMotifId) ? ' selected' : '';
|
||||||
const quotaLabel = motif.quota ? ' · décompte quota' : ' · hors quota';
|
const quotaLabel = motif.quota ? ' · décompte quota' : ' · hors quota';
|
||||||
@@ -665,6 +681,7 @@
|
|||||||
state.entries = [];
|
state.entries = [];
|
||||||
state.otherStructureEntries = [];
|
state.otherStructureEntries = [];
|
||||||
state.currentStructure = null;
|
state.currentStructure = null;
|
||||||
|
state.currentAgent = null;
|
||||||
state.planningId = null;
|
state.planningId = null;
|
||||||
state.quota = null;
|
state.quota = null;
|
||||||
state.savedCurrentCountedMinutes = 0;
|
state.savedCurrentCountedMinutes = 0;
|
||||||
@@ -726,6 +743,7 @@
|
|||||||
state.entries = [];
|
state.entries = [];
|
||||||
state.otherStructureEntries = [];
|
state.otherStructureEntries = [];
|
||||||
state.currentStructure = null;
|
state.currentStructure = null;
|
||||||
|
state.currentAgent = null;
|
||||||
state.planningId = null;
|
state.planningId = null;
|
||||||
state.savedCurrentCountedMinutes = 0;
|
state.savedCurrentCountedMinutes = 0;
|
||||||
setStatus(null);
|
setStatus(null);
|
||||||
@@ -744,6 +762,7 @@
|
|||||||
|
|
||||||
state.quota = data.quota || null;
|
state.quota = data.quota || null;
|
||||||
state.currentStructure = data.current_structure || null;
|
state.currentStructure = data.current_structure || null;
|
||||||
|
state.currentAgent = data.agent || null;
|
||||||
const calendarByDate = new Map((data.calendar_days || []).map((day) => [day.date, day]));
|
const calendarByDate = new Map((data.calendar_days || []).map((day) => [day.date, day]));
|
||||||
state.weekDays = state.weekDays.map((day) => ({ ...day, ...(calendarByDate.get(day.date) || {}) }));
|
state.weekDays = state.weekDays.map((day) => ({ ...day, ...(calendarByDate.get(day.date) || {}) }));
|
||||||
state.otherStructureEntries = (data.other_entries || []).map((entry) => ({
|
state.otherStructureEntries = (data.other_entries || []).map((entry) => ({
|
||||||
@@ -835,6 +854,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isMotifAllowedForAgent(motif)) {
|
||||||
|
errors.push(`${day?.label || row.dataset.date} ${label} : le motif « ${motif.label} » n’est pas ouvert à cet agent, qui n’a pas droit à l’enveloppe de formation.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!start || !end) {
|
if (!start || !end) {
|
||||||
errors.push(`${day?.label || row.dataset.date} ${label} : renseignez l’heure de début et l’heure de fin.`);
|
errors.push(`${day?.label || row.dataset.date} ${label} : renseignez l’heure de début et l’heure de fin.`);
|
||||||
return;
|
return;
|
||||||
|
|||||||
25
database/migration_v7_9b_droit_enveloppe_formation.sql
Normal file
25
database/migration_v7_9b_droit_enveloppe_formation.sql
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- PTA — Droit à l'enveloppe annuelle de formation (14 h)
|
||||||
|
-- ============================================================================
|
||||||
|
-- Tous les agents ne bénéficient pas de l'enveloppe complémentaire de 14 h de
|
||||||
|
-- formation. Cette colonne porte ce droit, coché par défaut à la création d'un
|
||||||
|
-- agent. Un agent sans ce droit ne peut plus se voir affecter le motif
|
||||||
|
-- « Formation » lors de la saisie des horaires, et son enveloppe de formation
|
||||||
|
-- au PTA annuel vaut 0.
|
||||||
|
--
|
||||||
|
-- À ne pas confondre avec `formation_repartition_annuelle`, déjà présente :
|
||||||
|
-- celle-ci signale que la formation est DÉJÀ comptée dans la répartition
|
||||||
|
-- annuelle de l'agent — l'agent y a droit, l'enveloppe complémentaire n'est
|
||||||
|
-- simplement pas ajoutée par-dessus.
|
||||||
|
--
|
||||||
|
-- Nommage : les migrations sont jouées dans l'ordre alphabétique. Le suffixe
|
||||||
|
-- « 9b » place ce fichier après migration_v7_9_tableau_de_bord.sql, ce qu'un
|
||||||
|
-- « v7_10 » ou « v7_11 » n'aurait pas fait ('1' trie avant '8' et '9').
|
||||||
|
--
|
||||||
|
-- Idempotente : rejouable sans effet de bord.
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
ALTER TABLE agent
|
||||||
|
ADD COLUMN IF NOT EXISTS droit_enveloppe_formation BOOLEAN NOT NULL DEFAULT TRUE
|
||||||
|
COMMENT 'Droit à l''enveloppe annuelle complémentaire de 14 h de formation'
|
||||||
|
AFTER formation_repartition_annuelle;
|
||||||
Reference in New Issue
Block a user