pour prod

This commit is contained in:
Loic Masi
2026-08-07 16:13:43 +02:00
commit 5fbf76868f
157 changed files with 24085 additions and 0 deletions

209
app/Models/QuotaModel.php Normal file
View File

@@ -0,0 +1,209 @@
<?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'],
];
}
}