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); } }