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
313
app/Services/GrantService.php
Normal file
313
app/Services/GrantService.php
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
final class GrantService
|
||||
{
|
||||
public const STATUSES = [
|
||||
'draft' => 'Brouillon',
|
||||
'ready' => 'Prête',
|
||||
'submitted' => 'Déposée',
|
||||
'awarded' => 'Accordée',
|
||||
'refused' => 'Refusée',
|
||||
'withdrawn' => 'Abandonnée',
|
||||
];
|
||||
public const CATEGORIES = [
|
||||
'official_form' => 'Formulaire officiel de l’organisme',
|
||||
'statutes' => 'Statuts',
|
||||
'prefecture' => 'Récépissé de préfecture',
|
||||
'jo' => 'Publication au Journal officiel',
|
||||
'rib' => 'RIB',
|
||||
'insurance' => 'Attestation d’assurance',
|
||||
'ag' => 'Procès-verbal d’assemblée générale',
|
||||
'activity' => 'Rapport d’activité',
|
||||
'accounts' => 'Comptes approuvés',
|
||||
'budget' => 'Budget prévisionnel',
|
||||
'other' => 'Autre',
|
||||
];
|
||||
public const REQUIRED = [
|
||||
'official_form',
|
||||
'statutes',
|
||||
'prefecture',
|
||||
'jo',
|
||||
'rib',
|
||||
'insurance',
|
||||
'ag',
|
||||
'activity',
|
||||
'accounts',
|
||||
'budget',
|
||||
];
|
||||
|
||||
public static function statusLabel(string $key): string
|
||||
{
|
||||
return t('grant.status.' . $key, [], self::STATUSES[$key] ?? $key);
|
||||
}
|
||||
public static function categoryLabel(string $key): string
|
||||
{
|
||||
return t('grant.category.' . $key, [], self::CATEGORIES[$key] ?? $key);
|
||||
}
|
||||
|
||||
public static function ensureYear(int $year): array
|
||||
{
|
||||
$db = DB::pdo();
|
||||
$s = $db->prepare('INSERT OR IGNORE INTO grant_years(year) VALUES(?)');
|
||||
$s->execute([$year]);
|
||||
$s = $db->prepare('SELECT * FROM grant_years WHERE year=?');
|
||||
$s->execute([$year]);
|
||||
return $s->fetch() ?: [];
|
||||
}
|
||||
|
||||
public static function statistics(int $year): array
|
||||
{
|
||||
$db = DB::pdo();
|
||||
$start = sprintf('%04d-01-01', $year);
|
||||
$end = sprintf('%04d-12-31', $year);
|
||||
$scalar = static function (string $sql, array $params = []) use ($db): float {
|
||||
$s = $db->prepare($sql);
|
||||
$s->execute($params);
|
||||
return (float) ($s->fetchColumn() ?: 0);
|
||||
};
|
||||
$p = [':start' => $start, ':end' => $end];
|
||||
$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 UNION SELECT a.id FROM animals a WHERE a.deleted_at IS NULL AND date(a.intake_date) BETWEEN :start AND :end)",
|
||||
$p,
|
||||
);
|
||||
$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)+(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))",
|
||||
$p,
|
||||
);
|
||||
$adoptions = $scalar('SELECT COUNT(*) FROM adoptions WHERE date(adoption_date) BETWEEN :start AND :end', $p);
|
||||
$deaths = $scalar(
|
||||
"SELECT COUNT(*) FROM animal_deaths WHERE date(COALESCE(deceased_date,printf('%04d-01-01',death_year))) BETWEEN :start AND :end",
|
||||
$p,
|
||||
);
|
||||
$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",
|
||||
$p,
|
||||
);
|
||||
$vaccinations = $scalar('SELECT COUNT(*) FROM vaccinations WHERE date(done_date) BETWEEN :start AND :end', $p);
|
||||
$treatments = $scalar('SELECT COUNT(*) FROM treatments WHERE date(start_date) BETWEEN :start AND :end', $p);
|
||||
$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)))",
|
||||
$p,
|
||||
);
|
||||
$current = $scalar(
|
||||
"SELECT COUNT(*) FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL AND status NOT IN ('adopte','decede')",
|
||||
);
|
||||
return compact(
|
||||
'entries',
|
||||
'exits',
|
||||
'adoptions',
|
||||
'deaths',
|
||||
'births',
|
||||
'vaccinations',
|
||||
'treatments',
|
||||
'avg',
|
||||
'current',
|
||||
);
|
||||
}
|
||||
|
||||
public static function detailedStatistics(int $year): array
|
||||
{
|
||||
$db = DB::pdo();
|
||||
$start = sprintf('%04d-01-01', $year);
|
||||
$end = sprintf('%04d-12-31', $year);
|
||||
$p = [':start' => $start, ':end' => $end];
|
||||
$rows = static function (string $sql, array $params) use ($db): array {
|
||||
$s = $db->prepare($sql);
|
||||
$s->execute($params);
|
||||
return $s->fetchAll(PDO::FETCH_ASSOC);
|
||||
};
|
||||
$map = static function (array $items, string $key = 'label'): array {
|
||||
$out = [];
|
||||
foreach ($items as $item) {
|
||||
$out[(string) $item[$key]] = (int) $item['total'];
|
||||
}
|
||||
return $out;
|
||||
};
|
||||
$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 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), entered AS (SELECT animal_id,MIN(entry_date) entry_date FROM entry_sources GROUP BY animal_id) ";
|
||||
$sex = $map(
|
||||
$rows(
|
||||
$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",
|
||||
$p,
|
||||
),
|
||||
);
|
||||
$ages = $map(
|
||||
$rows(
|
||||
$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",
|
||||
$p,
|
||||
),
|
||||
);
|
||||
$originRows = $rows(
|
||||
$base . 'SELECT a.rescue_location_name,a.rescue_address FROM entered e JOIN animals a ON a.id=e.animal_id',
|
||||
$p,
|
||||
);
|
||||
$origins = [];
|
||||
foreach ($originRows as $originRow) {
|
||||
$label = self::municipality((string) ($originRow['rescue_location_name'] ?: $originRow['rescue_address']));
|
||||
$origins[$label] = ($origins[$label] ?? 0) + 1;
|
||||
}
|
||||
arsort($origins, SORT_NUMERIC);
|
||||
$origins = array_slice($origins, 0, 12, true);
|
||||
$deathCauses = $map(
|
||||
$rows(
|
||||
"SELECT cause_code label,COUNT(*) total FROM animal_deaths WHERE date(COALESCE(deceased_date,printf('%04d-01-01',death_year))) BETWEEN :start AND :end GROUP BY cause_code ORDER BY total DESC",
|
||||
$p,
|
||||
),
|
||||
);
|
||||
$stays = $map(
|
||||
$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 date(ad.adoption_date) BETWEEN :start AND :end AND date(ad.adoption_date)>=date(COALESCE(NULLIF(a.intake_date,''),date(a.created_at))) GROUP BY label ORDER BY total DESC",
|
||||
$p,
|
||||
),
|
||||
);
|
||||
return compact('sex', 'ages', 'origins', 'deathCauses', 'stays');
|
||||
}
|
||||
|
||||
public static function comparison(int $year): array
|
||||
{
|
||||
$current = self::statistics($year);
|
||||
$previous = self::statistics($year - 1);
|
||||
$out = [];
|
||||
foreach (['entries', 'exits', 'adoptions', 'deaths', 'births', 'vaccinations', 'treatments', '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 municipality(string $value): string
|
||||
{
|
||||
$value = trim(preg_replace('/\s+/u', ' ', $value) ?? '');
|
||||
if ($value === '') {
|
||||
return 'Non renseignée';
|
||||
}
|
||||
if (preg_match('/\b\d{5}\s+([^,;]+)(?:[,;]|$)/u', $value, $match)) {
|
||||
$value = trim($match[1]);
|
||||
} elseif (str_contains($value, ',')) {
|
||||
$parts = array_values(array_filter(array_map('trim', explode(',', $value))));
|
||||
$value = (string) end($parts);
|
||||
}
|
||||
$value = preg_replace('/^(?:FRANCE|FR)\s*[-–—]?\s*/iu', '', $value) ?? $value;
|
||||
return $value !== '' ? mb_convert_case($value, MB_CASE_TITLE, 'UTF-8') : 'Non renseignée';
|
||||
}
|
||||
|
||||
public static function applications(int $year): array
|
||||
{
|
||||
$s = DB::pdo()->prepare(
|
||||
'SELECT * FROM grant_applications WHERE year=? ORDER BY COALESCE(deadline,\'9999-12-31\'), organization_name',
|
||||
);
|
||||
$s->execute([$year]);
|
||||
return $s->fetchAll();
|
||||
}
|
||||
public static function documents(int $year): array
|
||||
{
|
||||
$s = DB::pdo()->prepare(
|
||||
'SELECT d.*,u.display_name uploader,ga.organization_name application_name FROM grant_documents d LEFT JOIN users u ON u.id=d.uploaded_by LEFT JOIN grant_applications ga ON ga.id=d.application_id WHERE d.year IS NULL OR d.year=? ORDER BY d.category,d.created_at DESC',
|
||||
);
|
||||
$s->execute([$year]);
|
||||
return $s->fetchAll();
|
||||
}
|
||||
public static function checklist(int $year): array
|
||||
{
|
||||
$applications = self::applications($year);
|
||||
$documents = self::documents($year);
|
||||
$today = date('Y-m-d');
|
||||
$soon = date('Y-m-d', strtotime('+30 days'));
|
||||
$result = [];
|
||||
foreach ($applications as $application) {
|
||||
$items = [];
|
||||
$valid = 0;
|
||||
$warnings = 0;
|
||||
foreach (self::REQUIRED as $category) {
|
||||
$matches = array_values(
|
||||
array_filter($documents, static function (array $document) use ($category, $application): bool {
|
||||
if ($document['category'] !== $category) {
|
||||
return false;
|
||||
}
|
||||
if ($category === 'official_form') {
|
||||
return (int) ($document['application_id'] ?? 0) === (int) $application['id'];
|
||||
}
|
||||
return empty($document['application_id']) ||
|
||||
(int) $document['application_id'] === (int) $application['id'];
|
||||
}),
|
||||
);
|
||||
usort(
|
||||
$matches,
|
||||
static fn(array $a, array $b): int => strcmp((string) $b['created_at'], (string) $a['created_at']),
|
||||
);
|
||||
$document = $matches[0] ?? null;
|
||||
$state = 'missing';
|
||||
if ($document) {
|
||||
$expiry = (string) ($document['valid_until'] ?? '');
|
||||
if ($expiry !== '' && $expiry < $today) {
|
||||
$state = 'expired';
|
||||
} elseif ($expiry !== '' && $expiry <= $soon) {
|
||||
$state = 'expiring';
|
||||
} else {
|
||||
$state = 'valid';
|
||||
}
|
||||
}
|
||||
if ($state === 'valid') {
|
||||
$valid++;
|
||||
} elseif ($state === 'expiring') {
|
||||
$valid++;
|
||||
$warnings++;
|
||||
} else {
|
||||
$warnings++;
|
||||
}
|
||||
$items[$category] = ['state' => $state, 'document' => $document];
|
||||
}
|
||||
$total = count(self::REQUIRED);
|
||||
$result[(int) $application['id']] = [
|
||||
'items' => $items,
|
||||
'valid' => $valid,
|
||||
'total' => $total,
|
||||
'warnings' => $warnings,
|
||||
'complete' => $valid === $total,
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
public static function alerts(int $year): array
|
||||
{
|
||||
$applications = self::applications($year);
|
||||
$checklist = self::checklist($year);
|
||||
$today = date('Y-m-d');
|
||||
$soon = date('Y-m-d', strtotime('+30 days'));
|
||||
$deadlines = 0;
|
||||
$overdue = 0;
|
||||
$incomplete = 0;
|
||||
$documents = 0;
|
||||
foreach ($applications as $application) {
|
||||
if (in_array($application['status'], ['awarded', 'refused', 'withdrawn'], true)) {
|
||||
continue;
|
||||
}
|
||||
$deadline = (string) ($application['deadline'] ?? '');
|
||||
if ($deadline !== '' && $deadline < $today) {
|
||||
$overdue++;
|
||||
} elseif ($deadline !== '' && $deadline <= $soon) {
|
||||
$deadlines++;
|
||||
}
|
||||
if (!($checklist[(int) $application['id']]['complete'] ?? false)) {
|
||||
$incomplete++;
|
||||
}
|
||||
}
|
||||
foreach (self::documents($year) as $document) {
|
||||
$expiry = (string) ($document['valid_until'] ?? '');
|
||||
if ($expiry !== '' && $expiry <= $soon) {
|
||||
$documents++;
|
||||
}
|
||||
}
|
||||
return compact('deadlines', 'overdue', 'incomplete', 'documents');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue