210 lines
8.2 KiB
PHP
210 lines
8.2 KiB
PHP
<?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 l’anné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'],
|
||
];
|
||
}
|
||
}
|