entityManager->getRepository(Stats::class)->findAll(); echo 'on a trouvé ' . count($stats_all) . ' zones à labourer
'; foreach($stats_all as $stats) { echo '
on laboure la zone '.$stats->getZone() . ' '; $processedCount = 0; $updatedCount = 0; $insee_code = $stats->getZone(); // Vérifier si le code INSEE est un nombre valide // Vérifier si les stats ont été modifiées il y a moins de 24h if ($stats->getDateModified() !== null) { $now = new \DateTime(); $diff = $now->diff($stats->getDateModified()); $hours = $diff->h + ($diff->days * 24); if ($hours < 24) { echo 'Stats modifiées il y a moins de 24h - on passe au suivant
'; continue; } } if (!is_numeric($insee_code) || $insee_code == 'undefined' || $insee_code == '') { echo 'Code INSEE invalide : ' . $insee_code . ' - on passe au suivant
'; continue; } $places_overpass = $this->motocultrice->labourer($stats->getZone()); $places = $places_overpass; foreach ($places as $placeData) { // Vérifier si le lieu existe déjà $existingPlace = $this->entityManager->getRepository(Place::class) ->findOneBy(['osmId' => $placeData['id']]); if (!$existingPlace) { $place = new Place(); $place->setOsmId($placeData['id']) ->setOsmKind($placeData['type']) ->setZipCode($insee_code) ->setUuidForUrl($this->motocultrice->uuid_create()) ->setModifiedDate(new \DateTime()) ->setStats($stats) ->setDead(false) ->setOptedOut(false) ->setMainTag($this->motocultrice->find_main_tag($placeData['tags']) ?? '') ->setStreet($this->motocultrice->find_street($placeData['tags']) ?? '') ->setHousenumber($this->motocultrice->find_housenumber($placeData['tags']) ?? '') ->setSiret($this->motocultrice->find_siret($placeData['tags']) ?? '') ->setAskedHumainsSupport(false) ->setLastContactAttemptDate(null) ->setNote($this->motocultrice->find_tag($placeData['tags'], 'note') ? true : false) ->setNoteContent($this->motocultrice->find_tag($placeData['tags'], 'note') ?? '') ->setPlaceCount(0) // ->setOsmData($placeData['modified'] ?? null) ; // Mettre à jour les données depuis Overpass $place->update_place_from_overpass_data($placeData); $this->entityManager->persist($place); $stats->addPlace($place); $processedCount++; } elseif ($updateExisting) { // Mettre à jour les données depuis Overpass uniquement si updateExisting est true $existingPlace->update_place_from_overpass_data($placeData); $stats->addPlace($existingPlace); $this->entityManager->persist($existingPlace); $updatedCount++; } } // mettre à jour les stats // Récupérer tous les commerces de la zone $commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]); // Récupérer les stats existantes pour la zone $stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); if(!$stats) { $stats = new Stats(); $stats->setZone($insee_code); } $urls = $stats->getAllCTCUrlsMap(); $statsHistory = $this->entityManager->getRepository(StatsHistory::class) ->createQueryBuilder('sh') ->where('sh.stats = :stats') ->setParameter('stats', $stats) ->orderBy('sh.id', 'DESC') ->setMaxResults(365) ->getQuery() ->getResult(); // Calculer les statistiques $calculatedStats = $this->motocultrice->calculateStats($commerces); // Mettre à jour les stats pour la zone donnée $stats->setPlacesCount($calculatedStats['places_count']); $stats->setAvecHoraires($calculatedStats['counters']['avec_horaires']); $stats->setAvecAdresse($calculatedStats['counters']['avec_adresse']); $stats->setAvecSite($calculatedStats['counters']['avec_site']); $stats->setAvecAccessibilite($calculatedStats['counters']['avec_accessibilite']); $stats->setAvecNote($calculatedStats['counters']['avec_note']); $stats->setCompletionPercent($calculatedStats['completion_percent']); // Associer les stats à chaque commerce foreach ($commerces as $commerce) { $commerce->setStats($stats); $this->entityManager->persist($commerce); } $stats->computeCompletionPercent(); // Calculer les statistiques de fraîcheur des données OSM $timestamps = []; foreach ($stats->getPlaces() as $place) { if ($place->getOsmDataDate()) { $timestamps[] = $place->getOsmDataDate()->getTimestamp(); } } if (!empty($timestamps)) { // Date la plus ancienne (min) $minTimestamp = min($timestamps); $stats->setOsmDataDateMin(new \DateTime('@' . $minTimestamp)); // Date la plus récente (max) $maxTimestamp = max($timestamps); $stats->setOsmDataDateMax(new \DateTime('@' . $maxTimestamp)); // Date moyenne $avgTimestamp = array_sum($timestamps) / count($timestamps); $stats->setOsmDataDateAvg(new \DateTime('@' . (int)$avgTimestamp)); } if($stats->getDateCreated() == null) { $stats->setDateCreated(new \DateTime()); } $stats->setDateModified(new \DateTime()); // Créer un historique des statistiques $statsHistory = new StatsHistory(); $statsHistory->setDate(new \DateTime()) ->setStats($stats); // Compter les Places avec email et SIRET $placesWithEmail = 0; $placesWithSiret = 0; foreach ($stats->getPlaces() as $place) { if ($place->getEmail() && $place->getEmail() !== '') { $placesWithEmail++; } if ($place->getSiret() && $place->getSiret() !== '') { $placesWithSiret++; } } $statsHistory->setPlacesCount($stats->getPlaces()->count()) ->setOpeningHoursCount($stats->getAvecHoraires()) ->setAddressCount($stats->getAvecAdresse()) ->setWebsiteCount($stats->getAvecSite()) ->setSiretCount($placesWithSiret) ->setEmailsCount($placesWithEmail) // ->setAccessibiliteCount($stats->getAvecAccessibilite()) // ->setNoteCount($stats->getAvecNote()) ->setCompletionPercent($stats->getCompletionPercent()) ->setStats($stats); $this->entityManager->persist($statsHistory); $this->entityManager->persist($stats); $this->entityManager->flush(); $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().').'; } $this->addFlash('success', $message); } $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'); } #[Route('/admin', name: 'app_admin')] public function index(): Response { return $this->render('admin/index.html.twig', [ 'controller_name' => 'AdminController', ]); } #[Route('/admin/stats/{insee_code}', name: 'app_admin_stats')] public function calculer_stats(string $insee_code): Response { // Récupérer tous les commerces de la zone // $commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code, 'dead' => false]); // Récupérer les stats existantes pour la zone $stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); $commerces = $stats->getPlaces(); if(!$stats) { // Si aucune stat n'existe, on en crée une vide pour éviter les erreurs, mais sans la sauvegarder $stats = new Stats(); $stats->setZone($insee_code); $stats->setName('Nouvelle zone non labourée'); } $urls = $stats->getAllCTCUrlsMap(); $statsHistory = $this->entityManager->getRepository(StatsHistory::class) ->createQueryBuilder('sh') ->where('sh.stats = :stats') ->setParameter('stats', $stats) ->orderBy('sh.id', 'DESC') ->setMaxResults(365) ->getQuery() ->getResult(); // Données pour le graphique des modifications par trimestre $modificationsByQuarter = []; foreach ($commerces as $commerce) { if ($commerce->getOsmDataDate()) { $date = $commerce->getOsmDataDate(); $year = $date->format('Y'); $quarter = ceil($date->format('n') / 3); $key = $year . '-Q' . $quarter; if (!isset($modificationsByQuarter[$key])) { $modificationsByQuarter[$key] = 0; } $modificationsByQuarter[$key]++; } } ksort($modificationsByQuarter); // Trier par clé (année-trimestre) $geojson = [ 'type' => 'FeatureCollection', 'features' => [] ]; foreach ($commerces as $commerce) { if ($commerce->getLat() && $commerce->getLon()) { $geojson['features'][] = [ 'type' => 'Feature', 'geometry' => [ 'type' => 'Point', 'coordinates' => [$commerce->getLon(), $commerce->getLat()] ], 'properties' => [ 'id' => $commerce->getOsmId(), 'name' => $commerce->getName(), 'main_tag' => $commerce->getMainTag(), 'address' => $commerce->getStreet() . ' ' . $commerce->getHousenumber(), 'note' => $commerce->getNoteContent(), 'osm_url' => 'https://www.openstreetmap.org/' . $commerce->getOsmKind() . '/' . $commerce->getOsmId() ] ]; } } return $this->render('admin/stats.html.twig', [ 'stats' => $stats, 'commerces' => $commerces, 'urls' => $urls, 'geojson' => json_encode($geojson), 'modificationsByQuarter' => json_encode($modificationsByQuarter), 'maptiler_token' => $_ENV['MAPTILER_TOKEN'], 'statsHistory' => $statsHistory, 'CTC_urls' => $urls, 'overpass' => '' ]); } #[Route('/admin/placeType/{osm_kind}/{osm_id}', name: 'app_admin_by_osm_id')] public function placeType(string $osm_kind, string $osm_id): Response { $place = $this->entityManager->getRepository(Place::class)->findOneBy(['osm_kind' => $osm_kind, 'osmId' => $osm_id]); if($place) { return $this->redirectToRoute('app_admin_commerce', ['id' => $place->getId()]); } else { $this->addFlash('error', 'Le lieu n\'existe pas.'); return $this->redirectToRoute('app_public_index'); } } /** * rediriger vers l'url unique quand on est admin */ #[Route('/admin/commerce/{id}', name: 'app_admin_commerce')] public function commerce(int $id): Response { // Vérifier si on est en prod if ($this->getParameter('kernel.environment') === 'prod') { $this->addFlash('error', 'Vous n\'avez pas accès à cette page en production.'); return $this->redirectToRoute('app_public_index'); } $commerce = $this->entityManager->getRepository(Place::class)->find($id); if (!$commerce) { throw $this->createNotFoundException('Commerce non trouvé'); } // Redirection vers la page de modification avec les paramètres nécessaires return $this->redirectToRoute('app_public_edit', [ 'zipcode' => $commerce->getZipCode(), 'name' => $commerce->getName()!='' ? $commerce->getName() : '?', 'uuid' => $commerce->getUuidForUrl() ]); } /** * récupérer les commerces de la zone selon le code INSEE, créer les nouveaux lieux, et mettre à jour les existants */ #[Route('/admin/labourer/{insee_code}', name: 'app_admin_labourer')] public function labourer(Request $request, string $insee_code, bool $updateExisting = true): Response { $deleteMissing = $request->query->getBoolean('deleteMissing', true); // Vérifier si le code INSEE est valide (composé uniquement de chiffres) if (!ctype_digit($insee_code) || $insee_code == 'undefined' || $insee_code == '') { $this->addFlash('error', 'Code INSEE invalide : il doit être composé uniquement de chiffres.'); return $this->redirectToRoute('app_public_index'); } try { // Récupérer ou créer les stats pour cette zone $stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); $city = $this->motocultrice->get_city_osm_from_zip_code($insee_code); if (!$stats) { $stats = new Stats(); $stats->setDateCreated(new \DateTime()); $stats->setDateModified(new \DateTime()); $stats->setZone($insee_code) ->setPlacesCount(0) ->setAvecHoraires(0) ->setAvecAdresse(0) ->setAvecSite(0) ->setAvecAccessibilite(0) ->setAvecNote(0) ->setCompletionPercent(0); $this->entityManager->persist($stats); $this->entityManager->flush(); } $stats->setName($city); // Récupérer la population via l'API $population = null; try { $apiUrl = 'https://geo.api.gouv.fr/communes/' . $insee_code; $response = file_get_contents($apiUrl); if ($response !== false) { $data = json_decode($response, true); if (isset($data['population'])) { $population = (int)$data['population']; $stats->setPopulation($population); } if (isset($data['siren'])) { $stats->setSiren((int)$data['siren']); } if (isset($data['codeEpci'])) { $stats->setCodeEpci((int)$data['codeEpci']); } if (isset($data['codesPostaux'])) { $stats->setCodesPostaux(implode(';', $data['codesPostaux'])); } } } catch (\Exception $e) { $this->addFlash('error', 'Erreur lors de la récupération des données de l\'API : ' . $e->getMessage()); } // Récupérer toutes les données $places_overpass = $this->motocultrice->labourer($insee_code); $processedCount = 0; $updatedCount = 0; $deletedCount = 0; $overpass_osm_ids = array_map(fn($place) => $place['id'], $places_overpass); foreach ($places_overpass as $placeData) { // Vérifier si le lieu existe déjà $existingPlace = $this->entityManager->getRepository(Place::class) ->findOneBy(['osmId' => $placeData['id']]); if (!$existingPlace) { $place = new Place(); $place->setOsmId($placeData['id']) ->setOsmKind($placeData['type']) ->setZipCode($insee_code) ->setUuidForUrl($this->motocultrice->uuid_create()) ->setModifiedDate(new \DateTime()) ->setStats($stats) ->setDead(false) ->setOptedOut(false) ->setMainTag($this->motocultrice->find_main_tag($placeData['tags']) ?? '') ->setStreet($this->motocultrice->find_street($placeData['tags']) ?? '') ->setHousenumber($this->motocultrice->find_housenumber($placeData['tags']) ?? '') ->setSiret($this->motocultrice->find_siret($placeData['tags']) ?? '') ->setAskedHumainsSupport(false) ->setLastContactAttemptDate(null) ->setNote($this->motocultrice->find_tag($placeData['tags'], 'note') ? true : false) ->setNoteContent($this->motocultrice->find_tag($placeData['tags'], 'note') ?? '') ->setPlaceCount(0) // ->setOsmData($placeData['modified'] ?? null) ; // Mettre à jour les données depuis Overpass $place->update_place_from_overpass_data($placeData); $this->entityManager->persist($place); $stats->addPlace($place); $processedCount++; } elseif ($updateExisting) { // Mettre à jour les données depuis Overpass et s'assurer qu'il est marqué comme "vivant" $existingPlace->setDead(false); $existingPlace->update_place_from_overpass_data($placeData); $stats->addPlace($existingPlace); $this->entityManager->persist($existingPlace); $updatedCount++; } } // Supprimer les lieux qui ne sont plus dans la réponse Overpass, si activé if ($deleteMissing) { $db_places = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]); foreach ($db_places as $db_place) { if (!in_array($db_place->getOsmId(), $overpass_osm_ids)) { $this->entityManager->remove($db_place); $deletedCount++; } } } // Récupérer tous les commerces de la zone qui n'ont pas été supprimés $commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]); // Récupérer les stats existantes pour la zone $stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); if(!$stats) { $stats = new Stats(); $stats->setZone($insee_code); } $urls = $stats->getAllCTCUrlsMap(); $statsHistory = $this->entityManager->getRepository(StatsHistory::class) ->createQueryBuilder('sh') ->where('sh.stats = :stats') ->setParameter('stats', $stats) ->orderBy('sh.id', 'DESC') ->setMaxResults(365) ->getQuery() ->getResult(); // Calculer les statistiques $calculatedStats = $this->motocultrice->calculateStats($commerces); // Mettre à jour les stats pour la zone donnée $stats->setPlacesCount($calculatedStats['places_count']); $stats->setAvecHoraires($calculatedStats['counters']['avec_horaires']); $stats->setAvecAdresse($calculatedStats['counters']['avec_adresse']); $stats->setAvecSite($calculatedStats['counters']['avec_site']); $stats->setAvecAccessibilite($calculatedStats['counters']['avec_accessibilite']); $stats->setAvecNote($calculatedStats['counters']['avec_note']); $stats->setCompletionPercent($calculatedStats['completion_percent']); // Associer les stats à chaque commerce foreach ($commerces as $commerce) { $commerce->setStats($stats); $this->entityManager->persist($commerce); } $stats->computeCompletionPercent(); // Calculer les statistiques de fraîcheur des données OSM $timestamps = []; foreach ($stats->getPlaces() as $place) { if ($place->getOsmDataDate()) { $timestamps[] = $place->getOsmDataDate()->getTimestamp(); } } if (!empty($timestamps)) { // Date la plus ancienne (min) $minTimestamp = min($timestamps); $stats->setOsmDataDateMin(new \DateTime('@' . $minTimestamp)); // Date la plus récente (max) $maxTimestamp = max($timestamps); $stats->setOsmDataDateMax(new \DateTime('@' . $maxTimestamp)); // Date moyenne $avgTimestamp = array_sum($timestamps) / count($timestamps); $stats->setOsmDataDateAvg(new \DateTime('@' . (int)$avgTimestamp)); } if($stats->getDateCreated() == null) { $stats->setDateCreated(new \DateTime()); } $stats->setDateModified(new \DateTime()); // Créer un historique des statistiques $statsHistory = new StatsHistory(); $statsHistory->setDate(new \DateTime()) ->setStats($stats); // Compter les Places avec email et SIRET $placesWithEmail = 0; $placesWithSiret = 0; foreach ($stats->getPlaces() as $place) { if ($place->getEmail() && $place->getEmail() !== '') { $placesWithEmail++; } if ($place->getSiret() && $place->getSiret() !== '') { $placesWithSiret++; } } $statsHistory->setPlacesCount($stats->getPlaces()->count()) ->setOpeningHoursCount($stats->getAvecHoraires()) ->setAddressCount($stats->getAvecAdresse()) ->setWebsiteCount($stats->getAvecSite()) ->setSiretCount($placesWithSiret) ->setEmailsCount($placesWithEmail) // ->setAccessibiliteCount($stats->getAvecAccessibilite()) // ->setNoteCount($stats->getAvecNote()) ->setCompletionPercent($stats->getCompletionPercent()) ->setStats($stats); $this->entityManager->persist($statsHistory); $this->entityManager->persist($stats); $this->entityManager->flush(); $message = 'Labourage terminé avec succès. ' . $processedCount . ' nouveaux lieux traités.'; if ($updateExisting) { $message .= ' ' . $updatedCount . ' lieux existants mis à jour.'; } if ($deletedCount > 0) { $message .= ' ' . $deletedCount . ' lieux ont été supprimés.'; } $message .= ' Zone : '.$stats->getName().' ('.$stats->getZone().').'; $this->addFlash('success', $message); } catch (\Exception $e) { $this->addFlash('error', 'Erreur lors du labourage : ' . $e->getMessage()); die(var_dump($e)); } // return $this->redirectToRoute('app_public_dashboard'); return $this->redirectToRoute('app_admin_stats', ['insee_code' => $insee_code]); } #[Route('/admin/delete/{id}', name: 'app_admin_delete')] public function delete(int $id): Response { $commerce = $this->entityManager->getRepository(Place::class)->find($id); if($commerce) { $this->entityManager->remove($commerce); $this->entityManager->flush(); $this->addFlash('success', 'Le lieu '.$commerce->getName().' a été supprimé avec succès de OSM Mes commerces, mais pas dans OpenStreetMap.'); } else { $this->addFlash('error', 'Le lieu n\'existe pas.'); } return $this->redirectToRoute('app_public_dashboard'); } #[Route('/admin/delete_by_zone/{insee_code}', name: 'app_admin_delete_by_zone')] public function delete_by_zone(string $insee_code): Response { $stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); if (!$stats) { $this->addFlash('error', 'Aucune statistique trouvée pour la zone ' . $insee_code); return $this->redirectToRoute('app_public_dashboard'); } try { // 1. Supprimer tous les StatsHistory associés foreach ($stats->getStatsHistories() as $history) { $this->entityManager->remove($history); } // 2. Supprimer tous les Places associées foreach ($stats->getPlaces() as $place) { $this->entityManager->remove($place); } // 3. Supprimer l'objet Stats lui-même $this->entityManager->remove($stats); // 4. Appliquer les changements à la base de données $this->entityManager->flush(); $this->addFlash('success', 'La zone ' . $insee_code . ' et toutes les données associées ont été supprimées avec succès.'); } catch (\Exception $e) { $this->addFlash('error', 'Une erreur est survenue lors de la suppression de la zone ' . $insee_code . ': ' . $e->getMessage()); } return $this->redirectToRoute('app_public_dashboard'); } #[Route('/admin/export', name: 'app_admin_export')] public function export(): Response { $places = $this->entityManager->getRepository(Place::class)->findAll(); $csvData = []; $csvData[] = [ 'Nom', 'Email', 'Code postal', 'ID OSM', 'Type OSM', 'Date de modification', 'Date dernier contact', 'Note', 'Désabonné', 'Inactif', 'Support humain demandé', 'A des horaires', 'A une adresse', 'A un site web', 'A accessibilité', 'A une note' ]; foreach ($places as $place) { $csvData[] = [ $place->getName(), $place->getEmail(), $place->getZipCode(), $place->getOsmId(), $place->getOsmKind(), $place->getModifiedDate() ? $place->getModifiedDate()->format('Y-m-d H:i:s') : '', $place->getLastContactAttemptDate() ? $place->getLastContactAttemptDate()->format('Y-m-d H:i:s') : '', $place->getNote(), $place->isOptedOut() ? 'Oui' : 'Non', $place->isDead() ? 'Oui' : 'Non', $place->isAskedHumainsSupport() ? 'Oui' : 'Non', $place->hasOpeningHours() ? 'Oui' : 'Non', $place->hasAddress() ? 'Oui' : 'Non', $place->hasWebsite() ? 'Oui' : 'Non', $place->hasWheelchair() ? 'Oui' : 'Non', $place->hasNote() ? 'Oui' : 'Non' ]; } $response = new Response(); $response->headers->set('Content-Type', 'text/csv'); $response->headers->set('Content-Disposition', 'attachment; filename="export_places.csv"'); $handle = fopen('php://temp', 'r+'); foreach ($csvData as $row) { fputcsv($handle, $row, ';'); } rewind($handle); $response->setContent(stream_get_contents($handle)); fclose($handle); return $response; } #[Route('/admin/export_csv/{insee_code}', name: 'app_admin_export_csv')] public function export_csv(string $insee_code): Response { $stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); $response = new Response($this->motocultrice->export($insee_code)); $response->headers->set('Content-Type', 'text/csv'); $slug_name = str_replace(' ', '-', $stats->getName()); $response->headers->set('Content-Disposition', 'attachment; filename="osm-commerces-export_' . $insee_code . '_' . $slug_name . '_' . date('Y-m-d_H-i-s') . '.csv"'); return $response; } #[Route('/admin/make_email_for_place/{id}', name: 'app_admin_make_email_for_place')] public function make_email_for_place(Place $place): Response { return $this->render('admin/view_email_for_place.html.twig', ['place' => $place]); } #[Route('/admin/no_more_sollicitation_for_place/{id}', name: 'app_admin_no_more_sollicitation_for_place')] public function no_more_sollicitation_for_place(Place $place): Response { $place->setOptedOut(true); $this->entityManager->persist($place); $this->entityManager->flush(); $this->addFlash('success', 'Votre lieu '.$place->getName().' ne sera plus sollicité pour mettre à jour ses informations.'); return $this->redirectToRoute('app_public_index'); } #[Route('/admin/send_email_to_place/{id}', name: 'app_admin_send_email_to_place')] public function send_email_to_place(Place $place, \Symfony\Component\Mailer\MailerInterface $mailer): Response { // Vérifier si le lieu est opted out if ($place->isOptedOut()) { $this->addFlash('error', 'Ce lieu a demandé à ne plus être sollicité pour mettre à jour ses informations.'); return $this->redirectToRoute('app_public_index'); } // Vérifier si le lieu a déjà été contacté if ($place->getLastContactAttemptDate() !== null) { $this->addFlash('error', 'Ce lieu a déjà été contacté le ' . $place->getLastContactAttemptDate()->format('d/m/Y H:i:s')); return $this->redirectToRoute('app_public_index'); } // Générer le contenu de l'email avec le template $emailContent = $this->renderView('admin/email_content.html.twig', [ 'place' => $place ]); // Envoyer l'email $email = (new \Symfony\Component\Mime\Email()) ->from('contact@openstreetmap.fr') ->to('contact+send_email@cipherbliss.com') ->subject('Mise à jour des informations de votre établissement dans OpenStreetMap') ->html($emailContent); $mailer->send($email); // Mettre à jour la date de dernier contact $place->setLastContactAttemptDate(new \DateTime()); $this->entityManager->persist($place); $this->entityManager->flush(); $this->addFlash('success', 'Email envoyé avec succès à ' . $place->getName() . ' le ' . $place->getLastContactAttemptDate()->format('d/m/Y H:i:s')); return $this->redirectToRoute('app_public_index'); } #[Route('/admin/fraicheur/histogramme', name: 'admin_fraicheur_histogramme')] public function showFraicheurHistogramme(): Response { $jsonPath = $this->getParameter('kernel.project_dir') . '/var/fraicheur_osm.json'; if (!file_exists($jsonPath)) { // Générer le fichier si absent $this->calculateFraicheur(); } return $this->render('admin/fraicheur_histogramme.html.twig'); } #[Route('/admin/fraicheur/calculate', name: 'admin_fraicheur_calculate')] public function calculateFraicheur(): Response { $filesystem = new Filesystem(); $jsonPath = $this->getParameter('kernel.project_dir') . '/var/fraicheur_osm.json'; $now = new \DateTime(); // Si le fichier existe et a moins de 12h, on ne régénère pas if ($filesystem->exists($jsonPath)) { $fileMTime = filemtime($jsonPath); if ($fileMTime && ($now->getTimestamp() - $fileMTime) < 43200) { // 12h = 43200s return $this->redirectToRoute('admin_fraicheur_histogramme'); } } $places = $this->entityManager->getRepository(Place::class)->findAll(); $histogram = []; $total = 0; foreach ($places as $place) { $date = $place->getOsmDataDate(); if ($date) { $key = $date->format('Y-m'); if (!isset($histogram[$key])) { $histogram[$key] = 0; } $histogram[$key]++; $total++; } } ksort($histogram); $data = [ 'generated_at' => $now->format('c'), 'total' => $total, 'histogram' => $histogram ]; $filesystem->dumpFile($jsonPath, json_encode($data, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)); // --- Distribution villes selon lieux/habitants --- $distJsonPath = $this->getParameter('kernel.project_dir') . '/var/distribution_villes_lieux_par_habitant.json'; // Toujours régénérer $statsRepo = $this->entityManager->getRepository(Stats::class); $allStats = $statsRepo->findAll(); $histogram_lieux_par_habitant = []; $histogram_habitants_par_lieu = []; $totalVilles = 0; foreach ($allStats as $stat) { $places = $stat->getPlacesCount(); $population = $stat->getPopulation(); if ($places && $population && $population > 0) { // lieux par habitant (pas de 0.01) $ratio_lph = round($places / $population, 4); $bin_lph = round(floor($ratio_lph / 0.01) * 0.01, 2); if (!isset($histogram_lieux_par_habitant[$bin_lph])) $histogram_lieux_par_habitant[$bin_lph] = 0; $histogram_lieux_par_habitant[$bin_lph]++; // habitants par lieu (pas de 10) $ratio_hpl = ceil($population / $places); $bin_hpl = ceil($ratio_hpl / 10) * 10; if (!isset($histogram_habitants_par_lieu[$bin_hpl])) $histogram_habitants_par_lieu[$bin_hpl] = 0; $histogram_habitants_par_lieu[$bin_hpl]++; $totalVilles++; } } ksort($histogram_lieux_par_habitant); ksort($histogram_habitants_par_lieu); $distData = [ 'generated_at' => $now->format('c'), 'total_villes' => $totalVilles, 'histogram_001' => $histogram_lieux_par_habitant, 'histogram_10' => $histogram_habitants_par_lieu ]; $filesystem->dumpFile($distJsonPath, json_encode($distData, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)); return $this->redirectToRoute('admin_fraicheur_histogramme'); } #[Route('/admin/fraicheur/download', name: 'admin_fraicheur_download')] public function downloadFraicheur(): JsonResponse { $jsonPath = $this->getParameter('kernel.project_dir') . '/var/fraicheur_osm.json'; if (!file_exists($jsonPath)) { return new JsonResponse(['error' => 'Fichier non généré'], 404); } $content = file_get_contents($jsonPath); $data = json_decode($content, true); return new JsonResponse($data); } #[Route('/admin/distribution_villes_lieux_par_habitant_download', name: 'admin_distribution_villes_lieux_par_habitant_download')] public function downloadDistributionVillesLieuxParHabitant(): JsonResponse { $jsonPath = $this->getParameter('kernel.project_dir') . '/var/distribution_villes_lieux_par_habitant.json'; if (!file_exists($jsonPath)) { // Générer à la volée si absent $now = new \DateTime(); $filesystem = new \Symfony\Component\Filesystem\Filesystem(); $statsRepo = $this->entityManager->getRepository(\App\Entity\Stats::class); $allStats = $statsRepo->findAll(); $distribution = []; $histogram = []; $totalVilles = 0; foreach ($allStats as $stat) { $places = $stat->getPlacesCount(); $population = $stat->getPopulation(); if ($places && $population && $population > 0) { $ratio = round($places / $population, 4); // lieux par habitant $bin = round(floor($ratio / 0.01) * 0.01, 2); // pas de 0.01 if (!isset($histogram[$bin])) $histogram[$bin] = 0; $histogram[$bin]++; $totalVilles++; } } ksort($histogram); $distData = [ 'generated_at' => $now->format('c'), 'total_villes' => $totalVilles, 'histogram_001' => $histogram ]; $filesystem->dumpFile($jsonPath, json_encode($distData, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)); } $content = file_get_contents($jsonPath); $data = json_decode($content, true); return new JsonResponse($data); } #[Route('/admin/distribution_villes_lieux_par_habitant_villes', name: 'admin_distribution_villes_lieux_par_habitant_villes')] public function downloadDistributionVillesLieuxParHabitantVilles(): JsonResponse { $statsRepo = $this->entityManager->getRepository(\App\Entity\Stats::class); $allStats = $statsRepo->findAll(); $villesByBin = []; foreach ($allStats as $stat) { $places = $stat->getPlacesCount(); $population = $stat->getPopulation(); $name = $stat->getName(); if ($places && $population && $population > 0 && $name) { $ratio = round($places / $population, 4); // lieux par habitant $bin = round(floor($ratio / 0.01) * 0.01, 2); // pas de 0.01 if (!isset($villesByBin[$bin])) $villesByBin[$bin] = []; $villesByBin[$bin][] = $name; } } ksort($villesByBin); return new JsonResponse(['villes_by_bin' => $villesByBin]); } }