diff --git a/labourage.sh b/labourage.sh index 0c7180f2..ea9ee81f 100644 --- a/labourage.sh +++ b/labourage.sh @@ -238,4 +238,7 @@ for code in "${codes_insee[@]}"; do sleep 5 done -echo "Traitement terminé" +echo "Traitement terminé pour les codes insee" + +curl -X GET "https://osm-commerces.cipherbliss.com/https://127.0.0.1:8000/admin/followup/global" + diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php index 8c5c03e2..933a5164 100644 --- a/src/Controller/AdminController.php +++ b/src/Controller/AdminController.php @@ -19,17 +19,23 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Twig\Environment; use App\Service\ActionLogger; use DateTime; +use App\Service\FollowUpService; final class AdminController extends AbstractController { + private FollowUpService $followUpService; + public function __construct( private EntityManagerInterface $entityManager, private Motocultrice $motocultrice, private BudgetService $budgetService, private Environment $twig, - private ActionLogger $actionLogger - ) {} + private ActionLogger $actionLogger, + FollowUpService $followUpService + ) { + $this->followUpService = $followUpService; + } #[Route('/admin/labourer-toutes-les-zones', name: 'app_admin_labourer_tout')] @@ -230,6 +236,9 @@ final class AdminController extends AbstractController } $this->entityManager->flush(); + // Générer les suivis (followups) après la mise à jour des Places + $this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager); + $message = 'Labourage terminé avec succès. ' . $processedCount . ' nouveaux lieux traités.'; if ($updateExisting) { $message .= ' ' . $updatedCount . ' lieux existants mis à jour pour la zone ' . $stats->getName() . ' (' . $stats->getZone() . ').'; @@ -239,8 +248,6 @@ final class AdminController extends AbstractController $this->entityManager->flush(); - $this->entityManager->flush(); - $this->addFlash('success', 'Labourage des ' . count($stats_all) . ' zones terminé avec succès.'); return $this->redirectToRoute('app_public_dashboard'); } @@ -256,15 +263,17 @@ final class AdminController extends AbstractController #[Route('/admin/stats/{insee_code}', name: 'app_admin_stats')] public function calculer_stats(string $insee_code): Response { - // Récupérer les stats existantes pour la zone $stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); if (!$stats) { // Si aucune statistique n'existe pour cette zone, rediriger vers le labourage de la zone return $this->redirectToRoute('app_admin_labourer', ['insee_code' => $insee_code]); } + // Si aucun followup n'existe, on les régénère automatiquement + if ($stats->getCityFollowUps()->isEmpty()) { + $this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager); + } $commerces = $stats->getPlaces(); - $this->actionLogger->log('stats_de_ville', ['insee_code' => $insee_code, 'nom' => $stats->getZone()]); // Récupérer tous les commerces de la zone // $commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code, 'dead' => false]); @@ -428,7 +437,9 @@ final class AdminController extends AbstractController 'CTC_urls' => $urls, 'overpass' => '', 'podium_local' => $podium_local, - 'latestFollowups' => $latestFollowups + 'latestFollowups' => $latestFollowups, + 'followup_labels' => \App\Service\FollowUpService::getFollowUpThemes(), + 'followup_icons' => \App\Service\FollowUpService::getFollowUpIcons(), ]); } @@ -858,6 +869,9 @@ final class AdminController extends AbstractController $this->entityManager->persist($stats); $this->entityManager->flush(); + // Générer les suivis (followups) après la mise à jour des Places + $this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager); + $message = 'Labourage terminé avec succès. ' . $processedCount . ' nouveaux lieux traités.'; if ($updateExisting) { $message .= ' ' . $updatedCount . ' lieux existants mis à jour.'; diff --git a/src/Controller/FollowUpController.php b/src/Controller/FollowUpController.php index 44dd7b6c..0bb220df 100644 --- a/src/Controller/FollowUpController.php +++ b/src/Controller/FollowUpController.php @@ -5,6 +5,7 @@ namespace App\Controller; use App\Entity\Stats; use App\Entity\CityFollowUp; use App\Service\Motocultrice; +use App\Service\FollowUpService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -13,6 +14,13 @@ use Symfony\Component\Routing\Annotation\Route; class FollowUpController extends AbstractController { + private FollowUpService $followUpService; + + public function __construct(FollowUpService $followUpService) + { + $this->followUpService = $followUpService; + } + #[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]); @@ -35,156 +43,12 @@ class FollowUpController extends AbstractController Motocultrice $motocultrice, EntityManagerInterface $em ): Response { - // Récupérer la stats de la ville $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'); } - // 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 - $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') - ], - 'laboratory' => [ - 'label' => 'Laboratoires d\'analyse', - 'objects' => array_filter($elements, fn($el) => ($el['tags']['healthcare'] ?? null) === 'laboratory') - ], - 'school' => [ - '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) { - // 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); - }); - } 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); - }); - } elseif ($type === 'laboratory') { - $completed = array_filter($data['objects'], function($el) { - return !empty($el['tags']['website'] ?? null) || !empty($el['tags']['contact:website'] ?? null); - }); - } elseif ($type === 'school') { - $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(); - $followupCompletion->setName($type . '_completion') - ->setMeasure($completion) - ->setDate($now) - ->setStats($stats); - $em->persist($followupCompletion); - } - $em->flush(); - + $this->followUpService->generateCityFollowUps($stats, $motocultrice, $em); $this->addFlash('success', 'Suivi enregistré pour la ville.'); return $this->redirectToRoute('admin_followup_graph', ['insee_code' => $insee_code]); } @@ -202,146 +66,11 @@ class FollowUpController extends AbstractController } $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') - ], - 'laboratory' => [ - 'label' => 'Laboratoires d\'analyse', - 'objects' => array_filter($elements, fn($el) => ($el['tags']['healthcare'] ?? null) === 'laboratory') - ], - 'school' => [ - '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) { - // 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); - }); - } elseif ($type === 'laboratory') { - $completed = array_filter($data['objects'], function($el) { - return !empty($el['tags']['website'] ?? null) || !empty($el['tags']['contact:website'] ?? null); - }); - } elseif ($type === 'school') { - $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(); - $followupCompletion->setName($type . '_completion') - ->setMeasure($completion) - ->setDate($now) - ->setStats($stats); - $em->persist($followupCompletion); - } - $em->flush(); - // Recharger les followups + $this->followUpService->generateCityFollowUps($stats, $motocultrice, $em); $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()][] = [ @@ -352,7 +81,10 @@ class FollowUpController extends AbstractController } return $this->render('admin/followup_graph.html.twig', [ 'stats' => $stats, - 'series' => $series + 'series' => $series, + 'followup_labels' => FollowUpService::getFollowUpThemes(), + 'followup_icons' => FollowUpService::getFollowUpIcons(), + 'followup_overpass' => FollowUpService::getFollowUpOverpassQueries(), ]); } @@ -362,258 +94,16 @@ class FollowUpController extends AbstractController 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') - ], - 'laboratory' => [ - 'label' => 'Laboratoires d\'analyse', - 'objects' => array_filter($elements, fn($el) => ($el['tags']['healthcare'] ?? null) === 'laboratory') - ], - 'school' => [ - '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 - $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); - }); - } elseif ($type === 'laboratory') { - $completed = array_filter($data['objects'], function($el) { - return !empty($el['tags']['website'] ?? null) || !empty($el['tags']['contact:website'] ?? null); - }); - } elseif ($type === 'school') { - $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(); - $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); + $this->followUpService->generateCityFollowUps($stats, $motocultrice, $em); } - $em->flush(); $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->followUpService->generateGlobalFollowUps($em); $this->addFlash('success', 'Suivi global généré pour toutes les villes.'); return $this->redirectToRoute('admin_followup_global_graph'); } @@ -628,7 +118,6 @@ class FollowUpController extends AbstractController $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()][] = [ @@ -639,7 +128,10 @@ class FollowUpController extends AbstractController } return $this->render('admin/followup_global_graph.html.twig', [ 'stats' => $stats, - 'series' => $series + 'series' => $series, + 'followup_labels' => FollowUpService::getFollowUpThemes(), + 'followup_icons' => FollowUpService::getFollowUpIcons(), + 'followup_overpass' => FollowUpService::getFollowUpOverpassQueries(), ]); } } \ No newline at end of file diff --git a/src/Service/FollowUpService.php b/src/Service/FollowUpService.php new file mode 100644 index 00000000..6a8a894e --- /dev/null +++ b/src/Service/FollowUpService.php @@ -0,0 +1,275 @@ +getZone(); + $elements = $motocultrice->followUpCity($insee_code) ?? []; + $themes = self::getFollowUpThemes(); + $types = []; + foreach ($themes as $type => $label) { + if ($type === 'fire_hydrant') { + $objects = array_filter($elements, fn($el) => ($el['tags']['emergency'] ?? null) === 'fire_hydrant') ?? []; + } elseif ($type === 'charging_station') { + $objects = array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'charging_station') ?? []; + } elseif ($type === 'toilets') { + $objects = array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'toilets') ?? []; + } elseif ($type === 'bus_stop') { + $objects = array_filter($elements, fn($el) => ($el['tags']['highway'] ?? null) === 'bus_stop') ?? []; + } elseif ($type === 'defibrillator') { + $objects = array_filter($elements, fn($el) => ($el['tags']['emergency'] ?? null) === 'defibrillator') ?? []; + } elseif ($type === 'camera') { + $objects = array_filter($elements, fn($el) => ($el['tags']['man_made'] ?? null) === 'surveillance') ?? []; + } elseif ($type === 'recycling') { + $objects = array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'recycling') ?? []; + } elseif ($type === 'substation') { + $objects = array_filter($elements, fn($el) => ($el['tags']['power'] ?? null) === 'substation') ?? []; + } elseif ($type === 'laboratory') { + $objects = array_filter($elements, fn($el) => ($el['tags']['healthcare'] ?? null) === 'laboratory') ?? []; + } elseif ($type === 'school') { + $objects = array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'school') ?? []; + } elseif ($type === 'police') { + $objects = array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'police') ?? []; + } elseif ($type === 'healthcare') { + $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'; + }) ?? []; + } else { + $objects = []; + } + $types[$type] = [ + 'label' => $label, + 'objects' => $objects + ]; + } + $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); + }); + } 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); + }); + } elseif ($type === 'laboratory') { + $completed = array_filter($data['objects'], function($el) { + return !empty($el['tags']['website'] ?? null) || !empty($el['tags']['contact:website'] ?? null); + }); + } elseif ($type === 'school') { + $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(); + $followupCompletion->setName($type . '_completion') + ->setMeasure($completion) + ->setDate($now) + ->setStats($stats); + $em->persist($followupCompletion); + } + $em->flush(); + } + + public function generateGlobalFollowUps(EntityManagerInterface $em): void + { + $statsGlobal = $em->getRepository(\App\Entity\Stats::class)->findOneBy(['zone' => '00000']); + if (!$statsGlobal) { + $statsGlobal = new \App\Entity\Stats(); + $statsGlobal->setZone('00000'); + $statsGlobal->setName('toutes les villes'); + $em->persist($statsGlobal); + $em->flush(); + } + $now = new \DateTime(); + $themes = array_keys(self::getFollowUpThemes()); + $allStats = $em->getRepository(\App\Entity\Stats::class)->findAll(); + $allStats = array_filter($allStats, fn($s) => $s->getZone() !== '00000'); + $cityCount = count($allStats); + $globalCompletionSum = 0; + $globalCompletionCount = 0; + 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++; + } + } + $fuCount = new \App\Entity\CityFollowUp(); + $fuCount->setName($theme . '_count') + ->setMeasure($sumCount) + ->setDate($now) + ->setStats($statsGlobal); + $em->persist($fuCount); + $completionAvg = $nbCompletion > 0 ? round($sumCompletion / $nbCompletion, 1) : 0; + $fuCompletion = new \App\Entity\CityFollowUp(); + $fuCompletion->setName($theme . '_completion') + ->setMeasure($completionAvg) + ->setDate($now) + ->setStats($statsGlobal); + $em->persist($fuCompletion); + if ($theme !== 'places' && $nbCompletion > 0) { + $globalCompletionSum += $completionAvg; + $globalCompletionCount++; + } + } + $sumPlaces = 0; + foreach ($allStats as $stats) { + $sumPlaces += $stats->getPlacesCount() ?? 0; + } + $fuPlaces = new \App\Entity\CityFollowUp(); + $fuPlaces->setName('places_count') + ->setMeasure($sumPlaces) + ->setDate($now) + ->setStats($statsGlobal); + $em->persist($fuPlaces); + $globalCompletionAvg = $globalCompletionCount > 0 ? round($globalCompletionSum / $globalCompletionCount, 1) : 0; + $fuGlobalCompletion = new \App\Entity\CityFollowUp(); + $fuGlobalCompletion->setName('global_completion_average') + ->setMeasure($globalCompletionAvg) + ->setDate($now) + ->setStats($statsGlobal); + $em->persist($fuGlobalCompletion); + $fuCityCount = new \App\Entity\CityFollowUp(); + $fuCityCount->setName('city_count') + ->setMeasure($cityCount) + ->setDate($now) + ->setStats($statsGlobal); + $em->persist($fuCityCount); + $em->flush(); + } + + public static function getFollowUpThemes(): array + { + return [ + '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', + 'laboratory' => "Laboratoires d'analyse", + 'school' => 'Écoles', + 'police' => 'Commissariats', + 'healthcare' => 'Lieux de santé', + 'places' => 'Lieux' + ]; + } + + public static function getFollowUpIcons(): array + { + return [ + 'fire_hydrant' => 'bi-droplet', + 'charging_station' => 'bi-lightning-charge', + 'toilets' => 'bi-toilet', + 'bus_stop' => 'bi-bus-front', + 'defibrillator' => 'bi-heart-pulse', + 'camera' => 'bi-camera-video', + 'recycling' => 'bi-recycle', + 'substation' => 'bi-plug', + 'laboratory' => 'bi-beaker', + 'school' => 'bi-mortarboard', + 'police' => 'bi-shield-lock', + 'healthcare' => 'bi-hospital', + 'places' => 'bi-geo-alt' + ]; + } + + public static function getFollowUpOverpassQueries(): array + { + return [ + 'fire_hydrant' => 'nwr["emergency"="fire_hydrant"](area.searchArea);', + 'charging_station' => 'nwr["amenity"="charging_station"](area.searchArea);', + 'toilets' => 'nwr["amenity"="toilets"](area.searchArea);', + 'bus_stop' => 'nwr["highway"="bus_stop"](area.searchArea);', + 'defibrillator' => 'nwr["emergency"="defibrillator"](area.searchArea);', + 'camera' => 'nwr["man_made"="surveillance"](area.searchArea);', + 'recycling' => 'nwr["amenity"="recycling"](area.searchArea);', + 'substation' => 'nwr["power"="substation"](area.searchArea);', + 'laboratory' => 'nwr["healthcare"="laboratory"](area.searchArea);', + 'school' => 'nwr["amenity"="school"](area.searchArea);', + 'police' => 'nwr["amenity"="police"](area.searchArea);', + 'healthcare' => '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);', + 'places' => '' + ]; + } +} \ No newline at end of file diff --git a/templates/admin/followup_global_graph.html.twig b/templates/admin/followup_global_graph.html.twig index 0ff52b3a..2f8430c5 100644 --- a/templates/admin/followup_global_graph.html.twig +++ b/templates/admin/followup_global_graph.html.twig @@ -15,34 +15,16 @@
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', - 'laboratory': "Laboratoires d'analyse", - 'school': 'Écoles', - 'police': 'Commissariats', - 'healthcare': 'Lieux de santé' - } %} - {% for type in type_labels|keys %} -