Ajouter les statistiques par période

This commit is contained in:
Alexandre NOEL 2026-10-02 12:11:30 +02:00
commit 86642095cd
14 changed files with 315 additions and 125 deletions

View file

@ -4,10 +4,14 @@ declare(strict_types=1);
final class StatisticsService
{
public static function costs(?int $year, ?array $ids = null): array
public static function costs(?int $year, ?array $ids = null, ?string $start = null, ?string $end = null): array
{
$db = DB::pdo();
$scopeIds = self::costScopeIds($year, $ids);
if ($start === null && $year !== null) {
$start = sprintf('%04d-01-01', $year);
$end = sprintf('%04d-12-31', $year);
}
$scopeIds = self::costScopeIds($year, $ids, $start, $end);
$empty = [
'total_cents' => 0,
'animals' => 0,
@ -23,9 +27,9 @@ final class StatisticsService
}
$where = ['a.deleted_at IS NULL', 'ae.animal_id IN(' . implode(',', array_map('intval', $scopeIds)) . ')'];
$params = [];
if ($year !== null) {
if ($start !== null && $end !== null) {
$where[] = 'date(ae.occurred_on) BETWEEN :start AND :end';
$params = [':start' => sprintf('%04d-01-01', $year), ':end' => sprintf('%04d-12-31', $year)];
$params = [':start' => $start, ':end' => $end];
}
$base =
' FROM animal_expenses ae JOIN animals a ON a.id=ae.animal_id LEFT JOIN directory_contacts dc ON dc.id=ae.clinic_contact_id LEFT JOIN clinic_tariffs ct ON ct.id=ae.tariff_id WHERE ' .
@ -63,10 +67,14 @@ final class StatisticsService
];
}
public static function costsByMunicipality(?int $year): array
public static function costsByMunicipality(?int $year, ?string $start = null, ?string $end = null): array
{
$db = DB::pdo();
$scopeIds = self::costScopeIds($year, null);
if ($start === null && $year !== null) {
$start = sprintf('%04d-01-01', $year);
$end = sprintf('%04d-12-31', $year);
}
$scopeIds = self::costScopeIds($year, null, $start, $end);
if (!$scopeIds) {
return [];
}
@ -92,9 +100,9 @@ final class StatisticsService
}
$params = [];
$dateSql = '';
if ($year !== null) {
if ($start !== null && $end !== null) {
$dateSql = ' AND date(occurred_on) BETWEEN :start AND :end';
$params = [':start' => sprintf('%04d-01-01', $year), ':end' => sprintf('%04d-12-31', $year)];
$params = [':start' => $start, ':end' => $end];
}
$s = $db->prepare(
'SELECT animal_id,SUM(total_cents) total_cents FROM animal_expenses WHERE animal_id IN(' .
@ -133,22 +141,24 @@ final class StatisticsService
return $out;
}
private static function costScopeIds(?int $year, ?array $ids): array
private static function costScopeIds(?int $year, ?array $ids, ?string $start = null, ?string $end = null): array
{
$db = DB::pdo();
$limit = $ids === null ? '' : self::idSql($ids, 'a.id');
if ($year === null) {
$sql = 'SELECT a.id FROM animals a WHERE a.deleted_at IS NULL' . $limit;
} else {
if ($start === null && $year !== null) {
$start = sprintf('%04d-01-01', $year);
$end = sprintf('%04d-12-31', $year);
}
if ($start === null || $end === null) {
$sql = 'SELECT a.id FROM animals a WHERE a.deleted_at IS NULL' . $limit;
} else {
$sql =
'SELECT a.id FROM animals a WHERE a.deleted_at IS NULL' .
$limit .
" AND (date(a.intake_date) BETWEEN :start AND :end OR EXISTS(SELECT 1 FROM animal_movements m WHERE m.animal_id=a.id AND m.kind='entry' AND date(m.created_at) BETWEEN :start AND :end) OR EXISTS(SELECT 1 FROM animal_expenses ae WHERE ae.animal_id=a.id AND date(ae.occurred_on) BETWEEN :start AND :end))";
}
$s = $db->prepare($sql);
$s->execute($year === null ? [] : [':start' => $start, ':end' => $end]);
$s->execute($start === null || $end === null ? [] : [':start' => $start, ':end' => $end]);
return array_map('intval', $s->fetchAll(PDO::FETCH_COLUMN));
}
@ -224,64 +234,49 @@ final class StatisticsService
public static function annual(int $year, ?array $ids = null): array
{
if ($ids === null) {
$stats = GrantService::statistics($year);
} else {
$db = DB::pdo();
$p = [':start' => sprintf('%04d-01-01', $year), ':end' => sprintf('%04d-12-31', $year)];
$in = self::idSql($ids);
$scalar = static function (string $sql) use ($db, $p): float {
$s = $db->prepare($sql);
$s->execute($p);
return (float) ($s->fetchColumn() ?: 0);
};
$stats = [];
$stats['entries'] = $scalar(
"SELECT COUNT(*) FROM (SELECT m.animal_id FROM animal_movements m JOIN animals a ON a.id=m.animal_id WHERE a.deleted_at IS NULL AND m.kind='entry' AND date(m.created_at) BETWEEN :start AND :end" .
self::idSql($ids, 'm.animal_id') .
' UNION SELECT a.id FROM animals a WHERE a.deleted_at IS NULL AND date(a.intake_date) BETWEEN :start AND :end' .
self::idSql($ids, 'a.id') .
')',
);
$stats['exits'] = $scalar(
'SELECT (SELECT COUNT(*) FROM adoptions ad JOIN animals a ON a.id=ad.animal_id WHERE a.deleted_at IS NULL AND date(ad.adoption_date) BETWEEN :start AND :end' .
self::idSql($ids, 'ad.animal_id') .
")+(SELECT COUNT(*) FROM animal_deaths d JOIN animals a ON a.id=d.animal_id WHERE a.deleted_at IS NULL AND date(COALESCE(d.deceased_date,printf('%04d-01-01',d.death_year))) BETWEEN :start AND :end AND NOT EXISTS(SELECT 1 FROM adoptions ad WHERE ad.animal_id=d.animal_id)" .
self::idSql($ids, 'd.animal_id') .
')',
);
$stats['adoptions'] = $scalar(
'SELECT COUNT(*) FROM adoptions WHERE date(adoption_date) BETWEEN :start AND :end' . $in,
);
$stats['deaths'] = $scalar(
"SELECT COUNT(*) FROM animal_deaths WHERE date(COALESCE(deceased_date,printf('%04d-01-01',death_year))) BETWEEN :start AND :end" .
$in,
);
$stats['births'] = $scalar(
"SELECT COUNT(*) FROM litter_kittens lk JOIN litters l ON l.id=lk.litter_id JOIN animals a ON a.id=lk.animal_id WHERE a.deleted_at IS NULL AND date(COALESCE(NULLIF(l.birth_date,''),NULLIF(a.birth_date,''))) BETWEEN :start AND :end" .
self::idSql($ids, 'lk.animal_id'),
);
$stats['vaccinations'] = $scalar(
'SELECT COUNT(*) FROM vaccinations WHERE date(done_date) BETWEEN :start AND :end' . $in,
);
$stats['treatments'] = $scalar(
'SELECT COUNT(*) FROM treatments WHERE date(start_date) BETWEEN :start AND :end' . $in,
);
$stats['avg'] = $scalar(
"SELECT AVG(julianday(adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))) FROM adoptions ad JOIN animals a ON a.id=ad.animal_id WHERE date(adoption_date) BETWEEN :start AND :end AND date(adoption_date)>=date(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))" .
self::idSql($ids, 'a.id'),
);
$stats['current'] =
(float) ($db
->query(
"SELECT COUNT(*) FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL AND status NOT IN ('adopte','decede')" .
self::idSql($ids, 'id'),
)
->fetchColumn() ?:
0);
}
return self::period(sprintf('%04d-01-01', $year), sprintf('%04d-12-31', $year), $ids);
}
public static function period(string $start, string $end, ?array $ids = null): array
{
$db = DB::pdo();
$range = [':start' => sprintf('%04d-01-01', $year), ':end' => sprintf('%04d-12-31', $year)];
$range = [':start' => $start, ':end' => $end];
$scalar = static function (string $sql) use ($db, $range): float {
$s = $db->prepare($sql);
$s->execute($range);
return (float) ($s->fetchColumn() ?: 0);
};
$in = self::idSql($ids);
$stats = [];
$stats['entries'] = $scalar(
"SELECT COUNT(*) FROM (SELECT m.animal_id FROM animal_movements m JOIN animals a ON a.id=m.animal_id WHERE a.deleted_at IS NULL AND m.kind='entry' AND date(m.created_at) BETWEEN :start AND :end" .
self::idSql($ids, 'm.animal_id') .
' UNION SELECT a.id FROM animals a WHERE a.deleted_at IS NULL AND date(a.intake_date) BETWEEN :start AND :end' .
self::idSql($ids, 'a.id') .
')',
);
$stats['exits'] = $scalar(
'SELECT (SELECT COUNT(*) FROM adoptions ad JOIN animals a ON a.id=ad.animal_id WHERE a.deleted_at IS NULL AND date(ad.adoption_date) BETWEEN :start AND :end' .
self::idSql($ids, 'ad.animal_id') .
")+(SELECT COUNT(*) FROM animal_deaths d JOIN animals a ON a.id=d.animal_id WHERE a.deleted_at IS NULL AND date(COALESCE(d.deceased_date,printf('%04d-01-01',d.death_year))) BETWEEN :start AND :end AND NOT EXISTS(SELECT 1 FROM adoptions ad WHERE ad.animal_id=d.animal_id)" .
self::idSql($ids, 'd.animal_id') .
')',
);
$stats['adoptions'] = $scalar('SELECT COUNT(*) FROM adoptions WHERE date(adoption_date) BETWEEN :start AND :end' . $in);
$stats['deaths'] = $scalar(
"SELECT COUNT(*) FROM animal_deaths WHERE date(COALESCE(deceased_date,printf('%04d-01-01',death_year))) BETWEEN :start AND :end" . $in,
);
$stats['births'] = $scalar(
"SELECT COUNT(*) FROM litter_kittens lk JOIN litters l ON l.id=lk.litter_id JOIN animals a ON a.id=lk.animal_id WHERE a.deleted_at IS NULL AND date(COALESCE(NULLIF(l.birth_date,''),NULLIF(a.birth_date,''))) BETWEEN :start AND :end" . self::idSql($ids, 'lk.animal_id'),
);
$stats['vaccinations'] = $scalar('SELECT COUNT(*) FROM vaccinations WHERE date(done_date) BETWEEN :start AND :end' . $in);
$stats['treatments'] = $scalar('SELECT COUNT(*) FROM treatments WHERE date(start_date) BETWEEN :start AND :end' . $in);
$stats['avg'] = $scalar(
"SELECT AVG(julianday(adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))) FROM adoptions ad JOIN animals a ON a.id=ad.animal_id WHERE date(adoption_date) BETWEEN :start AND :end AND date(adoption_date)>=date(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))" . self::idSql($ids, 'a.id'),
);
$stats['current'] = (float) ($db->query(
"SELECT COUNT(*) FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL AND status NOT IN ('adopte','decede')" . self::idSql($ids, 'id'),
)->fetchColumn() ?: 0);
$filter = self::idSql($ids, 'a.id');
$s = $db->prepare(
"SELECT COUNT(*) FROM (SELECT m.animal_id FROM animal_movements m JOIN animals a ON a.id=m.animal_id WHERE a.deleted_at IS NULL AND m.kind='entry' AND date(m.created_at) BETWEEN :start AND :end" .
@ -303,6 +298,24 @@ final class StatisticsService
return $stats;
}
public static function comparisonPeriod(string $start, string $end, ?array $ids = null): array
{
$from = new DateTimeImmutable($start);
$to = new DateTimeImmutable($end);
$days = (int) $from->diff($to)->format('%a') + 1;
$previousEnd = $from->modify('-1 day');
$previousStart = $previousEnd->modify('-' . ($days - 1) . ' days');
$current = self::period($start, $end, $ids);
$previous = self::period($previousStart->format('Y-m-d'), $previousEnd->format('Y-m-d'), $ids);
$out = [];
foreach (['handled','entries','exits','adoptions','deaths','births','vaccinations','treatments','medical_notes','avg'] as $key) {
$a = (float) $current[$key];
$b = (float) $previous[$key];
$out[$key] = ['current' => $a,'previous' => $b,'difference' => $a - $b,'percent' => $b != 0 ? (($a - $b) / $b) * 100 : null];
}
return ['rows' => $out,'previous_start' => $previousStart->format('Y-m-d'),'previous_end' => $previousEnd->format('Y-m-d')];
}
public static function comparison(int $year, ?array $ids = null): array
{
$current = self::annual($year, $ids);
@ -532,10 +545,10 @@ final class StatisticsService
];
}
private static function details(int $year, array $ids): array
private static function detailsRange(string $start, string $end, ?array $ids): array
{
$db = DB::pdo();
$p = [':start' => sprintf('%04d-01-01', $year), ':end' => sprintf('%04d-12-31', $year)];
$p = [':start' => $start, ':end' => $end];
$base =
"WITH entry_sources AS (SELECT m.animal_id,date(m.created_at) entry_date FROM animal_movements m JOIN animals source_animal ON source_animal.id=m.animal_id WHERE source_animal.deleted_at IS NULL AND m.kind='entry' AND date(m.created_at) BETWEEN :start AND :end" .
self::idSql($ids, 'm.animal_id') .
@ -561,7 +574,7 @@ final class StatisticsService
);
$deathCauses = $map(
"SELECT cause_code label,COUNT(*) total FROM animal_deaths WHERE date(COALESCE(deceased_date,printf('%04d-01-01',death_year))) BETWEEN :start AND :end" .
$in .
self::idSql($ids) .
' GROUP BY cause_code ORDER BY total DESC',
);
$stays = $map(
@ -572,10 +585,33 @@ final class StatisticsService
return compact('sex', 'ages', 'deathCauses', 'stays');
}
public static function distributions(?int $year, ?array $ids = null, ?string $municipality = null): array
private static function originsForPeriod(string $start, string $end, ?array $ids): array
{
$db = DB::pdo();
$p = $year === null ? [] : [':start' => sprintf('%04d-01-01', $year), ':end' => sprintf('%04d-12-31', $year)];
$sql =
"SELECT DISTINCT a.id,a.rescue_location_name,a.rescue_address FROM animals a WHERE a.deleted_at IS NULL" .
self::idSql($ids, 'a.id') .
" AND (date(a.intake_date) BETWEEN :start AND :end OR EXISTS(SELECT 1 FROM animal_movements m WHERE m.animal_id=a.id AND m.kind='entry' AND date(m.created_at) BETWEEN :start AND :end))";
$stmt = $db->prepare($sql);
$stmt->execute([':start' => $start, ':end' => $end]);
$origins = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$city = GrantService::municipality((string) ($row['rescue_location_name'] ?: $row['rescue_address']));
$origins[$city] = ($origins[$city] ?? 0) + 1;
}
arsort($origins, SORT_NUMERIC);
return $origins;
}
public static function distributions(?int $year, ?array $ids = null, ?string $municipality = null, ?string $start = null, ?string $end = null): array
{
$db = DB::pdo();
if ($start === null && $year !== null) {
$start = sprintf('%04d-01-01', $year);
$end = sprintf('%04d-12-31', $year);
}
$bounded = $start !== null && $end !== null;
$p = $bounded ? [':start' => $start, ':end' => $end] : [];
$rows = static function (string $sql, array $params = []) use ($db): array {
$s = $db->prepare($sql);
$s->execute($params);
@ -585,9 +621,9 @@ final class StatisticsService
}
return $out;
};
$movementDate = $year === null ? '' : ' AND date(m.created_at) BETWEEN :start AND :end';
$animalDate = $year === null ? '' : ' AND date(intake_date) BETWEEN :start AND :end';
$datedAnimal = $year === null ? '' : " AND NULLIF(TRIM(intake_date),'') IS NOT NULL";
$movementDate = $bounded ? ' AND date(m.created_at) BETWEEN :start AND :end' : '';
$animalDate = $bounded ? ' AND date(intake_date) BETWEEN :start AND :end' : '';
$datedAnimal = $bounded ? " AND NULLIF(TRIM(intake_date),'') IS NOT NULL" : '';
$entered =
"WITH entry_sources AS (SELECT m.animal_id,date(m.created_at) entry_date FROM animal_movements m JOIN animals source_animal ON source_animal.id=m.animal_id WHERE source_animal.deleted_at IS NULL AND m.kind='entry'" .
$movementDate .
@ -627,7 +663,7 @@ final class StatisticsService
"SELECT CASE WHEN a.intake_owner_care_home=1 THEN 'owner_care_home' ELSE COALESCE(NULLIF(a.intake_reason,''),'unknown') END label,COUNT(*) total FROM entered e JOIN animals a ON a.id=e.animal_id GROUP BY label ORDER BY total DESC",
$p,
);
if ($year === null) {
if (!$bounded) {
$animalFilter = self::idSql($ids, 'a.id');
$details = [];
$details['sex'] = $rows(
@ -651,12 +687,11 @@ final class StatisticsService
' GROUP BY label ORDER BY total DESC',
);
$origins = $municipality !== null ? [$municipality => (int) array_sum($species)] : self::municipalities();
} elseif ($ids !== null) {
$details = self::details($year, $ids);
$origins = $municipality !== null ? [$municipality => (int) array_sum($species)] : [];
} else {
$details = GrantService::detailedStatistics($year);
$origins = $details['origins'];
$details = self::detailsRange($start, $end, $ids);
$origins = $municipality !== null
? [$municipality => (int) array_sum($species)]
: self::originsForPeriod($start, $end, $ids);
}
foreach ($details as &$items) {
arsort($items, SORT_NUMERIC);
@ -674,7 +709,7 @@ final class StatisticsService
' GROUP BY label ORDER BY total DESC,label',
);
$releaseSql =
$year === null
!$bounded
? "SELECT substr(free_cat_released_at,1,4) label,COUNT(*) total FROM animals WHERE deleted_at IS NULL AND free_cat_released_at IS NOT NULL AND trim(free_cat_released_at)<>''" .
$filter .
' GROUP BY label ORDER BY label DESC'