Files
pta/app/Models/QuotaModel.php
Loic Masi 5fbf76868f pour prod
2026-08-07 16:13:43 +02:00

210 lines
8.2 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace App\Models;
use DateTimeImmutable;
use InvalidArgumentException;
/**
* Suivi du quota PTA sur l'année CIVILE.
*
* Le PTA est planifié du 1er janvier au 31 décembre de l'année concernée.
* La date de début du contrat reste une information RH utile, mais elle ne sert
* plus d'ancre pour décaler la période annuelle du quota.
*/
final class QuotaModel extends BaseModel
{
public const REFERENCE_MINUTES = 1607 * 60;
public function createForAgent(int $agentId, int $year, float $rate): void
{
$target = (int) round(self::REFERENCE_MINUTES * ($rate / 100));
$stmt = $this->pdo->prepare(
'INSERT INTO quota_agent_annuel (
id_agent, annee, quotite_travail, quota_reference_minutes, quota_cible_minutes, commentaire
) VALUES (
:agent_id, :annee, :quotite, :quota_reference, :quota_cible, :commentaire
)'
);
$stmt->execute([
'agent_id' => $agentId,
'annee' => $year,
'quotite' => $rate,
'quota_reference' => self::REFERENCE_MINUTES,
'quota_cible' => $target,
'commentaire' => 'Quota PTA de référence pour lannée civile',
]);
}
/**
* Retourne le quota de l'année civile contenant la date fournie.
*
* Exemple : une date de contexte au 15/09/2026 renvoie toujours la période
* 01/01/2026 -> 31/12/2026, quelle que soit la date de début du contrat.
*/
public function forDate(int $agentId, string $contextDate): array
{
$period = $this->referencePeriod($agentId, $contextDate);
$quota = $this->quotaProfile($agentId, (int) $period['annee_periode']);
$usedStmt = $this->pdo->prepare(
"SELECT
COALESCE(SUM(CASE WHEN p.statut = 'VALIDE' THEN
TIMESTAMPDIFF(MINUTE, CONCAT(c.date_jour, ' ', c.heure_debut), CONCAT(c.date_jour, ' ', c.heure_fin))
ELSE 0 END), 0) AS minutes_valides,
COALESCE(SUM(CASE WHEN p.statut = 'BROUILLON' THEN
TIMESTAMPDIFF(MINUTE, CONCAT(c.date_jour, ' ', c.heure_debut), CONCAT(c.date_jour, ' ', c.heure_fin))
ELSE 0 END), 0) AS minutes_brouillon
FROM planning p
INNER JOIN creneau_horaire c ON c.id_planning = p.id_planning
INNER JOIN motif_planning m ON m.id_motif = c.id_motif
WHERE p.id_agent = :agent_id
AND c.date_jour BETWEEN :date_debut AND :date_fin
AND m.compte_dans_quota = TRUE"
);
$usedStmt->execute([
'agent_id' => $agentId,
'date_debut' => $period['date_debut_periode'],
'date_fin' => $period['date_fin_periode'],
]);
$used = $usedStmt->fetch() ?: ['minutes_valides' => 0, 'minutes_brouillon' => 0];
$validated = (int) $used['minutes_valides'];
$draft = (int) $used['minutes_brouillon'];
$target = (int) $quota['quota_cible_minutes'];
$allocated = $validated + $draft;
return array_merge($period, [
'annee' => (int) $period['annee_periode'],
'quotite_travail' => (float) $quota['quotite_travail'],
'quota_reference_minutes' => (int) $quota['quota_reference_minutes'],
'quota_cible_minutes' => $target,
'quota_source_annee' => (int) $quota['quota_source_annee'],
'minutes_valides' => $validated,
'minutes_brouillon' => $draft,
'minutes_affectees' => $allocated,
'minutes_restantes' => $target - $allocated,
'taux_consommation' => $target > 0 ? round(($allocated / $target) * 100, 2) : 0,
]);
}
public function forDateForApi(int $agentId, string $contextDate): array
{
return $this->enrich($this->forDate($agentId, $contextDate));
}
public function annual(int $agentId, int $year): array
{
return $this->forDate($agentId, sprintf('%04d-01-01', $year));
}
public function annualForApi(int $agentId, int $year): array
{
return $this->forDateForApi($agentId, sprintf('%04d-01-01', $year));
}
/**
* Période de référence PTA : toujours l'année civile.
* La date de contrat est retournée uniquement à titre informatif.
*/
public function referencePeriod(int $agentId, string $contextDate): array
{
$context = DateTimeImmutable::createFromFormat('!Y-m-d', $contextDate);
if (!$context || $context->format('Y-m-d') !== $contextDate) {
throw new InvalidArgumentException('Date de référence du quota invalide.');
}
$stmt = $this->pdo->prepare(
'SELECT date_debut_contrat FROM agent WHERE id_agent = :agent_id LIMIT 1'
);
$stmt->execute(['agent_id' => $agentId]);
$contractStartValue = $stmt->fetchColumn();
$year = (int) $context->format('Y');
$start = new DateTimeImmutable(sprintf('%04d-01-01', $year));
$end = new DateTimeImmutable(sprintf('%04d-12-31', $year));
return [
'date_reference' => $context->format('Y-m-d'),
'date_debut_contrat' => $contractStartValue ? (string) $contractStartValue : null,
'date_debut_periode' => $start->format('Y-m-d'),
'date_fin_periode' => $end->format('Y-m-d'),
'annee_periode' => $year,
'periode_libelle' => sprintf('année civile %d · du %s au %s', $year, $start->format('d/m/Y'), $end->format('d/m/Y')),
'periode_contractuelle' => false,
'periode_civile' => true,
// Conservé pour compatibilité avec les anciens écrans : la date de
// contrat n'est plus requise pour calculer la période du quota.
'date_contrat_manquante' => false,
];
}
public function enrich(array $quota): array
{
return array_merge($quota, [
'quota_cible' => $this->formatMinutes((int) $quota['quota_cible_minutes']),
'valides' => $this->formatMinutes((int) $quota['minutes_valides']),
'brouillons' => $this->formatMinutes((int) $quota['minutes_brouillon']),
'affectees' => $this->formatMinutes((int) $quota['minutes_affectees']),
'restantes' => $this->formatMinutes((int) $quota['minutes_restantes']),
]);
}
public function formatMinutes(int $minutes): array
{
$sign = $minutes < 0 ? -1 : 1;
$absolute = abs($minutes);
$hours = intdiv($absolute, 60);
$mins = $absolute % 60;
return [
'minutes' => $minutes,
'heures_decimales' => round($minutes / 60, 2),
'libelle' => ($sign < 0 ? '-' : '') . $hours . ' h ' . str_pad((string) $mins, 2, '0', STR_PAD_LEFT),
];
}
private function quotaProfile(int $agentId, int $year): array
{
$stmt = $this->pdo->prepare(
"SELECT annee, quotite_travail, quota_reference_minutes, quota_cible_minutes
FROM quota_agent_annuel
WHERE id_agent = :agent_id
ORDER BY CASE
WHEN annee = :year_exact THEN 0
WHEN annee < :year_past THEN 1
ELSE 2
END,
CASE WHEN annee <= :year_latest THEN annee END DESC,
CASE WHEN annee > :year_future THEN annee END ASC
LIMIT 1"
);
$stmt->execute([
'agent_id' => $agentId,
'year_exact' => $year,
'year_past' => $year,
'year_latest' => $year,
'year_future' => $year,
]);
$quota = $stmt->fetch();
if (!$quota) {
return [
'quotite_travail' => 100.00,
'quota_reference_minutes' => self::REFERENCE_MINUTES,
'quota_cible_minutes' => self::REFERENCE_MINUTES,
'quota_source_annee' => $year,
];
}
return [
'quotite_travail' => (float) $quota['quotite_travail'],
'quota_reference_minutes' => (int) $quota['quota_reference_minutes'],
'quota_cible_minutes' => (int) $quota['quota_cible_minutes'],
'quota_source_annee' => (int) $quota['annee'],
];
}
}