Publier Globinours 1.0.0-rc.3
This commit is contained in:
parent
ea8c24d622
commit
9a2b4068da
325 changed files with 38230 additions and 20 deletions
705
app/Services/StatisticsService.php
Normal file
705
app/Services/StatisticsService.php
Normal 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 d’un 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 d’un 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 d’un 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 d’un 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 d’un 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 d’un 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 d’accueil' 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue