Globinours/app/Services/StatisticsService.php

705 lines
36 KiB
PHP
Raw 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);
final class StatisticsService
{
public static function costs(?int $year, ?array $ids = null): array
{
$db = DB::pdo();
$scopeIds = self::costScopeIds($year, $ids);
$empty = [
'total_cents' => 0,
'animals' => 0,
'scope_animals' => count($scopeIds),
'average_all_cents' => 0,
'average_expense_cents' => 0,
'median_expense_cents' => 0,
'by_clinic' => [],
'by_category' => [],
];
if (!$scopeIds) {
return $empty;
}
$where = ['a.deleted_at IS NULL', 'ae.animal_id IN(' . implode(',', array_map('intval', $scopeIds)) . ')'];
$params = [];
if ($year !== null) {
$where[] = 'date(ae.occurred_on) BETWEEN :start AND :end';
$params = [':start' => sprintf('%04d-01-01', $year), ':end' => sprintf('%04d-12-31', $year)];
}
$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 ' .
implode(' AND ', $where);
$map = static function (string $sql) use ($db, $params): array {
$s = $db->prepare($sql);
$s->execute($params);
return $s->fetchAll(PDO::FETCH_ASSOC);
};
$perAnimal = $map(
'SELECT ae.animal_id,SUM(ae.total_cents) total_cents' .
$base .
' GROUP BY ae.animal_id ORDER BY total_cents',
);
$totals = array_map(static fn($row) => (int) $row['total_cents'], $perAnimal);
$total = array_sum($totals);
$animals = count($totals);
return [
'total_cents' => $total,
'animals' => $animals,
'scope_animals' => count($scopeIds),
'average_all_cents' => count($scopeIds) ? (int) round($total / count($scopeIds)) : 0,
'average_expense_cents' => $animals ? (int) round($total / $animals) : 0,
'median_expense_cents' => self::median($totals),
'by_clinic' => $map(
"SELECT dc.id contact_id,COALESCE(dc.name,'—') label,COUNT(DISTINCT ae.animal_id) animals,SUM(ae.total_cents) total_cents,ROUND(SUM(ae.total_cents)*1.0/COUNT(DISTINCT ae.animal_id)) average_cents" .
$base .
' GROUP BY dc.id,dc.name ORDER BY total_cents DESC',
),
'by_category' => $map(
"SELECT COALESCE(ct.category,'other') label,COUNT(DISTINCT ae.animal_id) animals,SUM(ae.total_cents) total_cents,ROUND(SUM(ae.total_cents)*1.0/COUNT(DISTINCT ae.animal_id)) average_cents" .
$base .
' GROUP BY ct.category ORDER BY total_cents DESC',
),
];
}
public static function costsByMunicipality(?int $year): array
{
$db = DB::pdo();
$scopeIds = self::costScopeIds($year, null);
if (!$scopeIds) {
return [];
}
$animals = $db
->query('SELECT id,rescue_location_name,rescue_address FROM animals WHERE deleted_at IS NULL')
->fetchAll(PDO::FETCH_ASSOC);
$cities = [];
$animalCity = [];
foreach ($animals as $row) {
$id = (int) $row['id'];
if (!in_array($id, $scopeIds, true)) {
continue;
}
$city = GrantService::municipality((string) ($row['rescue_location_name'] ?: $row['rescue_address']));
if ($city === 'Non renseignée') {
continue;
}
$animalCity[$id] = $city;
$cities[$city]['animal_ids'][$id] = true;
}
if (!$animalCity) {
return [];
}
$params = [];
$dateSql = '';
if ($year !== null) {
$dateSql = ' AND date(occurred_on) BETWEEN :start AND :end';
$params = [':start' => sprintf('%04d-01-01', $year), ':end' => sprintf('%04d-12-31', $year)];
}
$s = $db->prepare(
'SELECT animal_id,SUM(total_cents) total_cents FROM animal_expenses WHERE animal_id IN(' .
implode(',', array_keys($animalCity)) .
')' .
$dateSql .
' GROUP BY animal_id',
);
$s->execute($params);
foreach ($s->fetchAll(PDO::FETCH_ASSOC) as $row) {
$id = (int) $row['animal_id'];
$city = $animalCity[$id];
$cities[$city]['expense_totals'][$id] = (int) $row['total_cents'];
}
$out = [];
foreach ($cities as $city => $data) {
$totals = array_values($data['expense_totals'] ?? []);
$total = array_sum($totals);
$scope = count($data['animal_ids']);
$with = count($totals);
$out[] = [
'municipality' => $city,
'animals' => $scope,
'animals_with_expense' => $with,
'total_cents' => $total,
'average_all_cents' => $scope ? (int) round($total / $scope) : 0,
'average_expense_cents' => $with ? (int) round($total / $with) : 0,
'median_expense_cents' => self::median($totals),
];
}
usort(
$out,
static fn($a, $b) => $b['total_cents'] <=> $a['total_cents'] ?:
strnatcasecmp($a['municipality'], $b['municipality']),
);
return $out;
}
private static function costScopeIds(?int $year, ?array $ids): 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 {
$start = sprintf('%04d-01-01', $year);
$end = sprintf('%04d-12-31', $year);
$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]);
return array_map('intval', $s->fetchAll(PDO::FETCH_COLUMN));
}
private static function median(array $values): int
{
if (!$values) {
return 0;
}
sort($values, SORT_NUMERIC);
$count = count($values);
$middle = intdiv($count, 2);
return $count % 2 ? (int) $values[$middle] : (int) round(($values[$middle - 1] + $values[$middle]) / 2);
}
private static function idSql(?array $ids, string $column = 'animal_id'): string
{
if ($ids === null) {
return '';
}
if (!$ids) {
return ' AND 1=0';
}
return ' AND ' . $column . ' IN (' . implode(',', array_map('intval', $ids)) . ')';
}
public static function municipalities(): array
{
$rows = DB::pdo()
->query('SELECT id,rescue_location_name,rescue_address FROM animals WHERE deleted_at IS NULL')
->fetchAll(PDO::FETCH_ASSOC);
$items = [];
foreach ($rows as $row) {
$city = GrantService::municipality((string) ($row['rescue_location_name'] ?: $row['rescue_address']));
if ($city === 'Non renseignée') {
continue;
}
$items[$city] = ($items[$city] ?? 0) + 1;
}
uksort($items, static fn($a, $b) => strnatcasecmp($a, $b));
return $items;
}
public static function animalIdsForMunicipality(string $municipality): array
{
if ($municipality === '') {
return [];
}
$rows = DB::pdo()
->query('SELECT id,rescue_location_name,rescue_address FROM animals WHERE deleted_at IS NULL')
->fetchAll(PDO::FETCH_ASSOC);
$ids = [];
foreach ($rows as $row) {
if (
GrantService::municipality((string) ($row['rescue_location_name'] ?: $row['rescue_address'])) ===
$municipality
) {
$ids[] = (int) $row['id'];
}
}
return $ids;
}
public static function municipalityCoverage(): array
{
$row =
DB::pdo()
->query(
"SELECT COUNT(*) total,SUM(CASE WHEN TRIM(COALESCE(rescue_location_name,''))<>'' OR TRIM(COALESCE(rescue_address,''))<>'' THEN 1 ELSE 0 END) known,SUM(CASE WHEN (TRIM(COALESCE(rescue_location_name,''))<>'' OR TRIM(COALESCE(rescue_address,''))<>'') AND COALESCE(rescue_location_confidence,'')<>'inferred_family' THEN 1 ELSE 0 END) documented,SUM(CASE WHEN rescue_location_confidence='inferred_family' THEN 1 ELSE 0 END) inferred,SUM(CASE WHEN intake_type='born_in_care' OR intake_reason='birth' THEN 1 ELSE 0 END) born_in_care,SUM(CASE WHEN TRIM(COALESCE(rescue_location_name,''))='' AND TRIM(COALESCE(rescue_address,''))='' AND intake_type<>'born_in_care' AND intake_reason<>'birth' THEN 1 ELSE 0 END) unknown FROM animals WHERE deleted_at IS NULL",
)
->fetch(PDO::FETCH_ASSOC) ?:
[];
return array_map('intval', $row);
}
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);
}
$db = DB::pdo();
$range = [':start' => sprintf('%04d-01-01', $year), ':end' => sprintf('%04d-12-31', $year)];
$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" .
$filter .
' UNION SELECT a.id FROM animals a WHERE a.deleted_at IS NULL AND date(a.intake_date) BETWEEN :start AND :end' .
$filter .
" UNION SELECT a.id FROM animals a JOIN animal_deaths d ON d.animal_id=a.id WHERE a.deleted_at IS NULL AND NULLIF(TRIM(a.intake_date),'') IS NULL AND NOT EXISTS(SELECT 1 FROM animal_movements em WHERE em.animal_id=a.id AND em.kind='entry') AND date(COALESCE(d.deceased_date,printf('%04d-01-01',d.death_year))) BETWEEN :start AND :end" .
$filter .
')',
);
$s->execute($range);
$stats['handled'] = (int) $s->fetchColumn();
$s = $db->prepare(
'SELECT COUNT(*) FROM medical_notes mn JOIN animals a ON a.id=mn.animal_id WHERE date(mn.noted_at) BETWEEN :start AND :end' .
$filter,
);
$s->execute($range);
$stats['medical_notes'] = (int) $s->fetchColumn();
return $stats;
}
public static function comparison(int $year, ?array $ids = null): array
{
$current = self::annual($year, $ids);
$previous = self::annual($year - 1, $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 $out;
}
public static function years(): array
{
$db = DB::pdo();
$found = [];
$queries = [
'SELECT intake_date d FROM animals WHERE deleted_at IS NULL AND intake_date IS NOT NULL',
"SELECT created_at d FROM animal_movements WHERE kind IN ('entry','exit')",
'SELECT adoption_date d FROM adoptions',
"SELECT COALESCE(deceased_date,printf('%04d-01-01',death_year)) d FROM animal_deaths WHERE deceased_date IS NOT NULL OR death_year IS NOT NULL",
'SELECT birth_date d FROM litters WHERE birth_date IS NOT NULL',
'SELECT done_date d FROM vaccinations',
'SELECT start_date d FROM treatments',
'SELECT noted_at d FROM medical_notes',
'SELECT occurred_on d FROM animal_expenses',
];
foreach ($queries as $sql) {
foreach ($db->query($sql)->fetchAll(PDO::FETCH_COLUMN) as $date) {
$y = (int) substr((string) $date, 0, 4);
if ($y >= 2000 && $y <= 2200) {
$found[$y] = true;
}
}
}
if (!$found) {
$found[(int) date('Y')] = true;
}
$first = min(array_keys($found));
$last = max((int) date('Y'), max(array_keys($found)));
return range($first, $last);
}
public static function timeline(?array $years = null, ?array $ids = null): array
{
$out = [];
foreach ($years ?? self::years() as $year) {
$out[(int) $year] = self::annual((int) $year, $ids);
}
return $out;
}
public static function all(?array $ids = null): array
{
$stats = array_fill_keys(
['entries', 'exits', 'adoptions', 'deaths', 'births', 'vaccinations', 'treatments', 'medical_notes'],
0.0,
);
foreach (self::timeline(null, $ids) as $annual) {
foreach (array_keys($stats) as $key) {
$stats[$key] += (float) $annual[$key];
}
}
$db = DB::pdo();
$stats['deaths'] =
(float) ($db
->query(
'SELECT COUNT(*) FROM animal_deaths d JOIN animals a ON a.id=d.animal_id WHERE a.deleted_at IS NULL' .
self::idSql($ids, 'd.animal_id'),
)
->fetchColumn() ?:
0);
if ($ids === null) {
$stats['handled'] =
(float) ($db->query('SELECT COUNT(*) FROM animals WHERE deleted_at IS NULL')->fetchColumn() ?: 0);
$stats['avg'] =
(float) ($db
->query(
"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 adoption_date IS NOT NULL AND date(adoption_date)>=date(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))",
)
->fetchColumn() ?:
0);
} else {
$stats['handled'] = (float) count($ids);
$stats['avg'] =
(float) ($db
->query(
"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 adoption_date IS NOT NULL AND date(adoption_date)>=date(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))" .
self::idSql($ids, 'a.id'),
)
->fetchColumn() ?:
0);
}
$stats['entries'] = $stats['handled'];
$stats['exits'] =
(float) ($db
->query(
'SELECT (SELECT COUNT(*) FROM adoptions ad JOIN animals a ON a.id=ad.animal_id WHERE a.deleted_at IS NULL' .
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 NOT EXISTS(SELECT 1 FROM adoptions ad WHERE ad.animal_id=d.animal_id)' .
self::idSql($ids, 'd.animal_id') .
")+(SELECT COUNT(*) FROM animals a WHERE a.deleted_at IS NULL AND a.status='enfui' AND NOT EXISTS(SELECT 1 FROM adoptions ad WHERE ad.animal_id=a.id) AND NOT EXISTS(SELECT 1 FROM animal_deaths d WHERE d.animal_id=a.id)" .
self::idSql($ids, 'a.id') .
')',
)
->fetchColumn() ?:
0);
$stats['adoptions'] =
(float) ($db
->query(
'SELECT COUNT(*) FROM adoptions ad JOIN animals a ON a.id=ad.animal_id WHERE a.deleted_at IS NULL' .
self::idSql($ids, 'ad.animal_id'),
)
->fetchColumn() ?:
0);
$stats['births'] =
(float) ($db
->query(
'SELECT COUNT(*) FROM litter_kittens lk JOIN animals a ON a.id=lk.animal_id WHERE a.deleted_at IS NULL' .
self::idSql($ids, 'lk.animal_id'),
)
->fetchColumn() ?:
0);
$stats['vaccinations'] =
(float) ($db
->query(
'SELECT COUNT(*) FROM vaccinations v JOIN animals a ON a.id=v.animal_id WHERE a.deleted_at IS NULL' .
self::idSql($ids, 'v.animal_id'),
)
->fetchColumn() ?:
0);
$stats['medical_notes'] =
(float) ($db
->query(
'SELECT COUNT(*) FROM medical_notes mn JOIN animals a ON a.id=mn.animal_id WHERE a.deleted_at IS NULL' .
self::idSql($ids, 'mn.animal_id'),
)
->fetchColumn() ?:
0);
$stats['current'] = (float) (self::currentHealth($ids)['total'] ?? 0);
return $stats;
}
public static function undatedHandled(?array $ids = null): int
{
return (int) (DB::pdo()
->query(
"SELECT COUNT(*) FROM animals a WHERE a.deleted_at IS NULL AND (a.intake_date IS NULL OR TRIM(a.intake_date)='') AND NOT EXISTS(SELECT 1 FROM animal_movements m WHERE m.animal_id=a.id AND m.kind='entry')" .
self::idSql($ids, 'a.id'),
)
->fetchColumn() ?:
0);
}
public static function undatedDeaths(?array $ids = null): int
{
return (int) (DB::pdo()
->query(
"SELECT COUNT(*) FROM animal_deaths d JOIN animals a ON a.id=d.animal_id WHERE a.deleted_at IS NULL AND (d.deceased_date IS NULL OR TRIM(d.deceased_date)='') AND d.death_year IS NULL" .
self::idSql($ids, 'd.animal_id'),
)
->fetchColumn() ?:
0);
}
public static function undatedBirths(?array $ids = null): int
{
return (int) (DB::pdo()
->query(
"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 COALESCE(NULLIF(l.birth_date,''),NULLIF(a.birth_date,'')) IS NULL" .
self::idSql($ids, 'lk.animal_id'),
)
->fetchColumn() ?:
0);
}
public static function undatedExits(?array $ids = null): int
{
return (int) (DB::pdo()
->query(
"SELECT (SELECT COUNT(*) FROM animal_deaths d JOIN animals a ON a.id=d.animal_id WHERE a.deleted_at IS NULL AND (d.deceased_date IS NULL OR TRIM(d.deceased_date)='') AND d.death_year IS NULL AND NOT EXISTS(SELECT 1 FROM adoptions ad WHERE ad.animal_id=d.animal_id)" .
self::idSql($ids, 'd.animal_id') .
")+(SELECT COUNT(*) FROM animals a WHERE a.deleted_at IS NULL AND a.status='enfui' AND NOT EXISTS(SELECT 1 FROM adoptions ad WHERE ad.animal_id=a.id) AND NOT EXISTS(SELECT 1 FROM animal_deaths d WHERE d.animal_id=a.id)" .
self::idSql($ids, 'a.id') .
')',
)
->fetchColumn() ?:
0);
}
public static function scopeCounts(?array $ids = null): array
{
$db = DB::pdo();
return [
'vaccinated_animals' => (int) $db
->query(
'SELECT COUNT(DISTINCT v.animal_id) FROM vaccinations v JOIN animals a ON a.id=v.animal_id WHERE a.deleted_at IS NULL' .
self::idSql($ids, 'v.animal_id'),
)
->fetchColumn(),
'medical_animals' => (int) $db
->query(
'SELECT COUNT(DISTINCT mn.animal_id) FROM medical_notes mn JOIN animals a ON a.id=mn.animal_id WHERE a.deleted_at IS NULL' .
self::idSql($ids, 'mn.animal_id'),
)
->fetchColumn(),
];
}
private static function details(int $year, array $ids): array
{
$db = DB::pdo();
$p = [':start' => sprintf('%04d-01-01', $year), ':end' => sprintf('%04d-12-31', $year)];
$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') .
' UNION SELECT id animal_id,date(intake_date) entry_date FROM animals WHERE deleted_at IS NULL AND date(intake_date) BETWEEN :start AND :end' .
self::idSql($ids, 'id') .
'), entered AS (SELECT animal_id,MIN(entry_date) entry_date FROM entry_sources GROUP BY animal_id) ';
$map = static function (string $sql) use ($db, $p): array {
$s = $db->prepare($sql);
$s->execute($p);
$out = [];
foreach ($s->fetchAll(PDO::FETCH_ASSOC) as $r) {
$out[(string) $r['label']] = (int) $r['total'];
}
return $out;
};
$sex = $map(
$base .
"SELECT CASE a.sex WHEN 'F' THEN 'Femelles' WHEN 'M' THEN 'Mâles' ELSE 'Inconnu' END label,COUNT(*) total FROM entered e JOIN animals a ON a.id=e.animal_id GROUP BY a.sex ORDER BY total DESC",
);
$ages = $map(
$base .
"SELECT CASE WHEN a.birth_date IS NULL THEN 'Non renseigné' WHEN (julianday(e.entry_date)-julianday(a.birth_date))/365.25<1 THEN 'Moins dun an' WHEN (julianday(e.entry_date)-julianday(a.birth_date))/365.25<3 THEN '1 à 2 ans' WHEN (julianday(e.entry_date)-julianday(a.birth_date))/365.25<8 THEN '3 à 7 ans' ELSE '8 ans et plus' END label,COUNT(*) total FROM entered e JOIN animals a ON a.id=e.animal_id GROUP BY label ORDER BY total DESC",
);
$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 .
' GROUP BY cause_code ORDER BY total DESC',
);
$stays = $map(
"SELECT CASE WHEN julianday(ad.adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))<30 THEN 'Moins dun mois' WHEN julianday(ad.adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))<90 THEN '1 à 3 mois' WHEN julianday(ad.adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))<180 THEN '3 à 6 mois' WHEN julianday(ad.adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))<365 THEN '6 à 12 mois' ELSE 'Plus dun an' END label,COUNT(*) total FROM adoptions ad JOIN animals a ON a.id=ad.animal_id WHERE date(ad.adoption_date) BETWEEN :start AND :end AND date(ad.adoption_date)>=date(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))" .
self::idSql($ids, 'a.id') .
' GROUP BY label ORDER BY total DESC',
);
return compact('sex', 'ages', 'deathCauses', 'stays');
}
public static function distributions(?int $year, ?array $ids = null, ?string $municipality = null): array
{
$db = DB::pdo();
$p = $year === null ? [] : [':start' => sprintf('%04d-01-01', $year), ':end' => sprintf('%04d-12-31', $year)];
$rows = static function (string $sql, array $params = []) use ($db): array {
$s = $db->prepare($sql);
$s->execute($params);
$out = [];
foreach ($s->fetchAll(PDO::FETCH_ASSOC) as $r) {
$out[(string) $r['label']] = (int) $r['total'];
}
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";
$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 .
self::idSql($ids, 'm.animal_id') .
' UNION SELECT id animal_id,date(intake_date) entry_date FROM animals WHERE deleted_at IS NULL' .
$datedAnimal .
$animalDate .
self::idSql($ids, 'id') .
'), entered AS (SELECT animal_id,MIN(entry_date) entry_date FROM entry_sources GROUP BY animal_id) ';
$colors = $rows(
$entered .
"SELECT COALESCE(NULLIF(TRIM(a.color),''),'Non renseignée') label,COUNT(*) total FROM entered e JOIN animals a ON a.id=e.animal_id GROUP BY label ORDER BY total DESC,label",
$p,
);
$breeds = $rows(
$entered .
"SELECT COALESCE(NULLIF(TRIM(a.breed),''),'Non renseignée') label,COUNT(*) total FROM entered e JOIN animals a ON a.id=e.animal_id GROUP BY label ORDER BY total DESC,label",
$p,
);
$species = $rows(
$entered .
"SELECT CASE lower(a.species) WHEN 'cat' THEN 'Chats' WHEN 'chat' THEN 'Chats' WHEN 'dog' THEN 'Chiens' WHEN 'chien' THEN 'Chiens' ELSE COALESCE(NULLIF(TRIM(a.species),''),'Inconnue') END label,COUNT(*) total FROM entered e JOIN animals a ON a.id=e.animal_id GROUP BY label ORDER BY total DESC",
$p,
);
$intakeTypes = $rows(
$entered .
"SELECT COALESCE(NULLIF(a.intake_type,''),'unknown') label,COUNT(*) total FROM entered e JOIN animals a ON a.id=e.animal_id GROUP BY label ORDER BY total DESC",
$p,
);
$intakeReasons = $rows(
$entered .
"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) {
$animalFilter = self::idSql($ids, 'a.id');
$details = [];
$details['sex'] = $rows(
"SELECT CASE a.sex WHEN 'F' THEN 'Femelles' WHEN 'M' THEN 'Mâles' ELSE 'Inconnu' END label,COUNT(*) total FROM animals a WHERE a.deleted_at IS NULL" .
$animalFilter .
' GROUP BY a.sex ORDER BY total DESC',
);
$details['ages'] = $rows(
"SELECT CASE WHEN a.birth_date IS NULL OR NULLIF(TRIM(a.intake_date),'') IS NULL THEN 'Non renseigné' WHEN (julianday(a.intake_date)-julianday(a.birth_date))/365.25<1 THEN 'Moins dun an' WHEN (julianday(a.intake_date)-julianday(a.birth_date))/365.25<3 THEN '1 à 2 ans' WHEN (julianday(a.intake_date)-julianday(a.birth_date))/365.25<8 THEN '3 à 7 ans' ELSE '8 ans et plus' END label,COUNT(*) total FROM animals a WHERE a.deleted_at IS NULL" .
$animalFilter .
' GROUP BY label ORDER BY total DESC',
);
$details['deathCauses'] = $rows(
"SELECT COALESCE(NULLIF(d.cause_code,''),'unknown') label,COUNT(*) total FROM animal_deaths d JOIN animals a ON a.id=d.animal_id WHERE a.deleted_at IS NULL" .
self::idSql($ids, 'd.animal_id') .
' GROUP BY label ORDER BY total DESC',
);
$details['stays'] = $rows(
"SELECT CASE WHEN julianday(ad.adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))<30 THEN 'Moins dun mois' WHEN julianday(ad.adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))<90 THEN '1 à 3 mois' WHEN julianday(ad.adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))<180 THEN '3 à 6 mois' WHEN julianday(ad.adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))<365 THEN '6 à 12 mois' ELSE 'Plus dun an' END label,COUNT(*) total FROM adoptions ad JOIN animals a ON a.id=ad.animal_id WHERE a.deleted_at IS NULL AND ad.adoption_date IS NOT NULL AND date(ad.adoption_date)>=date(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))" .
$animalFilter .
' 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'];
}
foreach ($details as &$items) {
arsort($items, SORT_NUMERIC);
}
unset($items);
$filter = self::idSql($ids, 'id');
$currentStatus = $rows(
"SELECT CASE status WHEN 'refuge' THEN 'Refuge' WHEN 'fa' THEN 'Famille daccueil' WHEN 'fa_permanente' THEN 'Accueil permanent' WHEN 'chat_libre' THEN 'Chats libres suivis' WHEN 'quarantaine' THEN 'Quarantaine' WHEN 'soin' THEN 'Infirmerie' WHEN 'reserve' THEN 'Réservé' ELSE status END label,COUNT(*) total FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL AND lower(status) NOT IN ('adopte','adopté','decede','décédé')" .
$filter .
' GROUP BY status ORDER BY total DESC',
);
$freeCatSites = $rows(
"SELECT COALESCE(NULLIF(TRIM(free_cat_site_name),''),NULLIF(TRIM(free_cat_city),''),'Site non renseigné') label,COUNT(*) total FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL AND status='chat_libre'" .
$filter .
' GROUP BY label ORDER BY total DESC,label',
);
$releaseSql =
$year === null
? "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'
: "SELECT COALESCE(NULLIF(TRIM(free_cat_site_name),''),NULLIF(TRIM(free_cat_city),''),'Site non renseigné') label,COUNT(*) total FROM animals WHERE deleted_at IS NULL AND date(free_cat_released_at) BETWEEN :start AND :end" .
$filter .
' GROUP BY label ORDER BY total DESC,label';
$freeCatReleases = $rows($releaseSql, $p);
return [
'colors' => $colors,
'breeds' => $breeds,
'species' => $species,
'intakeTypes' => $intakeTypes,
'intakeReasons' => $intakeReasons,
'sex' => $details['sex'],
'ages' => $details['ages'],
'origins' => $origins,
'deathCauses' => $details['deathCauses'],
'stays' => $details['stays'],
'currentStatus' => $currentStatus,
'freeCatSites' => $freeCatSites,
'freeCatReleases' => $freeCatReleases,
];
}
public static function currentHealth(?array $ids = null): array
{
$sql =
"SELECT COUNT(*) total,SUM(CASE WHEN status='chat_libre' THEN 1 ELSE 0 END) free_cats,SUM(CASE WHEN sterilization_status='yes' THEN 1 ELSE 0 END) sterilized,SUM(CASE WHEN sterilization_status='no' THEN 1 ELSE 0 END) unsterilized,SUM(CASE WHEN COALESCE(NULLIF(sterilization_status,''),'unknown')='unknown' THEN 1 ELSE 0 END) sterilization_unknown,SUM(CASE WHEN COALESCE(NULLIF(TRIM(chip_id),''),NULLIF(TRIM(chip),'')) IS NOT NULL THEN 1 ELSE 0 END) chipped,SUM(CASE WHEN fiv_status='positive' THEN 1 ELSE 0 END) fiv_positive,SUM(CASE WHEN felv_status='positive' THEN 1 ELSE 0 END) felv_positive FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL AND lower(status) NOT IN ('adopte','adopté','decede','décédé')" .
self::idSql($ids, 'id');
$row = DB::pdo()->query($sql)->fetch(PDO::FETCH_ASSOC) ?: [];
return array_map('intval', $row);
}
}