Nouvelle colonne `agent.droit_enveloppe_formation`, cochée par défaut, saisie à la création de l'agent et modifiable depuis sa fiche. Un agent sans ce droit : - ne voit plus le motif « Formation » dans le sélecteur de motif de l'éditeur de planning ni dans le modal de la vue individuelle ; - se voit refuser toute affectation de ce motif par le serveur, avec un message qui nomme le motif et l'agent (save_planning.php et update_agent_entry.php) ; - a une enveloppe de formation de 0 min au PTA annuel, et une ALERTE FORMATION_SANS_DROIT si des heures de formation subsistent dans son planning. À ne pas confondre avec `formation_repartition_annuelle`, qui existait déjà : celle-ci dit que la formation est déjà comptée dans la répartition annuelle — l'agent y a droit, l'enveloppe complémentaire n'est simplement pas ajoutée. Les deux règles sont désormais rassemblées dans AgentModel::trainingEnvelopeMinutes(), qui remplace les deux calculs « 840 ou 0 » jusque-là dupliqués. Le motif interdit reste affiché lorsqu'il est déjà celui de la plage en cours d'édition : le retirer de la liste changerait silencieusement le motif d'une saisie existante. C'est le contrôle de saisie, puis le serveur, qui demandent alors de corriger la plage. La migration est nommée v7_9b et non v7_10 : les fichiers sont joués dans l'ordre alphabétique, où « v7_10 » passerait avant « v7_8 ». Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
476 lines
21 KiB
PHP
476 lines
21 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Models;
|
|
|
|
use DateTimeImmutable;
|
|
use PDO;
|
|
use Throwable;
|
|
|
|
final class AgentModel extends BaseModel
|
|
{
|
|
/** Enveloppe annuelle complémentaire de formation : 14 h. */
|
|
public const ENVELOPPE_FORMATION_MINUTES = 840;
|
|
|
|
/**
|
|
* Un agent sans droit à l'enveloppe de formation ne peut pas se voir
|
|
* affecter le motif « Formation ». Le droit vaut par défaut lorsque
|
|
* l'information n'est pas projetée : ne jamais retirer un droit sur une
|
|
* donnée absente.
|
|
*/
|
|
public static function hasTrainingRight(array $agent): bool
|
|
{
|
|
return (int) ($agent['droit_enveloppe_formation'] ?? 1) === 1;
|
|
}
|
|
|
|
/**
|
|
* Minutes de formation ouvertes à l'agent sur l'année : rien s'il n'y a pas
|
|
* droit, rien non plus si sa formation est déjà comptée dans la répartition
|
|
* annuelle, l'enveloppe complémentaire sinon.
|
|
*/
|
|
public static function trainingEnvelopeMinutes(array $agent): int
|
|
{
|
|
if (!self::hasTrainingRight($agent) || !empty($agent['formation_repartition_annuelle'])) {
|
|
return 0;
|
|
}
|
|
|
|
return self::ENVELOPPE_FORMATION_MINUTES;
|
|
}
|
|
|
|
private const AGENT_SELECT = <<<'SQL'
|
|
SELECT a.id_agent, a.matricule, a.nom, a.prenom, a.email,
|
|
a.telephone, a.adresse, a.est_diplome, a.diplome_libelle,
|
|
a.date_debut_contrat, a.date_fin_contrat, a.type_contrat,
|
|
a.formation_repartition_annuelle, a.droit_enveloppe_formation,
|
|
a.commentaire_pta,
|
|
a.id_poste, po.code AS poste_code, po.libelle AS poste_libelle,
|
|
po.famille AS poste_famille, po.heures_mercredi_minutes,
|
|
po.heures_extrascolaire_minutes, po.autorise_preparation,
|
|
po.peut_assurer_animation, po.preparation_lundi_si_100,
|
|
a.id_structure,
|
|
s.nom AS structure_nom,
|
|
s.nom AS structure_principale_nom,
|
|
s.type_affectation AS structure_type_affectation,
|
|
s.type_affectation AS structure_principale_type_affectation
|
|
FROM agent a
|
|
LEFT JOIN poste_agent po ON po.id_poste = a.id_poste
|
|
LEFT JOIN structure s ON s.id_structure = a.id_structure
|
|
SQL;
|
|
|
|
public function allActive(): array
|
|
{
|
|
return $this->pdo->query(
|
|
self::AGENT_SELECT . " WHERE a.actif = TRUE ORDER BY a.nom, a.prenom"
|
|
)->fetchAll();
|
|
}
|
|
|
|
public function allVisibleForStructure(int $structureId): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
self::AGENT_SELECT . "
|
|
WHERE a.actif = TRUE
|
|
AND (
|
|
a.id_structure = :structure_id_default
|
|
OR EXISTS (
|
|
SELECT 1
|
|
FROM planning p
|
|
WHERE p.id_agent = a.id_agent
|
|
AND p.id_structure = :structure_id_planning
|
|
)
|
|
)
|
|
ORDER BY a.nom, a.prenom"
|
|
);
|
|
$stmt->execute([
|
|
'structure_id_default' => $structureId,
|
|
'structure_id_planning' => $structureId,
|
|
]);
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
public function allEligibleForPta(): array
|
|
{
|
|
return $this->pdo->query(
|
|
self::AGENT_SELECT . "
|
|
WHERE a.actif = TRUE
|
|
AND a.type_contrat = 'PERMANENT'
|
|
AND a.id_poste IS NOT NULL
|
|
ORDER BY a.nom, a.prenom"
|
|
)->fetchAll();
|
|
}
|
|
|
|
public function allPosts(): array
|
|
{
|
|
return $this->pdo->query(
|
|
"SELECT id_poste, code, libelle, famille, heures_mercredi_minutes,
|
|
heures_extrascolaire_minutes, autorise_preparation,
|
|
peut_assurer_animation, preparation_lundi_si_100
|
|
FROM poste_agent
|
|
WHERE actif = TRUE
|
|
ORDER BY ordre_affichage, libelle"
|
|
)->fetchAll();
|
|
}
|
|
|
|
public function listForLocation(int $selectedLocation = 0): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
"SELECT a.id_agent, a.matricule, a.nom, a.prenom, a.id_structure,
|
|
a.id_poste, po.libelle AS poste_libelle,
|
|
s.nom AS structure_nom,
|
|
s.type_affectation AS structure_type_affectation,
|
|
CASE WHEN a.id_structure = :selected_location THEN 1 ELSE 0 END AS lieu_principal_selectionne
|
|
FROM agent a
|
|
LEFT JOIN poste_agent po ON po.id_poste = a.id_poste
|
|
LEFT JOIN structure s ON s.id_structure = a.id_structure
|
|
WHERE a.actif = TRUE
|
|
ORDER BY lieu_principal_selectionne DESC, a.nom, a.prenom"
|
|
);
|
|
$stmt->execute(['selected_location' => $selectedLocation]);
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
public function findActive(int $agentId): ?array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
self::AGENT_SELECT . " WHERE a.id_agent = :id AND a.actif = TRUE LIMIT 1"
|
|
);
|
|
$stmt->execute(['id' => $agentId]);
|
|
return $stmt->fetch() ?: null;
|
|
}
|
|
|
|
public function matriculeExists(string $matricule): bool
|
|
{
|
|
$stmt = $this->pdo->prepare('SELECT 1 FROM agent WHERE matricule = :matricule LIMIT 1');
|
|
$stmt->execute(['matricule' => $matricule]);
|
|
return $stmt->fetchColumn() !== false;
|
|
}
|
|
|
|
public function createWithDefaults(array $data): array
|
|
{
|
|
$contractStart = new DateTimeImmutable((string) $data['date_debut_contrat']);
|
|
$contractEnd = !empty($data['date_fin_contrat'])
|
|
? new DateTimeImmutable((string) $data['date_fin_contrat'])
|
|
: null;
|
|
$year = (int) $contractStart->format('Y');
|
|
// Le quota PTA est suivi sur l'année civile, indépendamment de la date
|
|
// anniversaire du contrat. La date de contrat reste une donnée RH.
|
|
$periodStart = new DateTimeImmutable(sprintf('%04d-01-01', $year));
|
|
$periodEnd = new DateTimeImmutable(sprintf('%04d-12-31', $year));
|
|
$quotaReference = QuotaModel::REFERENCE_MINUTES;
|
|
$quotaTarget = (int) round($quotaReference * ((float) $data['quotite_travail'] / 100));
|
|
$trainingEnvelope = self::trainingEnvelopeMinutes([
|
|
'droit_enveloppe_formation' => !empty($data['droit_enveloppe_formation']) ? 1 : 0,
|
|
'formation_repartition_annuelle' => $data['formation_repartition_annuelle'],
|
|
]);
|
|
|
|
try {
|
|
$this->pdo->beginTransaction();
|
|
|
|
$insertAgent = $this->pdo->prepare(
|
|
'INSERT INTO agent (
|
|
matricule, nom, prenom, email, telephone, adresse,
|
|
est_diplome, diplome_libelle, date_debut_contrat,
|
|
id_poste, type_contrat, date_fin_contrat,
|
|
formation_repartition_annuelle, droit_enveloppe_formation,
|
|
commentaire_pta, id_structure, actif
|
|
) VALUES (
|
|
:matricule, :nom, :prenom, :email, :telephone, :adresse,
|
|
:est_diplome, :diplome_libelle, :date_debut_contrat,
|
|
:id_poste, :type_contrat, :date_fin_contrat,
|
|
:formation_repartition_annuelle, :droit_enveloppe_formation,
|
|
:commentaire_pta, :structure_id, TRUE
|
|
)'
|
|
);
|
|
$insertAgent->execute([
|
|
'matricule' => $data['matricule'],
|
|
'nom' => $data['nom'],
|
|
'prenom' => $data['prenom'],
|
|
'email' => $data['email'],
|
|
'telephone' => $data['telephone'],
|
|
'adresse' => $data['adresse'],
|
|
'est_diplome' => $data['est_diplome'] ? 1 : 0,
|
|
'diplome_libelle' => $data['est_diplome'] ? $data['diplome_libelle'] : null,
|
|
'date_debut_contrat' => $contractStart->format('Y-m-d'),
|
|
'id_poste' => $data['id_poste'],
|
|
'type_contrat' => $data['type_contrat'],
|
|
'date_fin_contrat' => $contractEnd?->format('Y-m-d'),
|
|
'formation_repartition_annuelle' => $data['formation_repartition_annuelle'] ? 1 : 0,
|
|
'droit_enveloppe_formation' => !empty($data['droit_enveloppe_formation']) ? 1 : 0,
|
|
'commentaire_pta' => $data['commentaire_pta'],
|
|
'structure_id' => $data['structure_id'],
|
|
]);
|
|
$agentId = (int) $this->pdo->lastInsertId();
|
|
|
|
$history = $this->pdo->prepare(
|
|
'INSERT INTO agent_structure (id_agent, id_structure, date_debut, date_fin, actif)
|
|
VALUES (:agent_id, :structure_id, :date_debut, NULL, TRUE)'
|
|
);
|
|
$history->execute([
|
|
'agent_id' => $agentId,
|
|
'structure_id' => $data['structure_id'],
|
|
'date_debut' => $contractStart->format('Y-m-d'),
|
|
]);
|
|
|
|
$quota = $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
|
|
)'
|
|
);
|
|
$quota->execute([
|
|
'agent_id' => $agentId,
|
|
'annee' => $year,
|
|
'quotite' => $data['quotite_travail'],
|
|
'quota_reference' => $quotaReference,
|
|
'quota_cible' => $quotaTarget,
|
|
'commentaire' => 'Quota PTA de référence - année civile',
|
|
]);
|
|
|
|
$pta = $this->pdo->prepare(
|
|
'INSERT INTO pta_annuel (
|
|
id_agent, date_debut, date_fin, quotite_travail,
|
|
quota_reference_minutes, quota_cible_minutes,
|
|
enveloppe_formation_minutes, statut
|
|
) VALUES (
|
|
:agent_id, :date_debut, :date_fin, :quotite,
|
|
:quota_reference, :quota_cible,
|
|
:formation, \'BROUILLON\'
|
|
)'
|
|
);
|
|
$pta->execute([
|
|
'agent_id' => $agentId,
|
|
'date_debut' => $periodStart->format('Y-m-d'),
|
|
'date_fin' => $periodEnd->format('Y-m-d'),
|
|
'quotite' => $data['quotite_travail'],
|
|
'quota_reference' => $quotaReference,
|
|
'quota_cible' => $quotaTarget,
|
|
'formation' => $trainingEnvelope,
|
|
]);
|
|
|
|
$this->pdo->commit();
|
|
|
|
return [
|
|
'id_agent' => $agentId,
|
|
'annee_quota' => $year,
|
|
'date_debut_contrat' => $contractStart->format('Y-m-d'),
|
|
'date_fin_contrat' => $contractEnd?->format('Y-m-d'),
|
|
'quota_cible_minutes' => $quotaTarget,
|
|
'id_pta' => (int) $this->pdo->lastInsertId(),
|
|
];
|
|
} catch (Throwable $e) {
|
|
if ($this->pdo->inTransaction()) {
|
|
$this->pdo->rollBack();
|
|
}
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
public function updateProfile(int $agentId, array $data): void
|
|
{
|
|
try {
|
|
$this->pdo->beginTransaction();
|
|
|
|
$currentStmt = $this->pdo->prepare('SELECT id_structure FROM agent WHERE id_agent = :agent_id FOR UPDATE');
|
|
$currentStmt->execute(['agent_id' => $agentId]);
|
|
$currentStructure = $currentStmt->fetchColumn();
|
|
if ($currentStructure === false) {
|
|
throw new \RuntimeException('Agent introuvable.');
|
|
}
|
|
$currentStructureId = $currentStructure !== null ? (int) $currentStructure : null;
|
|
$newStructureId = $data['structure_id'];
|
|
|
|
$update = $this->pdo->prepare(
|
|
'UPDATE agent
|
|
SET email = :email,
|
|
telephone = :telephone,
|
|
adresse = :adresse,
|
|
est_diplome = :est_diplome,
|
|
diplome_libelle = :diplome_libelle,
|
|
date_debut_contrat = :date_debut_contrat,
|
|
id_poste = :id_poste,
|
|
type_contrat = :type_contrat,
|
|
date_fin_contrat = :date_fin_contrat,
|
|
formation_repartition_annuelle = :formation_repartition_annuelle,
|
|
droit_enveloppe_formation = :droit_enveloppe_formation,
|
|
commentaire_pta = :commentaire_pta,
|
|
id_structure = :structure_id
|
|
WHERE id_agent = :agent_id'
|
|
);
|
|
$update->bindValue(':agent_id', $agentId, PDO::PARAM_INT);
|
|
$update->bindValue(':email', $data['email']);
|
|
$update->bindValue(':telephone', $data['telephone']);
|
|
$update->bindValue(':adresse', $data['adresse']);
|
|
$update->bindValue(':est_diplome', $data['est_diplome'] ? 1 : 0, PDO::PARAM_INT);
|
|
$update->bindValue(':diplome_libelle', $data['est_diplome'] ? $data['diplome_libelle'] : null);
|
|
$update->bindValue(':date_debut_contrat', $data['date_debut_contrat']);
|
|
$update->bindValue(':id_poste', $data['id_poste'], PDO::PARAM_INT);
|
|
$update->bindValue(':type_contrat', $data['type_contrat']);
|
|
$update->bindValue(':date_fin_contrat', $data['date_fin_contrat']);
|
|
$update->bindValue(':formation_repartition_annuelle', $data['formation_repartition_annuelle'] ? 1 : 0, PDO::PARAM_INT);
|
|
$update->bindValue(':droit_enveloppe_formation', !empty($data['droit_enveloppe_formation']) ? 1 : 0, PDO::PARAM_INT);
|
|
$update->bindValue(':commentaire_pta', $data['commentaire_pta']);
|
|
if ($newStructureId === null) {
|
|
$update->bindValue(':structure_id', null, PDO::PARAM_NULL);
|
|
} else {
|
|
$update->bindValue(':structure_id', $newStructureId, PDO::PARAM_INT);
|
|
}
|
|
$update->execute();
|
|
|
|
if ($currentStructureId !== $newStructureId) {
|
|
$close = $this->pdo->prepare(
|
|
"UPDATE agent_structure
|
|
SET actif = FALSE,
|
|
date_fin = CASE
|
|
WHEN date_debut IS NOT NULL AND date_debut > CURRENT_DATE THEN date_debut
|
|
ELSE CURRENT_DATE
|
|
END
|
|
WHERE id_agent = :agent_id AND actif = TRUE"
|
|
);
|
|
$close->execute(['agent_id' => $agentId]);
|
|
|
|
if ($newStructureId !== null) {
|
|
$history = $this->pdo->prepare(
|
|
"INSERT INTO agent_structure (id_agent, id_structure, date_debut, date_fin, actif)
|
|
VALUES (:agent_id, :structure_id, CURRENT_DATE, NULL, TRUE)
|
|
ON DUPLICATE KEY UPDATE date_fin = NULL, actif = TRUE"
|
|
);
|
|
$history->execute(['agent_id' => $agentId, 'structure_id' => $newStructureId]);
|
|
}
|
|
}
|
|
|
|
$this->pdo->commit();
|
|
} catch (Throwable $e) {
|
|
if ($this->pdo->inTransaction()) {
|
|
$this->pdo->rollBack();
|
|
}
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
public function updateAssignment(int $agentId, ?int $structureId): void
|
|
{
|
|
try {
|
|
$this->pdo->beginTransaction();
|
|
|
|
$update = $this->pdo->prepare('UPDATE agent SET id_structure = :structure_id WHERE id_agent = :agent_id');
|
|
$update->bindValue(':agent_id', $agentId, PDO::PARAM_INT);
|
|
if ($structureId === null) {
|
|
$update->bindValue(':structure_id', null, PDO::PARAM_NULL);
|
|
} else {
|
|
$update->bindValue(':structure_id', $structureId, PDO::PARAM_INT);
|
|
}
|
|
$update->execute();
|
|
|
|
$close = $this->pdo->prepare(
|
|
"UPDATE agent_structure
|
|
SET actif = FALSE,
|
|
date_fin = CASE
|
|
WHEN date_debut IS NOT NULL AND date_debut > CURRENT_DATE THEN date_debut
|
|
ELSE CURRENT_DATE
|
|
END
|
|
WHERE id_agent = :agent_id AND actif = TRUE"
|
|
);
|
|
$close->execute(['agent_id' => $agentId]);
|
|
|
|
if ($structureId !== null) {
|
|
$history = $this->pdo->prepare(
|
|
"INSERT INTO agent_structure (id_agent, id_structure, date_debut, date_fin, actif)
|
|
VALUES (:agent_id, :structure_id, CURRENT_DATE, NULL, TRUE)
|
|
ON DUPLICATE KEY UPDATE date_fin = NULL, actif = TRUE"
|
|
);
|
|
$history->execute(['agent_id' => $agentId, 'structure_id' => $structureId]);
|
|
}
|
|
|
|
$this->pdo->commit();
|
|
} catch (Throwable $e) {
|
|
if ($this->pdo->inTransaction()) {
|
|
$this->pdo->rollBack();
|
|
}
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
public function entriesForWeek(int $agentId, int $year, int $week): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
"SELECT p.id_planning, p.statut,
|
|
s2.id_structure, s2.nom AS structure_nom,
|
|
s2.type_affectation AS structure_type_affectation,
|
|
c.id_creneau, c.id_motif, c.date_jour,
|
|
TIME_FORMAT(c.heure_debut, '%H:%i') AS heure_debut,
|
|
TIME_FORMAT(c.heure_fin, '%H:%i') AS heure_fin,
|
|
m.code AS motif_code, m.libelle AS motif_libelle, m.compte_dans_quota
|
|
FROM planning p
|
|
INNER JOIN semaine sem ON sem.id_semaine = p.id_semaine
|
|
INNER JOIN structure s2 ON s2.id_structure = p.id_structure
|
|
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 sem.annee = :annee
|
|
AND sem.numero_semaine = :semaine
|
|
ORDER BY c.date_jour, c.heure_debut, s2.nom"
|
|
);
|
|
$stmt->execute(['agent_id' => $agentId, 'annee' => $year, 'semaine' => $week]);
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
public function entriesForDateRange(int $agentId, string $startDate, string $endDate): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
"SELECT p.id_planning, p.statut,
|
|
s2.id_structure, s2.nom AS structure_nom,
|
|
s2.type_affectation AS structure_type_affectation,
|
|
c.id_creneau, c.id_motif, c.date_jour,
|
|
TIME_FORMAT(c.heure_debut, '%H:%i') AS heure_debut,
|
|
TIME_FORMAT(c.heure_fin, '%H:%i') AS heure_fin,
|
|
m.code AS motif_code, m.libelle AS motif_libelle, m.compte_dans_quota
|
|
FROM planning p
|
|
INNER JOIN structure s2 ON s2.id_structure = p.id_structure
|
|
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
|
|
ORDER BY c.date_jour, c.heure_debut, s2.nom"
|
|
);
|
|
$stmt->execute([
|
|
'agent_id' => $agentId,
|
|
'date_debut' => $startDate,
|
|
'date_fin' => $endDate,
|
|
]);
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
public function timeSummaryForRange(int $agentId, string $startDate, string $endDate): array
|
|
{
|
|
$stmt = $this->pdo->prepare(
|
|
"SELECT UPPER(m.code) AS motif_code,
|
|
m.libelle AS motif_libelle,
|
|
m.compte_dans_quota,
|
|
COALESCE(SUM(TIMESTAMPDIFF(MINUTE, CONCAT(c.date_jour, ' ', c.heure_debut), CONCAT(c.date_jour, ' ', c.heure_fin))), 0) AS minutes_total,
|
|
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
|
|
GROUP BY UPPER(m.code), m.libelle, m.compte_dans_quota
|
|
ORDER BY m.compte_dans_quota DESC, m.libelle"
|
|
);
|
|
$stmt->execute([
|
|
'agent_id' => $agentId,
|
|
'date_debut' => $startDate,
|
|
'date_fin' => $endDate,
|
|
]);
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
public function timeSummaryForYear(int $agentId, int $year): array
|
|
{
|
|
return $this->timeSummaryForRange($agentId, sprintf('%04d-01-01', $year), sprintf('%04d-12-31', $year));
|
|
}
|
|
}
|