Publier Globinours 1.0.0-rc.3

This commit is contained in:
Alexandre NOEL 2026-09-03 12:39:15 +02:00
commit 9a2b4068da
325 changed files with 38230 additions and 20 deletions

View file

@ -0,0 +1,705 @@
<?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);
}
}