211 lines
8.1 KiB
PHP
211 lines
8.1 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Controllers;
|
||
|
||
use App\Core\Access;
|
||
use App\Core\Csrf;
|
||
use App\Core\JsonResponse;
|
||
use App\Core\Request;
|
||
use App\Models\AnnualOverviewModel;
|
||
use App\Models\PlanningModel;
|
||
use App\Models\QuotaModel;
|
||
use PDO;
|
||
use Throwable;
|
||
|
||
final class AnnualOverviewController
|
||
{
|
||
public function __construct(private PDO $pdo)
|
||
{
|
||
}
|
||
|
||
public function overview(): void
|
||
{
|
||
(new Access($this->pdo))->requireService();
|
||
$agentId = filter_input(INPUT_GET, 'agent_id', FILTER_VALIDATE_INT);
|
||
$year = filter_input(INPUT_GET, 'year', FILTER_VALIDATE_INT);
|
||
$year ??= (int) date('Y');
|
||
|
||
if (!$agentId || $year < 2000 || $year > 2100) {
|
||
JsonResponse::send(422, ['error' => 'Agent ou année invalide.']);
|
||
}
|
||
|
||
try {
|
||
JsonResponse::send(200, (new AnnualOverviewModel($this->pdo))->overview((int) $agentId, (int) $year));
|
||
} catch (Throwable $e) {
|
||
JsonResponse::send(500, [
|
||
'error' => 'Impossible de générer la vue annuelle.',
|
||
'details' => (getenv('PTA_DEBUG') ?: '1') !== '0' ? $e->getMessage() : null,
|
||
]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Contrôle puis validation en une seule opération de tous les plannings
|
||
* en brouillon de l'agent pour l'année civile affichée.
|
||
*
|
||
* Aucune validation partielle : si une erreur est trouvée, aucun planning
|
||
* n'est validé. Les semaines concernées sont retournées au front afin de
|
||
* pouvoir les ouvrir dans un nouvel onglet et les corriger.
|
||
*/
|
||
public function validateYear(): void
|
||
{
|
||
(new Access($this->pdo))->requireService();
|
||
Request::requireMethod('POST');
|
||
$payload = Request::json();
|
||
Csrf::assertPayload($payload);
|
||
|
||
$agentId = filter_var($payload['agent_id'] ?? null, FILTER_VALIDATE_INT);
|
||
$year = filter_var($payload['year'] ?? null, FILTER_VALIDATE_INT);
|
||
$forceQuota = filter_var($payload['force_quota'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||
|
||
if (!$agentId || !$year || $year < 2000 || $year > 2100) {
|
||
JsonResponse::send(422, ['error' => 'Agent ou année invalide.']);
|
||
}
|
||
|
||
try {
|
||
$planningModel = new PlanningModel($this->pdo);
|
||
$quotaModel = new QuotaModel($this->pdo);
|
||
$plannings = $planningModel->pendingValidationForAgentYear((int) $agentId, (int) $year);
|
||
|
||
if ($plannings === []) {
|
||
JsonResponse::send(200, [
|
||
'success' => true,
|
||
'status' => 'nothing_to_validate',
|
||
'message' => 'Aucun planning en brouillon avec des créneaux n’est à valider pour cette année.',
|
||
'validated_count' => 0,
|
||
'plannings' => [],
|
||
'quota' => $quotaModel->annualForApi((int) $agentId, (int) $year),
|
||
]);
|
||
}
|
||
|
||
$results = [];
|
||
$hasErrors = false;
|
||
foreach ($plannings as $planning) {
|
||
$controls = $this->controlsForPlanning($planningModel, $planning);
|
||
$errors = array_values(array_filter(
|
||
$controls,
|
||
static fn(array $row): bool => $row['niveau'] === 'ERREUR'
|
||
));
|
||
$hasErrors = $hasErrors || $errors !== [];
|
||
$results[] = [
|
||
'id_planning' => (int) $planning['id_planning'],
|
||
'id_structure' => (int) $planning['id_structure'],
|
||
'structure_nom' => (string) $planning['structure_nom'],
|
||
'annee' => (int) $planning['annee'],
|
||
'numero_semaine' => (int) $planning['numero_semaine'],
|
||
'date_debut' => (string) $planning['date_debut'],
|
||
'date_fin' => (string) $planning['date_fin'],
|
||
'nombre_creneaux' => (int) $planning['nombre_creneaux'],
|
||
'total_minutes' => (int) $planning['total_minutes'],
|
||
'controls' => $controls,
|
||
'has_errors' => $errors !== [],
|
||
];
|
||
}
|
||
|
||
$quota = $quotaModel->annualForApi((int) $agentId, (int) $year);
|
||
$quotaWarning = (int) $quota['minutes_restantes'] < 0;
|
||
|
||
if ($hasErrors) {
|
||
JsonResponse::send(422, [
|
||
'success' => false,
|
||
'status' => 'errors',
|
||
'error' => 'Certaines semaines contiennent des erreurs. Aucune semaine n’a été validée.',
|
||
'plannings' => $results,
|
||
'quota' => $quota,
|
||
]);
|
||
}
|
||
|
||
if ($quotaWarning && !$forceQuota) {
|
||
JsonResponse::send(409, [
|
||
'success' => false,
|
||
'status' => 'warning',
|
||
'message' => sprintf(
|
||
'Le quota annuel est dépassé de %s. Confirmez pour valider malgré cet avertissement.',
|
||
$quotaModel->formatMinutes(abs((int) $quota['minutes_restantes']))['libelle']
|
||
),
|
||
'plannings' => $results,
|
||
'quota' => $quota,
|
||
]);
|
||
}
|
||
|
||
$ids = array_map(static fn(array $planning): int => (int) $planning['id_planning'], $plannings);
|
||
$validatedCount = $planningModel->markValidatedMany($ids);
|
||
|
||
JsonResponse::send(200, [
|
||
'success' => true,
|
||
'status' => 'validated',
|
||
'validated_count' => $validatedCount,
|
||
'message' => sprintf(
|
||
'%d planning(s) ont été validés pour l’année %d.',
|
||
$validatedCount,
|
||
(int) $year
|
||
),
|
||
'plannings' => $results,
|
||
'quota' => $quotaModel->annualForApi((int) $agentId, (int) $year),
|
||
]);
|
||
} catch (Throwable $e) {
|
||
JsonResponse::send(500, [
|
||
'error' => 'Impossible de contrôler ou valider l’année complète.',
|
||
'details' => (getenv('PTA_DEBUG') ?: '1') !== '0' ? $e->getMessage() : null,
|
||
]);
|
||
}
|
||
}
|
||
|
||
private function controlsForPlanning(PlanningModel $model, array $planning): array
|
||
{
|
||
$planningId = (int) $planning['id_planning'];
|
||
$controls = [];
|
||
|
||
if ($model->countEntries($planningId) === 0) {
|
||
$this->addControl($controls, 'ERREUR', 'AUCUN_CRENEAU', 'Le planning ne contient aucune affectation.');
|
||
}
|
||
|
||
foreach ($model->entriesOutsideWeek($planningId, (string) $planning['date_debut'], (string) $planning['date_fin']) as $row) {
|
||
$this->addControl(
|
||
$controls,
|
||
'ERREUR',
|
||
'CRENEAU_HORS_SEMAINE',
|
||
sprintf('L’affectation #%d du %s est en dehors de la semaine sélectionnée.', $row['id_creneau'], $row['date_jour'])
|
||
);
|
||
}
|
||
|
||
foreach ($model->internalOverlaps($planningId) as $row) {
|
||
$this->addControl(
|
||
$controls,
|
||
'ERREUR',
|
||
'CHEVAUCHEMENT_INTERNE',
|
||
sprintf('Les affectations #%d et #%d se chevauchent le %s.', $row['c1'], $row['c2'], $row['date_jour'])
|
||
);
|
||
}
|
||
|
||
foreach ($model->externalOverlaps($planningId) as $row) {
|
||
$this->addControl(
|
||
$controls,
|
||
'ERREUR',
|
||
'CHEVAUCHEMENT_AUTRE_STRUCTURE',
|
||
sprintf(
|
||
'L’affectation #%d chevauche l’affectation #%d sur le lieu « %s » le %s (%s).',
|
||
$row['c1'],
|
||
$row['c2'],
|
||
$row['structure_nom'],
|
||
$row['date_jour'],
|
||
strtolower((string) $row['autre_statut'])
|
||
)
|
||
);
|
||
}
|
||
|
||
if ($controls === []) {
|
||
$this->addControl($controls, 'INFO', 'OK', 'Aucune anomalie bloquante détectée pour cette semaine.');
|
||
}
|
||
|
||
return $controls;
|
||
}
|
||
|
||
private function addControl(array &$controls, string $level, string $code, string $message): void
|
||
{
|
||
$controls[] = ['niveau' => $level, 'code' => $code, 'message' => $message];
|
||
}
|
||
}
|