update
This commit is contained in:
@@ -6,6 +6,7 @@ namespace App\Controllers;
|
||||
|
||||
use App\Core\Access;
|
||||
use App\Core\Csrf;
|
||||
use App\Core\Journal;
|
||||
use App\Core\JsonResponse;
|
||||
use App\Core\MotifCategories;
|
||||
use App\Core\Request;
|
||||
@@ -164,6 +165,7 @@ final class PlanningController
|
||||
// affectable à un agent sans droit à l'enveloppe de formation.
|
||||
$this->assertMotifsAllowedForAgent($rawEntries, $agent);
|
||||
$entries = $this->normalizeEntries($rawEntries, $year, $weekNumber, $this->motifModel->activeIdsForAgent($agent));
|
||||
$this->assertDatesWithinContract($agent, array_column($entries, 'date'));
|
||||
$this->assertNoInternalOverlap($entries);
|
||||
$this->assertNoOtherLocationOverlap(
|
||||
$entries,
|
||||
@@ -193,6 +195,14 @@ final class PlanningController
|
||||
]);
|
||||
}
|
||||
|
||||
Journal::ecriture(
|
||||
'PLANNING_ENREGISTRE',
|
||||
'planning',
|
||||
$planningId,
|
||||
sprintf('Brouillon enregistré : %d créneau(x) pour l’agent #%d, semaine %s.', count($entries), (int) $agentId, $weekValue),
|
||||
['id_agent' => (int) $agentId, 'id_structure' => (int) $structureId, 'semaine' => $weekValue]
|
||||
);
|
||||
|
||||
JsonResponse::send(200, [
|
||||
'success' => true,
|
||||
'planning_id' => $planningId,
|
||||
@@ -264,6 +274,7 @@ final class PlanningController
|
||||
if (!$validDate || (int) $dateObj->format('N') > 5 || !$quarterHour($start) || !$quarterHour($end) || $end <= $start) {
|
||||
JsonResponse::send(422, ['error' => 'La date ou la plage horaire est invalide. Le jour doit être compris entre le lundi et le vendredi et les horaires saisis par quarts d’heure.']);
|
||||
}
|
||||
$this->assertDatesWithinContract($entryAgent, [$date]);
|
||||
|
||||
try {
|
||||
$result = $this->planningModel->updateEntryFromAgentView(
|
||||
@@ -284,6 +295,14 @@ final class PlanningController
|
||||
}
|
||||
|
||||
$reopened = count($result['reopened_planning_ids']) > 0;
|
||||
Journal::ecriture(
|
||||
'CRENEAU_MODIFIE',
|
||||
'creneau_horaire',
|
||||
(int) $entryId,
|
||||
sprintf('Créneau du %s modifié (%s-%s) pour l’agent #%d.', $date, $start, $end, (int) $result['agent_id']),
|
||||
['id_agent' => (int) $result['agent_id'], 'id_structure' => (int) $structureId, 'revalidation' => $reopened]
|
||||
);
|
||||
|
||||
JsonResponse::send(200, [
|
||||
'success' => true,
|
||||
'message' => $reopened
|
||||
@@ -337,6 +356,14 @@ final class PlanningController
|
||||
$message .= ' Le planning validé concerné a été repassé en brouillon.';
|
||||
}
|
||||
|
||||
Journal::ecriture(
|
||||
'CRENEAU_SUPPRIME',
|
||||
'creneau_horaire',
|
||||
(int) $entryId,
|
||||
sprintf('Créneau du %s supprimé pour l’agent #%d.', (string) $result['date'], (int) $result['agent_id']),
|
||||
['id_agent' => (int) $result['agent_id'], 'revalidation' => (bool) $result['reopened']]
|
||||
);
|
||||
|
||||
JsonResponse::send(200, [
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
@@ -383,6 +410,12 @@ final class PlanningController
|
||||
if (!$validDate || (int) $dateObj->format('N') > 5) {
|
||||
JsonResponse::send(422, ['error' => 'Le jour cible doit être un jour ouvré du lundi au vendredi.']);
|
||||
}
|
||||
|
||||
$entryAgent = $this->agentModel->findActive((int) $entry['id_agent']);
|
||||
if ($entryAgent === null) {
|
||||
JsonResponse::send(422, ['error' => 'Agent invalide ou inactif.']);
|
||||
}
|
||||
$this->assertDatesWithinContract($entryAgent, [$targetDate]);
|
||||
try {
|
||||
$result = $this->planningModel->moveOrDuplicateEntry(
|
||||
(int) $entryId,
|
||||
@@ -410,6 +443,14 @@ final class PlanningController
|
||||
$message .= ' Le planning concerné a été repassé en brouillon et devra être validé à nouveau.';
|
||||
}
|
||||
|
||||
Journal::ecriture(
|
||||
$result['action'] === 'duplicate' ? 'CRENEAU_DUPLIQUE' : 'CRENEAU_DEPLACE',
|
||||
'creneau_horaire',
|
||||
(int) $entryId,
|
||||
sprintf('Créneau reporté au %s pour l’agent #%d.', $targetDate, (int) $result['agent_id']),
|
||||
['id_agent' => (int) $result['agent_id'], 'action' => $result['action'], 'revalidation' => $reopened]
|
||||
);
|
||||
|
||||
JsonResponse::send(200, [
|
||||
'success' => true,
|
||||
'action' => $result['action'],
|
||||
@@ -448,9 +489,15 @@ final class PlanningController
|
||||
if (!$agentId || $sourceWeek === null || $targetWeek === null || !in_array($mode, ['merge', 'replace'], true)) {
|
||||
JsonResponse::send(422, ['error' => 'Agent, semaines ou mode de duplication invalide.']);
|
||||
}
|
||||
if ($this->agentModel->findActive((int) $agentId) === null) {
|
||||
$agent = $this->agentModel->findActive((int) $agentId);
|
||||
if ($agent === null) {
|
||||
JsonResponse::send(404, ['error' => 'Agent introuvable ou inactif.']);
|
||||
}
|
||||
// La copie ne connaît pas d'avance les jours réellement recopiés : on
|
||||
// contrôle les cinq jours ouvrés de la semaine cible, ce qui refuse une
|
||||
// semaine à cheval sur la fin d'un contrat aussi bien qu'une semaine
|
||||
// entièrement hors contrat.
|
||||
$this->assertDatesWithinContract($agent, $this->workingDaysOfWeek($targetWeek[0], $targetWeek[1]));
|
||||
|
||||
try {
|
||||
$result = $this->planningModel->copyAgentWeek(
|
||||
@@ -479,6 +526,25 @@ final class PlanningController
|
||||
$message .= ' Les plannings validés modifiés ont été repassés en brouillon.';
|
||||
}
|
||||
|
||||
Journal::ecriture(
|
||||
'SEMAINE_COPIEE',
|
||||
'planning',
|
||||
null,
|
||||
sprintf(
|
||||
'%d créneau(x) copiés de la semaine %s vers %s pour l’agent #%d.',
|
||||
(int) $result['copied_entries'],
|
||||
$sourceWeekValue,
|
||||
$targetWeekValue,
|
||||
(int) $agentId
|
||||
),
|
||||
[
|
||||
'id_agent' => (int) $agentId,
|
||||
'semaine_source' => $sourceWeekValue,
|
||||
'semaine_cible' => $targetWeekValue,
|
||||
'mode' => $mode,
|
||||
]
|
||||
);
|
||||
|
||||
JsonResponse::send(200, [
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
@@ -531,6 +597,10 @@ final class PlanningController
|
||||
* - AUCUN_CRENEAU : planning vide ;
|
||||
* - CRENEAU_HORS_SEMAINE : créneau hors des bornes de la semaine ;
|
||||
* - CHEVAUCHEMENT_INTERNE : deux créneaux du planning se recouvrent ;
|
||||
* - HORS_CONTRAT : affectation antérieure à l'arrivée de l'agent ou
|
||||
* postérieure à son départ (V7.15). Les gardes d'écriture empêchent d'en
|
||||
* créer, ce contrôle attrape celles qui préexistaient ou qu'un contrat
|
||||
* raccourci a rendues illégitimes ;
|
||||
* - CHEVAUCHEMENT_AUTRE_STRUCTURE : recouvrement avec un créneau DÉJÀ
|
||||
* VALIDÉ sur un autre lieu. Seuls les plannings validés sont opposables :
|
||||
* un brouillon concurrent peut encore être corrigé, le bloquer
|
||||
@@ -569,6 +639,19 @@ final class PlanningController
|
||||
$this->addControl($controls, 'ERREUR', 'AUCUN_CRENEAU', 'Le planning ne contient aucune affectation.');
|
||||
}
|
||||
|
||||
foreach ($this->planningModel->entriesOutsideContract((int) $planningId) as $row) {
|
||||
$this->addControl(
|
||||
$controls,
|
||||
'ERREUR',
|
||||
'HORS_CONTRAT',
|
||||
sprintf(
|
||||
'L’affectation #%d du %s est en dehors des dates de contrat de l’agent.',
|
||||
$row['id_creneau'],
|
||||
$row['date_jour']
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($this->planningModel->entriesOutsideWeek((int) $planningId, $planning['date_debut'], $planning['date_fin']) as $row) {
|
||||
$this->addControl(
|
||||
$controls,
|
||||
@@ -643,6 +726,25 @@ final class PlanningController
|
||||
$this->planningModel->markValidated((int) $planningId);
|
||||
$this->addControl($controls, 'INFO', 'OK', 'Le planning a été validé.');
|
||||
|
||||
// Journalisé APRÈS le passage en base : une validation refusée par les
|
||||
// contrôles métier plus haut n'a pas eu lieu, elle n'a rien à tracer.
|
||||
Journal::ecriture(
|
||||
'PLANNING_VALIDE',
|
||||
'planning',
|
||||
(int) $planningId,
|
||||
sprintf(
|
||||
'Planning validé pour l’agent #%d, semaine du %s.',
|
||||
(int) $planning['id_agent'],
|
||||
(string) $planning['date_debut']
|
||||
),
|
||||
[
|
||||
'id_agent' => (int) $planning['id_agent'],
|
||||
'id_structure' => isset($planning['id_structure']) ? (int) $planning['id_structure'] : null,
|
||||
'date_debut' => (string) $planning['date_debut'],
|
||||
'quota_force' => $forceQuota,
|
||||
]
|
||||
);
|
||||
|
||||
JsonResponse::send(200, [
|
||||
'success' => true,
|
||||
'status' => 'validated',
|
||||
@@ -687,6 +789,18 @@ final class PlanningController
|
||||
JsonResponse::send(409, ['error' => 'Le planning a été modifié entre-temps. Rechargez la page.']);
|
||||
}
|
||||
|
||||
Journal::ecriture(
|
||||
'PLANNING_ROUVERT',
|
||||
'planning',
|
||||
(int) $planningId,
|
||||
sprintf(
|
||||
'Planning repassé en brouillon pour l’agent #%d, semaine du %s.',
|
||||
(int) $planning['id_agent'],
|
||||
(string) $planning['date_debut']
|
||||
),
|
||||
['id_agent' => (int) $planning['id_agent'], 'date_debut' => (string) $planning['date_debut']]
|
||||
);
|
||||
|
||||
JsonResponse::send(200, [
|
||||
'success' => true,
|
||||
'status' => 'BROUILLON',
|
||||
@@ -892,6 +1006,52 @@ final class PlanningController
|
||||
* @param array<int, array{niveau: string, code: string, message: string}> $controls
|
||||
* @param string $level ERREUR (bloquant), ALERTE (confirmable) ou INFO.
|
||||
*/
|
||||
/**
|
||||
* Refuse toute affectation hors des dates de contrat de l'agent (V7.15).
|
||||
*
|
||||
* Un agent n'existe pour PTA qu'entre sa date de début et sa date de fin de
|
||||
* contrat : le planifier avant son arrivée ou après son départ produit des
|
||||
* heures que personne ne fera, et fausse les cumuls annuels comme les
|
||||
* contrôles de couverture. La règle s'applique à TOUS les motifs, congés et
|
||||
* absences compris — hors contrat, il n'y a rien à décompter.
|
||||
*
|
||||
* Toutes les dates sont contrôlées d'un coup pour que l'utilisateur voie
|
||||
* l'ensemble des jours refusés, et non le premier.
|
||||
*
|
||||
* @param array<string, mixed> $agent Fiche agent.
|
||||
* @param array<int, string> $dates Dates AAAA-MM-JJ visées par l'écriture.
|
||||
*/
|
||||
private function assertDatesWithinContract(array $agent, array $dates): void
|
||||
{
|
||||
$hors = AgentModel::datesOutsideContract($agent, $dates);
|
||||
if ($hors === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
JsonResponse::send(422, [
|
||||
'error' => AgentModel::contractViolationMessage($agent, $hors),
|
||||
'code' => 'HORS_CONTRAT',
|
||||
'dates' => $hors,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Les cinq jours ouvrés d'une semaine ISO, au format AAAA-MM-JJ.
|
||||
* Le modèle métier s'arrête au vendredi : samedi et dimanche n'ont pas à
|
||||
* être contrôlés, aucun créneau ne peut y être posé.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function workingDaysOfWeek(int $year, int $weekNumber): array
|
||||
{
|
||||
$lundi = (new DateTimeImmutable())->setISODate($year, $weekNumber, 1);
|
||||
|
||||
return array_map(
|
||||
static fn(int $offset): string => $lundi->modify('+' . $offset . ' day')->format('Y-m-d'),
|
||||
range(0, 4)
|
||||
);
|
||||
}
|
||||
|
||||
private function addControl(array &$controls, string $level, string $code, string $message): void
|
||||
{
|
||||
$controls[] = ['niveau' => $level, 'code' => $code, 'message' => $message];
|
||||
|
||||
Reference in New Issue
Block a user