diff --git a/src/Controller/FollowUpController.php b/src/Controller/FollowUpController.php index 5105e1b..44dd7b6 100644 --- a/src/Controller/FollowUpController.php +++ b/src/Controller/FollowUpController.php @@ -13,7 +13,7 @@ use Symfony\Component\Routing\Annotation\Route; class FollowUpController extends AbstractController { - #[Route('/admin/followup/{insee_code}/delete', name: 'admin_followup_delete')] + #[Route('/admin/followup/{insee_code}/delete', name: 'admin_followup_delete', requirements: ['insee_code' => '\\d+'])] public function deleteFollowups(string $insee_code, EntityManagerInterface $em): Response { $stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); if (!$stats) { @@ -29,7 +29,7 @@ class FollowUpController extends AbstractController return $this->redirectToRoute('admin_followup_graph', ['insee_code' => $insee_code]); } - #[Route('/admin/followup/{insee_code}', name: 'admin_followup')] + #[Route('/admin/followup/{insee_code}', name: 'admin_followup', requirements: ['insee_code' => '\\d+'])] public function followup( string $insee_code, Motocultrice $motocultrice, @@ -93,6 +93,21 @@ class FollowUpController extends AbstractController 'label' => 'Écoles', 'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'school') ], + 'police' => [ + 'label' => 'Commissariats', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'police') + ], + 'healthcare' => [ + 'label' => 'Lieux de santé', + 'objects' => array_filter($elements, function($el) { + return isset($el['tags']['healthcare']) + || ($el['tags']['amenity'] ?? null) === 'doctors' + || ($el['tags']['amenity'] ?? null) === 'pharmacy' + || ($el['tags']['amenity'] ?? null) === 'hospital' + || ($el['tags']['amenity'] ?? null) === 'clinic' + || ($el['tags']['amenity'] ?? null) === 'social_facility'; + }) + ], ]; $now = new \DateTime(); foreach ($types as $type => $data) { @@ -146,6 +161,19 @@ class FollowUpController extends AbstractController $completed = array_filter($data['objects'], function($el) { return !empty($el['tags']['ref:UAI'] ?? null) && !empty($el['tags']['isced:level'] ?? null) && !empty($el['tags']['school:FR'] ?? null); }); + } elseif ($type === 'police') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['phone'] ?? null) || !empty($el['tags']['website'] ?? null); + }); + } elseif ($type === 'healthcare') { + $completed = array_filter($data['objects'], function($el) { + $tags = $el['tags'] ?? []; + return !empty($tags['name'] ?? null) + || !empty($tags['contact:phone'] ?? null) + || !empty($tags['phone'] ?? null) + || !empty($tags['email'] ?? null) + || !empty($tags['contact:email'] ?? null); + }); } $completion = count($data['objects']) > 0 ? round(count($completed) / count($data['objects']) * 100) : 0; $followupCompletion = new CityFollowUp(); @@ -161,7 +189,7 @@ class FollowUpController extends AbstractController return $this->redirectToRoute('admin_followup_graph', ['insee_code' => $insee_code]); } - #[Route('/admin/followup/{insee_code}/graph', name: 'admin_followup_graph')] + #[Route('/admin/followup/{insee_code}/graph', name: 'admin_followup_graph', requirements: ['insee_code' => '\\d+'])] public function followupGraph( string $insee_code, EntityManagerInterface $em, @@ -219,6 +247,21 @@ class FollowUpController extends AbstractController 'label' => 'Écoles', 'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'school') ], + 'police' => [ + 'label' => 'Commissariats', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'police') + ], + 'healthcare' => [ + 'label' => 'Lieux de santé', + 'objects' => array_filter($elements, function($el) { + return isset($el['tags']['healthcare']) + || ($el['tags']['amenity'] ?? null) === 'doctors' + || ($el['tags']['amenity'] ?? null) === 'pharmacy' + || ($el['tags']['amenity'] ?? null) === 'hospital' + || ($el['tags']['amenity'] ?? null) === 'clinic' + || ($el['tags']['amenity'] ?? null) === 'social_facility'; + }) + ], ]; $now = new \DateTime(); foreach ($types as $type => $data) { @@ -270,6 +313,19 @@ class FollowUpController extends AbstractController $completed = array_filter($data['objects'], function($el) { return !empty($el['tags']['ref:UAI'] ?? null) && !empty($el['tags']['isced:level'] ?? null) && !empty($el['tags']['school:FR'] ?? null); }); + } elseif ($type === 'police') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['phone'] ?? null) || !empty($el['tags']['website'] ?? null); + }); + } elseif ($type === 'healthcare') { + $completed = array_filter($data['objects'], function($el) { + $tags = $el['tags'] ?? []; + return !empty($tags['name'] ?? null) + || !empty($tags['contact:phone'] ?? null) + || !empty($tags['phone'] ?? null) + || !empty($tags['email'] ?? null) + || !empty($tags['contact:email'] ?? null); + }); } $completion = count($data['objects']) > 0 ? round(count($completed) / count($data['objects']) * 100) : 0; $followupCompletion = new CityFollowUp(); @@ -356,6 +412,21 @@ class FollowUpController extends AbstractController 'label' => 'Écoles', 'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'school') ], + 'police' => [ + 'label' => 'Commissariats', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'police') + ], + 'healthcare' => [ + 'label' => 'Lieux de santé', + 'objects' => array_filter($elements, function($el) { + return isset($el['tags']['healthcare']) + || ($el['tags']['amenity'] ?? null) === 'doctors' + || ($el['tags']['amenity'] ?? null) === 'pharmacy' + || ($el['tags']['amenity'] ?? null) === 'hospital' + || ($el['tags']['amenity'] ?? null) === 'clinic' + || ($el['tags']['amenity'] ?? null) === 'social_facility'; + }) + ], ]; foreach ($types as $type => $data) { // Suivi du nombre @@ -406,6 +477,19 @@ class FollowUpController extends AbstractController $completed = array_filter($data['objects'], function($el) { return !empty($el['tags']['ref:UAI'] ?? null) && !empty($el['tags']['isced:level'] ?? null) && !empty($el['tags']['school:FR'] ?? null); }); + } elseif ($type === 'police') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['phone'] ?? null) || !empty($el['tags']['website'] ?? null); + }); + } elseif ($type === 'healthcare') { + $completed = array_filter($data['objects'], function($el) { + $tags = $el['tags'] ?? []; + return !empty($tags['name'] ?? null) + || !empty($tags['contact:phone'] ?? null) + || !empty($tags['phone'] ?? null) + || !empty($tags['email'] ?? null) + || !empty($tags['contact:email'] ?? null); + }); } $completion = count($data['objects']) > 0 ? round(count($completed) / count($data['objects']) * 100) : 0; $followupCompletion = new CityFollowUp(); @@ -434,4 +518,128 @@ class FollowUpController extends AbstractController $this->addFlash('success', 'Suivi généré pour toutes les villes.'); return $this->redirectToRoute('app_admin'); } + + #[Route('/admin/followup/global', name: 'admin_followup_global')] + public function followupGlobal(EntityManagerInterface $em) { + // Récupérer ou créer l'objet Stats global + $statsGlobal = $em->getRepository(Stats::class)->findOneBy(['zone' => '00000']); + if (!$statsGlobal) { + $statsGlobal = new Stats(); + $statsGlobal->setZone('00000'); + $statsGlobal->setName('toutes les villes'); + $em->persist($statsGlobal); + $em->flush(); + } + $now = new \DateTime(); + $themes = [ + 'fire_hydrant', 'charging_station', 'toilets', 'bus_stop', 'defibrillator', 'camera', 'recycling', 'substation', 'laboratory', 'school', 'police', 'healthcare', 'places' + ]; + $allStats = $em->getRepository(Stats::class)->findAll(); + // Exclure l'objet global + $allStats = array_filter($allStats, fn($s) => $s->getZone() !== '00000'); + $cityCount = count($allStats); + $globalCompletionSum = 0; + $globalCompletionCount = 0; + // Pour chaque thème, somme des counts et moyenne des completions + foreach ($themes as $theme) { + $sumCount = 0; + $sumCompletion = 0; + $nbCompletion = 0; + foreach ($allStats as $stats) { + $latestCount = null; + $latestCompletion = null; + foreach ($stats->getCityFollowUps() as $fu) { + if ($fu->getName() === $theme . '_count') { + if ($latestCount === null || $fu->getDate() > $latestCount->getDate()) { + $latestCount = $fu; + } + } + if ($fu->getName() === $theme . '_completion') { + if ($latestCompletion === null || $fu->getDate() > $latestCompletion->getDate()) { + $latestCompletion = $fu; + } + } + } + if ($latestCount) $sumCount += $latestCount->getMeasure(); + if ($latestCompletion) { + $sumCompletion += $latestCompletion->getMeasure(); + $nbCompletion++; + } + } + // Ajout du CityFollowUp global pour le count + $fuCount = new CityFollowUp(); + $fuCount->setName($theme . '_count') + ->setMeasure($sumCount) + ->setDate($now) + ->setStats($statsGlobal); + $em->persist($fuCount); + // Ajout du CityFollowUp global pour la complétion + $completionAvg = $nbCompletion > 0 ? round($sumCompletion / $nbCompletion, 1) : 0; + $fuCompletion = new CityFollowUp(); + $fuCompletion->setName($theme . '_completion') + ->setMeasure($completionAvg) + ->setDate($now) + ->setStats($statsGlobal); + $em->persist($fuCompletion); + // Pour la complétion globale moyenne + if ($theme !== 'places' && $nbCompletion > 0) { + $globalCompletionSum += $completionAvg; + $globalCompletionCount++; + } + } + // Suivi du nombre total de lieux (places_count) + $sumPlaces = 0; + foreach ($allStats as $stats) { + $sumPlaces += $stats->getPlacesCount() ?? 0; + } + $fuPlaces = new CityFollowUp(); + $fuPlaces->setName('places_count') + ->setMeasure($sumPlaces) + ->setDate($now) + ->setStats($statsGlobal); + $em->persist($fuPlaces); + // Suivi de la complétion globale moyenne + $globalCompletionAvg = $globalCompletionCount > 0 ? round($globalCompletionSum / $globalCompletionCount, 1) : 0; + $fuGlobalCompletion = new CityFollowUp(); + $fuGlobalCompletion->setName('global_completion_average') + ->setMeasure($globalCompletionAvg) + ->setDate($now) + ->setStats($statsGlobal); + $em->persist($fuGlobalCompletion); + // Suivi du nombre de villes + $fuCityCount = new CityFollowUp(); + $fuCityCount->setName('city_count') + ->setMeasure($cityCount) + ->setDate($now) + ->setStats($statsGlobal); + $em->persist($fuCityCount); + $em->flush(); + $this->addFlash('success', 'Suivi global généré pour toutes les villes.'); + return $this->redirectToRoute('admin_followup_global_graph'); + } + + #[Route('/admin/followup/global/graph', name: 'admin_followup_global_graph')] + public function followupGlobalGraph(EntityManagerInterface $em) { + $stats = $em->getRepository(Stats::class)->findOneBy(['zone' => '00000']); + if (!$stats) { + $this->addFlash('error', 'Aucun suivi global trouvé.'); + return $this->redirectToRoute('app_admin'); + } + $followups = $stats->getCityFollowUps(); + $followups = $followups->toArray(); + usort($followups, fn($a, $b) => $a->getDate() <=> $b->getDate()); + // Grouper par type + $series = []; + foreach ($followups as $fu) { + $series[$fu->getName()][] = [ + 'date' => $fu->getDate()->format('c'), + 'value' => $fu->getMeasure(), + 'name' => $fu->getName(), + ]; + } + return $this->render('admin/followup_global_graph.html.twig', [ + 'stats' => $stats, + 'series' => $series + ]); + } } \ No newline at end of file diff --git a/src/Service/Motocultrice.php b/src/Service/Motocultrice.php index 7313add..1561628 100644 --- a/src/Service/Motocultrice.php +++ b/src/Service/Motocultrice.php @@ -573,12 +573,19 @@ area["ref:INSEE"="$zone"]->.searchArea; nwr["man_made"="surveillance"](area.searchArea); nwr["amenity"="recycling"](area.searchArea); nwr["power"="substation"](area.searchArea); + nwr["healthcare"](area.searchArea); + nwr["amenity"="doctors"](area.searchArea); + nwr["amenity"="pharmacy"](area.searchArea); + nwr["amenity"="hospital"](area.searchArea); + nwr["amenity"="clinic"](area.searchArea); + nwr["amenity"="social_facility"](area.searchArea); nwr["healthcare"="laboratory"](area.searchArea); nwr["amenity"="school"](area.searchArea); + nwr["amenity"="police"](area.searchArea); ); -out body; +(._;>;); +out meta; >; -out skel qt; QUERY; } diff --git a/templates/admin/followup_global_graph.html.twig b/templates/admin/followup_global_graph.html.twig new file mode 100644 index 0000000..eebf0a4 --- /dev/null +++ b/templates/admin/followup_global_graph.html.twig @@ -0,0 +1,105 @@ +{% extends 'base.html.twig' %} +{% block title %}Suivi global de toutes les villes{% endblock %} +{% block body %} +
Historique des objets suivis (nombre et complétion).
{% set type_labels = { @@ -21,7 +27,9 @@ 'recycling': 'Points de recyclage', 'substation': 'Sous-stations électriques', 'laboratory': "Laboratoires d'analyse", - 'school': 'Écoles' + 'school': 'Écoles', + 'police': 'Commissariats', + 'healthcare': 'Lieux de santé' } %} {% for type in type_labels|keys %}Chaque critère rempli augmente le score de complétion d'une part égale. Un commerce parfaitement renseigné aura un score de 100%.
@@ -326,39 +443,7 @@