V7.10 — Tableau de bord, guide, semaine type et enveloppe de formation
Reprise dans le répertoire de production du travail mené dans le worktree ux-service-enfance : - tableau de bord Service Enfance (dashboard.php) et navigation regroupée ; - guide d'utilisation (faq.php) accessible depuis toute la navigation ; - éditeur de semaine type mis en avant sur l'écran de saisie ; - droit à l'enveloppe de formation par agent, en remplacement de la case « formation déjà prévue dans la répartition annuelle » ; - correctif de « [object Object] » sur les cartes de quota du PTA ; - motif de travail par défaut à la saisie, heures travaillées complètes dans les PDF. Le docker-compose.yml de production est conservé : celui du worktree décrit une pile de test (conteneurs et volume suffixés « test », ports 9080/9081), qui aurait fait redémarrer l'application sur un volume MariaDB vide. Les migrations database/migration_v7_9*.sql sont obligatoires ; le service db-migrate les applique au déploiement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ namespace App\Controllers;
|
||||
use App\Core\Access;
|
||||
use App\Core\Csrf;
|
||||
use App\Core\JsonResponse;
|
||||
use App\Core\MotifCategories;
|
||||
use App\Core\Request;
|
||||
use App\Models\AgentModel;
|
||||
use App\Models\MotifModel;
|
||||
@@ -98,7 +99,8 @@ final class PlanningController
|
||||
if (!is_array($rawEntries)) {
|
||||
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.']);
|
||||
}
|
||||
if ($this->structureModel->findActive((int) $structureId) === null) {
|
||||
@@ -107,7 +109,10 @@ final class PlanningController
|
||||
|
||||
[$year, $weekNumber] = $week;
|
||||
$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->assertNoOtherLocationOverlap(
|
||||
$entries,
|
||||
@@ -169,7 +174,12 @@ final class PlanningController
|
||||
if ($this->structureModel->findActive((int) $structureId) === null) {
|
||||
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.']);
|
||||
}
|
||||
|
||||
@@ -550,6 +560,48 @@ final class PlanningController
|
||||
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
|
||||
{
|
||||
$weekStart = (new DateTimeImmutable())->setISODate($year, $weekNumber, 1)->setTime(0, 0);
|
||||
|
||||
Reference in New Issue
Block a user