From 0cdb2f9ae98202ea670e7a85d84eff31952994c9 Mon Sep 17 00:00:00 2001 From: Tykayn Date: Sun, 29 Jun 2025 16:41:18 +0200 Subject: [PATCH] =?UTF-8?q?ajout=20de=20followup=20sur=20plusieurs=20th?= =?UTF-8?q?=C3=A8mes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/Version20250629142706.php | 35 +++ src/Controller/AdminController.php | 46 +++- src/Controller/FollowUpController.php | 305 ++++++++++++++++++++++- src/Service/Motocultrice.php | 8 +- templates/admin/followup_graph.html.twig | 224 +++++++++-------- templates/admin/stats.html.twig | 3 + 6 files changed, 501 insertions(+), 120 deletions(-) create mode 100644 migrations/Version20250629142706.php diff --git a/migrations/Version20250629142706.php b/migrations/Version20250629142706.php new file mode 100644 index 00000000..63f5a2b8 --- /dev/null +++ b/migrations/Version20250629142706.php @@ -0,0 +1,35 @@ +addSql(<<<'SQL' + ALTER TABLE place CHANGE email email VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note note VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note_content note_content VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE street street VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE housenumber housenumber VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE siret siret VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE osm_user osm_user VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE email_content email_content LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci` + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE place CHANGE email email VARCHAR(255) DEFAULT NULL, CHANGE note note VARCHAR(255) DEFAULT NULL, CHANGE name name VARCHAR(255) DEFAULT NULL, CHANGE note_content note_content VARCHAR(255) DEFAULT NULL, CHANGE street street VARCHAR(255) DEFAULT NULL, CHANGE housenumber housenumber VARCHAR(255) DEFAULT NULL, CHANGE siret siret VARCHAR(255) DEFAULT NULL, CHANGE osm_user osm_user VARCHAR(255) DEFAULT NULL, CHANGE email_content email_content LONGTEXT DEFAULT NULL + SQL); + } +} diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php index ea19aa14..8c5c03e2 100644 --- a/src/Controller/AdminController.php +++ b/src/Controller/AdminController.php @@ -374,6 +374,49 @@ final class AdminController extends AbstractController return ($b['completion_pondere_normalisee'] ?? 0) <=> ($a['completion_pondere_normalisee'] ?? 0); }); + // Récupérer les derniers followups pour chaque type + $latestFollowups = []; + $types = [ + 'fire_hydrant', 'charging_station', 'toilets', 'bus_stop', 'defibrillator', 'camera', 'recycling', 'substation', 'places' + ]; + foreach ($types as $type) { + $count = null; + $completion = null; + foreach ($stats->getCityFollowUps() as $fu) { + if ($fu->getName() === $type . '_count') { + if ($count === null || $fu->getDate() > $count->getDate()) { + $count = $fu; + } + } + if ($fu->getName() === $type . '_completion') { + if ($completion === null || $fu->getDate() > $completion->getDate()) { + $completion = $fu; + } + } + } + $latestFollowups[$type] = []; + if ($count) $latestFollowups[$type]['count'] = $count; + if ($completion) $latestFollowups[$type]['completion'] = $completion; + } + // Pour les lieux (places_count et places_completion) + $count = null; + $completion = null; + foreach ($stats->getCityFollowUps() as $fu) { + if ($fu->getName() === 'places_count') { + if ($count === null || $fu->getDate() > $count->getDate()) { + $count = $fu; + } + } + if ($fu->getName() === 'places_completion') { + if ($completion === null || $fu->getDate() > $completion->getDate()) { + $completion = $fu; + } + } + } + $latestFollowups['places'] = []; + if ($count) $latestFollowups['places']['count'] = $count; + if ($completion) $latestFollowups['places']['completion'] = $completion; + return $this->render('admin/stats.html.twig', [ 'stats' => $stats, 'commerces' => $commerces, @@ -384,7 +427,8 @@ final class AdminController extends AbstractController 'statsHistory' => $statsHistory, 'CTC_urls' => $urls, 'overpass' => '', - 'podium_local' => $podium_local + 'podium_local' => $podium_local, + 'latestFollowups' => $latestFollowups ]); } diff --git a/src/Controller/FollowUpController.php b/src/Controller/FollowUpController.php index ff8df33a..8a13e068 100644 --- a/src/Controller/FollowUpController.php +++ b/src/Controller/FollowUpController.php @@ -13,6 +13,22 @@ use Symfony\Component\Routing\Annotation\Route; class FollowUpController extends AbstractController { + #[Route('/admin/followup/{insee_code}/delete', name: 'admin_followup_delete')] + public function deleteFollowups(string $insee_code, EntityManagerInterface $em): Response { + $stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); + if (!$stats) { + $this->addFlash('error', 'Aucune stats trouvée pour ce code INSEE.'); + return $this->redirectToRoute('app_admin'); + } + $followups = $stats->getCityFollowUps(); + foreach ($followups as $fu) { + $em->remove($fu); + } + $em->flush(); + $this->addFlash('success', 'Tous les suivis ont été supprimés pour cette ville.'); + return $this->redirectToRoute('admin_followup_graph', ['insee_code' => $insee_code]); + } + #[Route('/admin/followup/{insee_code}', name: 'admin_followup')] public function followup( string $insee_code, @@ -25,26 +41,52 @@ class FollowUpController extends AbstractController $this->addFlash('error', 'Aucune stats trouvée pour ce code INSEE.'); return $this->redirectToRoute('app_admin'); } + // Ne plus supprimer les anciens suivis ! + // $followups = $stats->getCityFollowUps(); + // foreach ($followups as $fu) { + // $em->remove($fu); + // } + // $em->flush(); // Récupérer les objets OSM $elements = $motocultrice->followUpCity($insee_code); // Séparer les objets par type - $fire_hydrants = array_filter($elements, fn($el) => ($el['tags']['emergency'] ?? null) === 'fire_hydrant'); - $charging_stations = array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'charging_station'); - - // --- Suivi du nombre d'objets --- - $now = new \DateTime(); $types = [ 'fire_hydrant' => [ 'label' => 'Bornes incendie', - 'objects' => $fire_hydrants + 'objects' => array_filter($elements, fn($el) => ($el['tags']['emergency'] ?? null) === 'fire_hydrant') ], 'charging_station' => [ 'label' => 'Bornes de recharge', - 'objects' => $charging_stations - ] + 'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'charging_station') + ], + 'toilets' => [ + 'label' => 'Toilettes publiques', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'toilets') + ], + 'bus_stop' => [ + 'label' => 'Arrêts de bus', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['highway'] ?? null) === 'bus_stop') + ], + 'defibrillator' => [ + 'label' => 'Défibrillateurs', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['emergency'] ?? null) === 'defibrillator') + ], + 'camera' => [ + 'label' => 'Caméras de surveillance', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['man_made'] ?? null) === 'surveillance') + ], + 'recycling' => [ + 'label' => 'Points de recyclage', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'recycling') + ], + 'substation' => [ + 'label' => 'Sous-stations électriques', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['power'] ?? null) === 'substation') + ], ]; + $now = new \DateTime(); foreach ($types as $type => $data) { // Suivi du nombre $followupCount = new CityFollowUp(); @@ -54,7 +96,8 @@ class FollowUpController extends AbstractController ->setStats($stats); $em->persist($followupCount); - // Suivi de la complétion personnalisé + // Suivi de la complétion personnalisé (exemples) + $completed = []; if ($type === 'fire_hydrant') { $completed = array_filter($data['objects'], function($el) { return !empty($el['tags']['ref'] ?? null); @@ -63,8 +106,30 @@ class FollowUpController extends AbstractController $completed = array_filter($data['objects'], function($el) { return !empty($el['tags']['charging_station:output'] ?? null) && !empty($el['tags']['capacity'] ?? null); }); - } else { - $completed = []; + } elseif ($type === 'toilets') { + $completed = array_filter($data['objects'], function($el) { + return ($el['tags']['wheelchair'] ?? null) === 'yes'; + }); + } elseif ($type === 'bus_stop') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['shelter'] ?? null); + }); + } elseif ($type === 'defibrillator') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['indoor'] ?? null); + }); + } elseif ($type === 'camera') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['surveillance:type'] ?? null); + }); + } elseif ($type === 'recycling') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['recycling_type'] ?? null); + }); + } elseif ($type === 'substation') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['substation'] ?? null); + }); } $completion = count($data['objects']) > 0 ? round(count($completed) / count($data['objects']) * 100) : 0; $followupCompletion = new CityFollowUp(); @@ -83,14 +148,109 @@ class FollowUpController extends AbstractController #[Route('/admin/followup/{insee_code}/graph', name: 'admin_followup_graph')] public function followupGraph( string $insee_code, - EntityManagerInterface $em - ): Response { + EntityManagerInterface $em, + Motocultrice $motocultrice + ) { $stats = $em->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); if (!$stats) { $this->addFlash('error', 'Aucune stats trouvée pour ce code INSEE.'); return $this->redirectToRoute('app_admin'); } $followups = $stats->getCityFollowUps(); + if ($followups->isEmpty()) { + // Générer les followup comme dans l'action followup + // (ne pas supprimer les anciens, car il n'y en a pas) + $elements = $motocultrice->followUpCity($insee_code); + // Séparer les objets par type + $types = [ + 'fire_hydrant' => [ + 'label' => 'Bornes incendie', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['emergency'] ?? null) === 'fire_hydrant') + ], + 'charging_station' => [ + 'label' => 'Bornes de recharge', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'charging_station') + ], + 'toilets' => [ + 'label' => 'Toilettes publiques', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'toilets') + ], + 'bus_stop' => [ + 'label' => 'Arrêts de bus', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['highway'] ?? null) === 'bus_stop') + ], + 'defibrillator' => [ + 'label' => 'Défibrillateurs', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['emergency'] ?? null) === 'defibrillator') + ], + 'camera' => [ + 'label' => 'Caméras de surveillance', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['man_made'] ?? null) === 'surveillance') + ], + 'recycling' => [ + 'label' => 'Points de recyclage', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'recycling') + ], + 'substation' => [ + 'label' => 'Sous-stations électriques', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['power'] ?? null) === 'substation') + ], + ]; + $now = new \DateTime(); + foreach ($types as $type => $data) { + // Suivi du nombre + $followupCount = new CityFollowUp(); + $followupCount->setName($type . '_count') + ->setMeasure(count($data['objects'])) + ->setDate($now) + ->setStats($stats); + $em->persist($followupCount); + // Suivi de la complétion personnalisé (exemples) + $completed = []; + if ($type === 'fire_hydrant') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['ref'] ?? null); + }); + } elseif ($type === 'charging_station') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['charging_station:output'] ?? null) && !empty($el['tags']['capacity'] ?? null); + }); + } elseif ($type === 'toilets') { + $completed = array_filter($data['objects'], function($el) { + return ($el['tags']['wheelchair'] ?? null) === 'yes'; + }); + } elseif ($type === 'bus_stop') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['shelter'] ?? null); + }); + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['indoor'] ?? null); + }); + } elseif ($type === 'camera') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['surveillance:type'] ?? null); + }); + } elseif ($type === 'recycling') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['recycling_type'] ?? null); + }); + } elseif ($type === 'substation') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['substation'] ?? null); + }); + } + $completion = count($data['objects']) > 0 ? round(count($completed) / count($data['objects']) * 100) : 0; + $followupCompletion = new CityFollowUp(); + $followupCompletion->setName($type . '_completion') + ->setMeasure($completion) + ->setDate($now) + ->setStats($stats); + $em->persist($followupCompletion); + } + $em->flush(); + // Recharger les followups + $followups = $stats->getCityFollowUps(); + } $followups = $followups->toArray(); usort($followups, fn($a, $b) => $a->getDate() <=> $b->getDate()); // Grouper par type @@ -107,4 +267,123 @@ class FollowUpController extends AbstractController 'series' => $series ]); } + + #[Route('/admin/followup/all', name: 'admin_followup_all')] + public function followupAll( + EntityManagerInterface $em, + Motocultrice $motocultrice + ) { + $statsList = $em->getRepository(Stats::class)->findAll(); + $now = new \DateTime(); + foreach ($statsList as $stats) { + // Ne plus supprimer les anciens suivis ! + // foreach ($stats->getCityFollowUps() as $fu) { + // $em->remove($fu); + // } + // Générer les followups OSM + $insee_code = $stats->getZone(); + $elements = $motocultrice->followUpCity($insee_code); + $types = [ + 'fire_hydrant' => [ + 'label' => 'Bornes incendie', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['emergency'] ?? null) === 'fire_hydrant') + ], + 'charging_station' => [ + 'label' => 'Bornes de recharge', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'charging_station') + ], + 'toilets' => [ + 'label' => 'Toilettes publiques', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'toilets') + ], + 'bus_stop' => [ + 'label' => 'Arrêts de bus', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['highway'] ?? null) === 'bus_stop') + ], + 'defibrillator' => [ + 'label' => 'Défibrillateurs', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['emergency'] ?? null) === 'defibrillator') + ], + 'camera' => [ + 'label' => 'Caméras de surveillance', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['man_made'] ?? null) === 'surveillance') + ], + 'recycling' => [ + 'label' => 'Points de recyclage', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'recycling') + ], + 'substation' => [ + 'label' => 'Sous-stations électriques', + 'objects' => array_filter($elements, fn($el) => ($el['tags']['power'] ?? null) === 'substation') + ], + ]; + foreach ($types as $type => $data) { + // Suivi du nombre + $followupCount = new CityFollowUp(); + $followupCount->setName($type . '_count') + ->setMeasure(count($data['objects'])) + ->setDate($now) + ->setStats($stats); + $em->persist($followupCount); + // Suivi de la complétion personnalisé (exemples) + $completed = []; + if ($type === 'fire_hydrant') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['ref'] ?? null); + }); + } elseif ($type === 'charging_station') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['charging_station:output'] ?? null) && !empty($el['tags']['capacity'] ?? null); + }); + } elseif ($type === 'toilets') { + $completed = array_filter($data['objects'], function($el) { + return ($el['tags']['wheelchair'] ?? null) === 'yes'; + }); + } elseif ($type === 'bus_stop') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['shelter'] ?? null); + }); + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['indoor'] ?? null); + }); + } elseif ($type === 'camera') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['surveillance:type'] ?? null); + }); + } elseif ($type === 'recycling') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['recycling_type'] ?? null); + }); + } elseif ($type === 'substation') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['substation'] ?? null); + }); + } + $completion = count($data['objects']) > 0 ? round(count($completed) / count($data['objects']) * 100) : 0; + $followupCompletion = new CityFollowUp(); + $followupCompletion->setName($type . '_completion') + ->setMeasure($completion) + ->setDate($now) + ->setStats($stats); + $em->persist($followupCompletion); + } + // Ajout du suivi sur le nombre de Places + $followupPlaces = new CityFollowUp(); + $followupPlaces->setName('places_count') + ->setMeasure($stats->getPlacesCount() ?? 0) + ->setDate($now) + ->setStats($stats); + $em->persist($followupPlaces); + // Ajout du suivi sur la complétion moyenne + $followupCompletion = new CityFollowUp(); + $followupCompletion->setName('places_completion') + ->setMeasure($stats->getCompletionPercent() ?? 0) + ->setDate($now) + ->setStats($stats); + $em->persist($followupCompletion); + } + $em->flush(); + $this->addFlash('success', 'Suivi généré pour toutes les villes.'); + return $this->redirectToRoute('app_admin'); + } } \ No newline at end of file diff --git a/src/Service/Motocultrice.php b/src/Service/Motocultrice.php index 051563de..b8fe30b3 100644 --- a/src/Service/Motocultrice.php +++ b/src/Service/Motocultrice.php @@ -558,7 +558,7 @@ out meta;'; } /** - * Génère la requête Overpass pour les bornes incendie et bornes de recharge + * Génère la requête Overpass pour tous les objets de suivi (thématiques étendues) */ public function get_followup_query($zone) { return <<.searchArea; ( nwr["emergency"="fire_hydrant"](area.searchArea); nwr["amenity"="charging_station"](area.searchArea); + nwr["amenity"="toilets"](area.searchArea); + nwr["highway"="bus_stop"](area.searchArea); + nwr["emergency"="defibrillator"](area.searchArea); + nwr["man_made"="surveillance"](area.searchArea); + nwr["amenity"="recycling"](area.searchArea); + nwr["power"="substation"](area.searchArea); ); out body; >; diff --git a/templates/admin/followup_graph.html.twig b/templates/admin/followup_graph.html.twig index 5bf77587..6c26b4b7 100644 --- a/templates/admin/followup_graph.html.twig +++ b/templates/admin/followup_graph.html.twig @@ -5,14 +5,26 @@ {% block body %}

Suivi des objets OSM pour {{ stats.name }} ({{ stats.zone }})

-

Historique des bornes incendie et bornes de recharge (nombre et complétion).

- -

Bornes de recharge

- - -

Bornes incendie

- - + +

Historique des objets suivis (nombre et complétion).

+ {% set type_labels = { + 'fire_hydrant': 'Bornes incendie', + 'charging_station': 'Bornes de recharge', + 'toilets': 'Toilettes publiques', + 'bus_stop': 'Arrêts de bus', + 'defibrillator': 'Défibrillateurs', + 'camera': 'Caméras de surveillance', + 'recycling': 'Points de recyclage', + 'substation': 'Sous-stations électriques' + } %} + {% for type in type_labels|keys %} +

{{ type_labels[type] }}

+ + {% endfor %}

Données brutes

@@ -36,7 +48,6 @@ {% endfor %}
- Retour à la fiche ville
{% endblock %} @@ -46,105 +57,108 @@ {% endblock %} \ No newline at end of file diff --git a/templates/admin/stats.html.twig b/templates/admin/stats.html.twig index 07e569d4..35922f44 100644 --- a/templates/admin/stats.html.twig +++ b/templates/admin/stats.html.twig @@ -55,6 +55,9 @@