pour prod
This commit is contained in:
210
app/Controllers/AnnualOverviewController.php
Normal file
210
app/Controllers/AnnualOverviewController.php
Normal file
@@ -0,0 +1,210 @@
|
||||
<?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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user