osm-labo/src/Controller/AdminController.php

3787 lines
160 KiB
PHP
Raw Normal View History

2025-05-26 11:55:44 +02:00
<?php
2025-06-27 00:35:11 +02:00
2025-05-26 11:55:44 +02:00
namespace App\Controller;
use App\Entity\Place;
use App\Entity\Stats;
2025-06-17 19:38:44 +02:00
use App\Entity\StatsHistory;
2025-08-08 18:51:44 +02:00
use App\Service\ActionLogger;
use App\Service\BudgetService;
2025-08-08 18:51:44 +02:00
use App\Service\FollowUpService;
use App\Service\Motocultrice;
use Doctrine\ORM\EntityManagerInterface;
2025-08-08 18:51:44 +02:00
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
2025-06-24 00:29:15 +02:00
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\JsonResponse;
2025-08-08 18:51:44 +02:00
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Twig\Environment;
2025-08-08 18:51:44 +02:00
use function uuid_create;
2025-05-26 11:55:44 +02:00
final class AdminController extends AbstractController
{
2025-06-27 00:35:11 +02:00
2025-06-29 19:24:00 +02:00
private FollowUpService $followUpService;
2025-07-07 23:30:09 +02:00
// Flag pour activer/désactiver la suppression de ville
private $allowDeleteCity = false;
2025-06-29 19:24:00 +02:00
public function __construct(
private EntityManagerInterface $entityManager,
2025-08-08 18:51:44 +02:00
private Motocultrice $motocultrice,
private BudgetService $budgetService,
private Environment $twig,
private ActionLogger $actionLogger,
FollowUpService $followUpService
)
{
2025-06-29 19:24:00 +02:00
$this->followUpService = $followUpService;
}
#[Route('/admin/labourer-toutes-les-zones', name: 'app_admin_labourer_tout')]
public function labourer_tout(): Response
{
2025-06-26 23:40:37 +02:00
$this->actionLogger->log('labourer_toutes_les_zones', []);
2025-06-27 00:35:11 +02:00
$updateExisting = true;
2025-06-27 00:35:11 +02:00
$stats_all = $this->entityManager->getRepository(Stats::class)->findAll();
echo 'on a trouvé ' . count($stats_all) . ' zones à labourer<br>';
2025-06-27 00:35:11 +02:00
foreach ($stats_all as $stats) {
echo '<br> on laboure la zone ' . $stats->getZone() . ' ';
$processedCount = 0;
$updatedCount = 0;
$insee_code = $stats->getZone();
// Vérifier si le code INSEE est un nombre valide
2025-06-27 00:35:11 +02:00
// 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<br>';
continue;
}
2025-06-27 00:35:11 +02:00
}
if (!is_numeric($insee_code) || $insee_code == 'undefined' || $insee_code == '') {
echo 'Code INSEE invalide : ' . $insee_code . ' - on passe au suivant<br>';
continue;
}
$places_overpass = $this->motocultrice->labourer($stats->getZone());
$places = $places_overpass;
foreach ($places as $placeData) {
2025-06-27 00:35:11 +02:00
// Vérifier si le lieu existe déjà
$existingPlace = $this->entityManager->getRepository(Place::class)
->findOneBy(['osmId' => $placeData['id']]);
2025-06-27 00:35:11 +02:00
if (!$existingPlace) {
$place = new Place();
$place->setOsmId($placeData['id'])
2025-06-27 00:35:11 +02:00
->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)
2025-08-08 18:51:44 +02:00
->setPlaceCount(0)// ->setOsmData($placeData['modified'] ?? null)
2025-06-27 00:35:11 +02:00
;
// Mettre à jour les données depuis Overpass
$place->update_place_from_overpass_data($placeData);
2025-06-27 00:35:11 +02:00
$this->entityManager->persist($place);
$stats->addPlace($place);
$processedCount++;
// Générer le contenu de l'email avec le template
$emailContent = $this->twig->render('admin/email_content.html.twig', ['place' => $place]);
$place->setEmailContent($emailContent);
} elseif ($updateExisting) {
// Mettre à jour les données depuis Overpass uniquement si updateExisting est true
$existingPlace->update_place_from_overpass_data($placeData);
2025-06-21 10:26:55 +02:00
$stats->addPlace($existingPlace);
$this->entityManager->persist($existingPlace);
$updatedCount++;
}
2025-06-27 00:35:11 +02:00
}
// 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
2025-07-15 21:22:02 +02:00
$stats_exist = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
if ($stats_exist) {
$stats = $stats_exist;
} else {
$stats = new Stats();
2025-08-08 19:04:42 +02:00
// dump('nouvelle stat', $insee_code);
// die();
2025-07-15 21:22:02 +02:00
$stats->setZone($insee_code);
}
2025-06-27 00:35:11 +02:00
$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();
2025-06-27 00:35:11 +02:00
// 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']);
2025-06-27 00:35:11 +02:00
$stats->setCompletionPercent($calculatedStats['completion_percent']);
// Associer les stats à chaque commerce
foreach ($commerces as $commerce) {
$commerce->setStats($stats);
2025-07-05 16:53:12 +02:00
// Injection de l'emoji pour le template
$mainTag = $commerce->getMainTag();
$emoji = self::getTagEmoji($mainTag);
$commerce->tagEmoji = $emoji;
$this->entityManager->persist($commerce);
}
$stats->computeCompletionPercent();
2025-06-19 12:49:30 +02:00
// Calculer les statistiques de fraîcheur des données OSM
$timestamps = [];
foreach ($stats->getPlaces() as $place) {
if ($place->getOsmDataDate()) {
$timestamps[] = $place->getOsmDataDate()->getTimestamp();
}
}
2025-06-27 00:35:11 +02:00
2025-06-19 12:49:30 +02:00
if (!empty($timestamps)) {
// Date la plus ancienne (min)
$minTimestamp = min($timestamps);
$stats->setOsmDataDateMin(new \DateTime('@' . $minTimestamp));
2025-06-27 00:35:11 +02:00
2025-06-19 12:49:30 +02:00
// Date la plus récente (max)
$maxTimestamp = max($timestamps);
$stats->setOsmDataDateMax(new \DateTime('@' . $maxTimestamp));
2025-06-27 00:35:11 +02:00
2025-06-19 12:49:30 +02:00
// Date moyenne
$avgTimestamp = array_sum($timestamps) / count($timestamps);
$stats->setOsmDataDateAvg(new \DateTime('@' . (int)$avgTimestamp));
}
2025-06-27 00:35:11 +02:00
if ($stats->getDateCreated() == null) {
2025-06-19 12:49:30 +02:00
$stats->setDateCreated(new \DateTime());
}
$stats->setDateModified(new \DateTime());
// Créer un historique des statistiques
$statsHistory = new StatsHistory();
$statsHistory->setDate(new \DateTime())
2025-06-27 00:35:11 +02:00
->setStats($stats);
2025-06-19 12:49:30 +02:00
// 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())
2025-06-27 00:35:11 +02:00
->setOpeningHoursCount($stats->getAvecHoraires())
->setAddressCount($stats->getAvecAdresse())
->setWebsiteCount($stats->getAvecSite())
->setSiretCount($placesWithSiret)
->setEmailsCount($placesWithEmail)
->setCompletionPercent($stats->getCompletionPercent())
->setStats($stats);
2025-06-19 12:49:30 +02:00
$this->entityManager->persist($statsHistory);
2025-06-27 00:35:11 +02:00
$this->entityManager->persist($stats);
2025-06-19 12:49:30 +02:00
$this->entityManager->flush();
2025-06-27 00:35:11 +02:00
2025-06-29 10:09:47 +02:00
// Générer les contenus d'email après le flush pour éviter les problèmes de mémoire
$placesToUpdate = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]);
foreach ($placesToUpdate as $place) {
if (!$place->getEmailContent()) {
$emailContent = $this->twig->render('admin/email_content.html.twig', ['place' => $place]);
$place->setEmailContent($emailContent);
$this->entityManager->persist($place);
}
}
$this->entityManager->flush();
2025-06-29 19:24:00 +02:00
// Générer les suivis (followups) après la mise à jour des Places
$themes = \App\Service\FollowUpService::getFollowUpThemes();
foreach (array_keys($themes) as $theme) {
$this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager, true, $theme);
2025-06-19 12:49:30 +02:00
}
$this->entityManager->flush();
2025-07-15 23:23:32 +02:00
// Après le flush, vérifier s'il n'y a aucun commerce compté
if ($stats->getPlacesCount() < 2) {
// Récupérer les lieux via Motocultrice
try {
$placesData = $this->motocultrice->labourer($insee_code);
$places = $stats->getPlaces();
foreach ($places as $place) {
// Chercher les données correspondantes par OSM ID
foreach ($placesData as $placeData) {
if ($place->getOsmId() == $placeData['id']) {
// Mettre à jour les tags et coordonnées
$place->update_place_from_overpass_data($placeData);
$place->setStat($stats);
$stats->addPlace($place);
$this->entityManager->persist($place);
break;
}
}
}
$this->entityManager->flush();
} catch (\Exception $e) {
// Ignorer les erreurs silencieusement
}
}
return $this->redirectToRoute('app_admin_stats', ['insee_code' => $insee_code]);
}
2025-06-27 00:35:11 +02:00
$this->entityManager->flush();
2025-06-27 00:35:11 +02:00
$this->addFlash('success', 'Labourage des ' . count($stats_all) . ' zones terminé avec succès.');
return $this->redirectToRoute('app_public_dashboard');
}
2025-05-26 11:55:44 +02:00
#[Route('/admin', name: 'app_admin')]
public function index(): Response
{
return $this->render('admin/index.html.twig', [
'controller_name' => 'AdminController',
]);
}
2025-06-30 15:03:37 +02:00
#[Route('/admin/stats/{insee_code}', name: 'app_admin_stats', requirements: ['insee_code' => '\\d+'])]
public function stats(string $insee_code): Response
{
2025-06-17 18:27:19 +02:00
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
2025-06-26 18:20:43 +02:00
if (!$stats) {
2025-07-15 21:22:02 +02:00
$this->addFlash('error', '1 Aucune stats trouvée pour ce code INSEE. Veuillez d\'abord ajouter la ville.');
return $this->redirectToRoute('app_admin_import_stats');
2025-06-26 18:20:43 +02:00
}
// $completion = $stats->getCompletionPercent();
// if (!$completion) {
// $stats->computeCompletionPercent();
// $completion = $stats->getCompletionPercent();
// }
2025-06-30 15:03:37 +02:00
$followups = $stats->getCityFollowUps();
$refresh = false;
if (!$followups->isEmpty()) {
$latest = null;
foreach ($followups as $fu) {
if ($latest === null || $fu->getDate() > $latest->getDate()) {
$latest = $fu;
}
}
if ($latest && $latest->getDate() < (new \DateTime('-1 day'))) {
$refresh = true;
}
} else {
$refresh = true;
}
if ($refresh) {
2025-06-29 19:24:00 +02:00
$this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager);
2025-06-30 15:03:37 +02:00
$followups = $stats->getCityFollowUps();
2025-06-29 19:24:00 +02:00
}
2025-08-08 18:51:44 +02:00
$commerces = $stats->getPlacesCount();
2025-08-08 19:04:42 +02:00
// Remove existing duplicates
$this->removeDuplicatePlaces($stats);
// labourer
$places_found = $this->motocultrice->labourer($insee_code);
if (count($places_found)) {
// var_dump(count($places_found));
$placeRepo = $this->entityManager->getRepository(Place::class);
$addedCount = 0;
foreach ($places_found as $placeData) {
// Check if a place with the same osm_id and osm_kind already exists
$existingPlace = $placeRepo->findOneBy([
'osmId' => $placeData['id'],
'osm_kind' => $placeData['type'],
'stats' => $stats
]);
2025-08-08 18:51:44 +02:00
2025-08-08 19:04:42 +02:00
if (!$existingPlace) {
// Create a new place only if it doesn't already exist
2025-08-08 18:51:44 +02:00
$newPlace = new Place();
$newPlace->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)
->setPlaceCount(0)// ->setOsmData($placeData['modified'] ?? null)
;
// Mettre à jour les données depuis Overpass
$newPlace->update_place_from_overpass_data($placeData);
$stats->addPlace($newPlace);
$newPlace->setStats($stats);
$this->entityManager->persist($newPlace);
2025-08-08 19:04:42 +02:00
$addedCount++;
2025-08-08 18:51:44 +02:00
}
}
2025-08-08 19:04:42 +02:00
// Update the places_count property
$stats->setPlacesCount($stats->getPlaces()->count());
// $this->entityManager->persist($stats);
2025-08-08 18:51:44 +02:00
}
2025-08-08 19:04:42 +02:00
2025-08-08 18:51:44 +02:00
$this->entityManager->flush();
2025-06-26 23:40:37 +02:00
$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]);
2025-06-27 00:35:11 +02:00
if (!$stats) {
2025-06-21 11:28:31 +02:00
// 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);
2025-06-21 11:28:31 +02:00
$stats->setName('Nouvelle zone non labourée');
}
2025-06-21 11:28:31 +02:00
$urls = $stats->getAllCTCUrlsMap();
$statsHistory = $this->entityManager->getRepository(StatsHistory::class)
->createQueryBuilder('sh')
->where('sh.stats = :stats')
->setParameter('stats', $stats)
->orderBy('sh.id', 'DESC')
2025-06-26 23:40:37 +02:00
->setMaxResults(100)
2025-06-21 11:28:31 +02:00
->getQuery()
2025-06-27 00:35:11 +02:00
->getResult();
2025-06-21 11:28:31 +02:00
// Données pour le graphique des modifications par trimestre
$modificationsByQuarter = [];
2025-08-31 12:29:41 +02:00
$totalDates = 0;
$sumDates = 0;
$averageUpdateDate = null;
$daysSinceUpdate = null;
2025-08-08 19:04:42 +02:00
$commerces = $stats->getPlaces();
if (isset($commerces) && count($commerces) > 0 && is_iterable($commerces)) {
2025-08-31 12:29:41 +02:00
$allDates = [];
2025-08-08 18:51:44 +02:00
foreach ($commerces as $commerce) {
2025-08-31 12:29:41 +02:00
$date = null;
2025-08-08 18:51:44 +02:00
if ($commerce->getOsmDataDate()) {
$date = $commerce->getOsmDataDate();
2025-08-31 12:29:41 +02:00
$allDates[] = $date;
$totalDates++;
$sumDates += $date->getTimestamp();
2025-08-08 18:51:44 +02:00
}
2025-08-31 12:29:41 +02:00
if ($date) {
$year = $date->format('Y');
$quarter = ceil($date->format('n') / 3);
$key = $year . '-Q' . $quarter;
if (!isset($modificationsByQuarter[$key])) {
$modificationsByQuarter[$key] = 0;
}
$modificationsByQuarter[$key]++;
2025-06-21 11:28:31 +02:00
}
2025-08-31 12:29:41 +02:00
}
// Calculer la date moyenne de mise à jour
if ($totalDates > 0) {
$averageTimestamp = $sumDates / $totalDates;
$averageUpdateDate = new \DateTime();
$averageUpdateDate->setTimestamp((int)$averageTimestamp);
// Calculer le nombre de jours depuis la date moyenne
$now = new \DateTime();
$daysSinceUpdate = $now->diff($averageUpdateDate)->days;
2025-06-19 12:49:30 +02:00
}
}
2025-06-21 11:28:31 +02:00
ksort($modificationsByQuarter); // Trier par clé (année-trimestre)
2025-06-19 12:49:30 +02:00
$geojson = [
'type' => 'FeatureCollection',
'features' => []
];
2025-08-08 19:04:42 +02:00
if (isset($commerces) && is_iterable($commerces)) {
foreach ($commerces as $commerce) {
if ($commerce->getLat() && $commerce->getLon()) {
// Collect missing tags
$missingTags = [];
if (!$commerce->getName()) $missingTags[] = 'name';
if (!$commerce->hasAddress()) $missingTags[] = 'address';
if (!$commerce->hasOpeningHours()) $missingTags[] = 'opening_hours';
if (!$commerce->hasWebsite()) $missingTags[] = 'website';
if (!$commerce->hasWheelchair()) $missingTags[] = 'wheelchair';
if (!$commerce->getSiret()) $missingTags[] = 'siret';
2025-08-08 19:04:42 +02:00
$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(),
'completion' => $commerce->getCompletionPercentage(),
'missing_tags' => $missingTags
2025-08-08 19:04:42 +02:00
]
];
}
}
}
2025-05-27 12:17:46 +02:00
2025-06-27 11:14:27 +02:00
// Générer le podium local des contributeurs OSM pour cette ville
$placeRepo = $this->entityManager->getRepository(\App\Entity\Place::class);
$qb = $placeRepo->createQueryBuilder('p')
->select(
'p.osm_user',
'COUNT(p.id) as nb',
'AVG((CASE WHEN p.has_opening_hours = true THEN 1 ELSE 0 END) +'
2025-08-08 18:51:44 +02:00
. ' (CASE WHEN p.has_address = true THEN 1 ELSE 0 END) +'
. ' (CASE WHEN p.has_website = true THEN 1 ELSE 0 END) +'
. ' (CASE WHEN p.has_wheelchair = true THEN 1 ELSE 0 END) +'
. ' (CASE WHEN p.has_note = true THEN 1 ELSE 0 END)) / 5 * 100 as completion_moyen'
2025-06-27 11:14:27 +02:00
)
->where('p.osm_user IS NOT NULL')
->andWhere("p.osm_user != ''")
->andWhere('p.stats = :stats')
->setParameter('stats', $stats)
->groupBy('p.osm_user')
->orderBy('nb', 'DESC')
->setMaxResults(100);
$podium_local = $qb->getQuery()->getResult();
// Calcul du score pondéré et normalisation locale
$maxPondere = 0;
foreach ($podium_local as &$row) {
$row['completion_moyen'] = $row['completion_moyen'] !== null ? round($row['completion_moyen'], 1) : null;
$row['completion_pondere'] = ($row['completion_moyen'] !== null && $row['nb'] > 0)
? round($row['completion_moyen'] * $row['nb'], 1)
: null;
if ($row['completion_pondere'] !== null && $row['completion_pondere'] > $maxPondere) {
$maxPondere = $row['completion_pondere'];
}
}
unset($row);
2025-07-05 16:15:56 +02:00
2025-06-29 11:29:48 +02:00
// Normalisation des scores pondérés entre 0 et 100
foreach ($podium_local as &$row) {
if ($maxPondere > 0 && $row['completion_pondere'] !== null) {
$row['completion_pondere_normalisee'] = round($row['completion_pondere'] / $maxPondere * 100, 1);
} else {
$row['completion_pondere_normalisee'] = null;
2025-06-27 11:14:27 +02:00
}
}
2025-06-29 11:29:48 +02:00
unset($row);
2025-07-05 16:15:56 +02:00
2025-06-29 11:29:48 +02:00
// Tri décroissant sur le score normalisé
2025-06-27 11:14:27 +02:00
usort($podium_local, function ($a, $b) {
return ($b['completion_pondere_normalisee'] ?? 0) <=> ($a['completion_pondere_normalisee'] ?? 0);
});
// Récupérer les derniers followups pour chaque type
$latestFollowups = [];
2025-06-29 20:02:51 +02:00
$types = array_keys(\App\Service\FollowUpService::getFollowUpThemes());
foreach ($types as $type) {
$count = null;
$completion = null;
foreach ($stats->getCityFollowUps() as $fu) {
if ($fu->getName() === $type . '_count') {
2025-06-29 20:02:51 +02:00
if ($count === null) {
$count = $fu;
} else if ($fu->getDate() > $count->getDate()) {
$count = $fu;
}
}
if ($fu->getName() === $type . '_completion') {
2025-06-29 20:02:51 +02:00
if ($completion === null) {
$completion = $fu;
} else if ($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') {
2025-06-29 20:02:51 +02:00
if ($count === null) {
$count = $fu;
} else if ($fu->getDate() > $count->getDate()) {
$count = $fu;
}
}
if ($fu->getName() === 'places_completion') {
2025-06-29 20:02:51 +02:00
if ($completion === null) {
$completion = $fu;
} else if ($fu->getDate() > $completion->getDate()) {
$completion = $fu;
}
}
}
$latestFollowups['places'] = [];
if ($count) $latestFollowups['places']['count'] = $count;
if ($completion) $latestFollowups['places']['completion'] = $completion;
2025-07-05 10:29:53 +02:00
// Calculer la progression sur 7 jours pour chaque type
$progression7Days = [];
foreach ($types as $type) {
$progression7Days[$type] = \App\Service\FollowUpService::calculate7DayProgression($stats, $type);
}
$progression7Days['places'] = \App\Service\FollowUpService::calculate7DayProgression($stats, 'places');
2025-07-16 17:00:09 +02:00
2025-07-14 18:17:41 +02:00
// --- Ajout : mesures CTC CityFollowUp pour le graphique d'évolution ---
$ctc_completion_series = [];
foreach ($stats->getCityFollowUps() as $fu) {
// On ne prend que les types *_count importés CTC (name_count, hours_count, etc.)
if (preg_match('/^(name|hours|website|address|siret)_count$/', $fu->getName())) {
$ctc_completion_series[$fu->getName()][] = [
'date' => $fu->getDate()->format('Y-m-d'),
'value' => $fu->getMeasure(),
];
}
}
// Tri par date dans chaque série
foreach ($ctc_completion_series as &$points) {
2025-08-08 18:51:44 +02:00
usort($points, function ($a, $b) {
2025-07-14 18:17:41 +02:00
return strcmp($a['date'], $b['date']);
});
}
unset($points);
2025-07-05 10:29:53 +02:00
return $this->render('admin/stats.html.twig', [
'stats' => $stats,
2025-06-21 11:28:31 +02:00
'commerces' => $commerces,
'urls' => $urls,
'geojson' => json_encode($geojson),
2025-06-21 11:28:31 +02:00
'modificationsByQuarter' => json_encode($modificationsByQuarter),
'maptiler_token' => $_ENV['MAPTILER_TOKEN'],
2025-06-17 18:27:19 +02:00
'statsHistory' => $statsHistory,
2025-06-18 00:41:24 +02:00
'CTC_urls' => $urls,
2025-06-27 11:14:27 +02:00
'overpass' => '',
'podium_local' => $podium_local,
2025-06-29 19:24:00 +02:00
'latestFollowups' => $latestFollowups,
'followup_labels' => \App\Service\FollowUpService::getFollowUpThemes(),
'followup_icons' => \App\Service\FollowUpService::getFollowUpIcons(),
2025-08-08 18:51:44 +02:00
'progression7Days' => $progression7Days,
2025-07-05 15:25:33 +02:00
'all_types' => \App\Service\FollowUpService::getFollowUpThemes(),
2025-07-05 16:53:12 +02:00
'getTagEmoji' => [self::class, 'getTagEmoji'],
'completion_tags' => \App\Service\FollowUpService::getFollowUpCompletionTags(),
2025-07-14 18:17:41 +02:00
'ctc_completion_series' => $ctc_completion_series,
2025-08-31 12:29:41 +02:00
'averageUpdateDate' => $averageUpdateDate,
'daysSinceUpdate' => $daysSinceUpdate,
]);
}
2025-07-05 15:25:33 +02:00
#[Route('/admin/stats/{insee_code}/followup-graph/{theme}', name: 'admin_followup_theme_graph', requirements: ['insee_code' => '\d+'])]
2025-07-05 12:37:01 +02:00
public function followupThemeGraph(string $insee_code, string $theme): Response
{
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
if (!$stats) {
// Si aucune stats n'existe, rechercher dans l'API geo.api.gouv.fr
$apiUrl = "https://geo.api.gouv.fr/communes/{$insee_code}";
$response = @file_get_contents($apiUrl);
if ($response === false) {
$this->addFlash('error', 'Aucune stats trouvée pour ce code INSEE et impossible de récupérer les informations depuis l\'API geo.api.gouv.fr.');
return $this->redirectToRoute('app_admin');
}
$communeData = json_decode($response, true);
if (!$communeData || !isset($communeData['nom'])) {
$this->addFlash('error', 'Aucune commune trouvée avec ce code INSEE dans l\'API geo.api.gouv.fr.');
return $this->redirectToRoute('app_admin');
}
// Créer un nouvel objet Stats avec les données de l'API
$stats = new Stats();
$stats->setZone($insee_code)
->setName($communeData['nom'])
->setDateCreated(new \DateTime())
->setDateModified(new \DateTime())
->setKind('request');
// Ajouter la population si disponible
if (isset($communeData['population'])) {
$stats->setPopulation($communeData['population']);
}
// Ajouter les coordonnées si disponibles
if (isset($communeData['centre']) && isset($communeData['centre']['coordinates'])) {
$stats->setLon((string)$communeData['centre']['coordinates'][0]);
$stats->setLat((string)$communeData['centre']['coordinates'][1]);
}
// Ajouter les codes postaux si disponibles
if (isset($communeData['codesPostaux']) && !empty($communeData['codesPostaux'])) {
$stats->setCodesPostaux(implode(',', $communeData['codesPostaux']));
}
// Ajouter le code EPCI si disponible
if (isset($communeData['codeEpci'])) {
$stats->setCodeEpci((int)$communeData['codeEpci']);
}
// Ajouter le SIREN si disponible
if (isset($communeData['siren'])) {
$stats->setSiren((int)$communeData['siren']);
}
// Persister l'objet Stats
$this->entityManager->persist($stats);
$this->entityManager->flush();
$this->addFlash('success', 'Nouvelle commune ajoutée à partir des données de l\'API geo.api.gouv.fr.');
2025-07-05 12:37:01 +02:00
}
$themes = \App\Service\FollowUpService::getFollowUpThemes();
if (!isset($themes[$theme])) {
$this->addFlash('error', 'Thème non reconnu.');
return $this->redirectToRoute('app_admin_stats', ['insee_code' => $insee_code]);
}
// Récupérer toutes les données de followup pour ce thème
$followups = $stats->getCityFollowUps();
$countData = [];
$completionData = [];
2025-07-05 16:15:56 +02:00
2025-07-05 12:37:01 +02:00
foreach ($followups as $fu) {
if ($fu->getName() === $theme . '_count') {
$countData[] = [
'date' => $fu->getDate()->format('Y-m-d'),
'value' => $fu->getMeasure()
];
}
if ($fu->getName() === $theme . '_completion') {
$completionData[] = [
'date' => $fu->getDate()->format('Y-m-d'),
'value' => $fu->getMeasure()
];
}
}
// Trier par date
usort($countData, fn($a, $b) => $a['date'] <=> $b['date']);
usort($completionData, fn($a, $b) => $a['date'] <=> $b['date']);
2025-07-05 15:25:33 +02:00
// Récupérer les objets du thème (Place) pour la ville
$places = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]);
$motocultrice = $this->motocultrice;
$objects = [];
// Récupérer la correspondance thème <-> requête Overpass
$themeQueries = \App\Service\FollowUpService::getFollowUpOverpassQueries();
$overpass_type_query = $themeQueries[$theme] ?? '';
if ($overpass_type_query) {
$overpass_query = "[out:json][timeout:60];\narea[\"ref:INSEE\"=\"$insee_code\"]->.searchArea;\n($overpass_type_query);\n(._;>;);\nout meta;\n>;";
$josm_url = 'http://127.0.0.1:8111/import?url=https://overpass-api.de/api/interpreter?data=' . urlencode($overpass_query);
} else {
$josm_url = null;
}
// Fonction utilitaire pour extraire clé/valeur de la requête Overpass
2025-07-05 16:15:56 +02:00
$extractTag = function ($query) {
2025-07-05 15:25:33 +02:00
if (preg_match('/\\[([a-zA-Z0-9:_-]+)\\]="([^"]+)"/', $query, $matches)) {
return [$matches[1], $matches[2]];
}
return [null, null];
};
list($tagKey, $tagValue) = $extractTag($themeQueries[$theme] ?? '');
2025-07-05 17:47:00 +02:00
// DEBUG : journaliser les main_tag et le filtrage
$all_main_tags = array_map(fn($p) => $p->getMainTag(), $places);
$debug_info = [
'theme' => $theme,
'tagKey' => $tagKey,
'tagValue' => $tagValue,
'main_tags' => $all_main_tags,
'places_count' => count($places),
];
$debug_filtered = [];
2025-07-05 15:25:33 +02:00
foreach ($places as $place) {
$match = false;
2025-07-05 17:35:20 +02:00
$main_tag = $place->getMainTag();
// Cas particuliers multi-valeurs (ex: healthcare)
2025-07-05 15:25:33 +02:00
if ($theme === 'healthcare') {
if ($main_tag && (
2025-08-08 18:51:44 +02:00
str_starts_with($main_tag, 'healthcare=') ||
in_array($main_tag, [
'amenity=doctors',
'amenity=pharmacy',
'amenity=hospital',
'amenity=clinic',
'amenity=social_facility'
])
)) {
2025-07-05 15:25:33 +02:00
$match = true;
}
2025-07-05 17:35:20 +02:00
} else {
// Détection générique : si le mainTag correspond à la clé/valeur du thème
if ($tagKey && $tagValue && $main_tag === "$tagKey=$tagValue") {
2025-07-05 15:25:33 +02:00
$match = true;
}
}
2025-07-05 17:35:20 +02:00
// Ajouter l'objet si match
2025-07-05 15:25:33 +02:00
if ($match && $place->getLat() && $place->getLon()) {
$objects[] = [
'id' => $place->getOsmId(),
'osm_kind' => $place->getOsmKind(),
'lat' => $place->getLat(),
'lon' => $place->getLon(),
'name' => $place->getName(),
2025-07-05 16:15:56 +02:00
'tags' => ['main_tag' => $place->getMainTag()],
2025-07-05 15:25:33 +02:00
'is_complete' => !empty($place->getName()),
'osm_url' => 'https://www.openstreetmap.org/' . $place->getOsmKind() . '/' . $place->getOsmId(),
'uuid' => $place->getUuidForUrl(),
'zip_code' => $place->getZipCode(),
2025-07-05 15:25:33 +02:00
];
2025-07-05 17:47:00 +02:00
$debug_filtered[] = $main_tag;
2025-07-05 15:25:33 +02:00
}
}
2025-07-05 17:47:00 +02:00
$debug_info['filtered_count'] = count($objects);
$debug_info['filtered_main_tags'] = $debug_filtered;
if (property_exists($this, 'actionLogger') && $this->actionLogger) {
$this->actionLogger->log('[DEBUG][followupThemeGraph]', $debug_info);
} else {
error_log('[DEBUG][followupThemeGraph] ' . json_encode($debug_info));
}
2025-07-05 15:25:33 +02:00
$geojson = [
'type' => 'FeatureCollection',
2025-07-05 16:15:56 +02:00
'features' => array_map(function ($obj) {
2025-07-05 15:25:33 +02:00
return [
'type' => 'Feature',
'geometry' => [
'type' => 'Point',
'coordinates' => [$obj['lon'], $obj['lat']]
],
'properties' => $obj
];
}, $objects)
];
// Centre de la carte : centre géographique des objets ou de la ville
$center = null;
if (count($objects) > 0) {
$lat = array_sum(array_column($objects, 'lat')) / count($objects);
$lon = array_sum(array_column($objects, 'lon')) / count($objects);
$center = [$lon, $lat];
} elseif ($stats->getPlaces()->count() > 0) {
$first = $stats->getPlaces()->first();
$center = [$first->getLon(), $first->getLat()];
}
2025-07-05 12:37:01 +02:00
2025-08-12 11:23:20 +02:00
// Calculate current metrics from objects array (from Overpass data)
$currentCount = count($objects);
2025-08-12 11:23:20 +02:00
// Calculate current completion percentage
$completionTags = \App\Service\FollowUpService::getFollowUpCompletionTags()[$theme] ?? [];
$currentCompletion = 0;
2025-08-12 11:23:20 +02:00
if ($currentCount > 0 && !empty($completionTags)) {
$totalTags = count($completionTags) * $currentCount;
$filledTags = 0;
2025-08-12 11:23:20 +02:00
foreach ($objects as $obj) {
// Get the original Place object to check tags
$place = null;
foreach ($places as $p) {
if ($p->getOsmId() === $obj['id'] && $p->getOsmKind() === $obj['osm_kind']) {
$place = $p;
break;
}
}
2025-08-12 11:23:20 +02:00
if ($place) {
foreach ($completionTags as $tag) {
// Simple check for name tag
if ($tag === 'name' && !empty($place->getName())) {
$filledTags++;
}
// Add more tag checks as needed
}
}
}
2025-08-12 11:23:20 +02:00
$currentCompletion = $totalTags > 0 ? round(($filledTags / $totalTags) * 100) : 0;
}
2025-08-12 11:23:20 +02:00
// Add current data to history if empty
if (empty($countData)) {
$countData[] = [
'date' => (new \DateTime())->format('Y-m-d'),
'value' => $currentCount
];
}
2025-08-12 11:23:20 +02:00
if (empty($completionData)) {
$completionData[] = [
'date' => (new \DateTime())->format('Y-m-d'),
'value' => $currentCompletion
];
}
2025-07-05 12:37:01 +02:00
return $this->render('admin/followup_theme_graph.html.twig', [
'stats' => $stats,
'theme' => $theme,
'theme_label' => $themes[$theme],
2025-08-12 11:23:20 +02:00
'count_data' => $countData,
'completion_data' => $completionData,
'current_count' => $currentCount,
'current_completion' => $currentCompletion,
2025-07-05 12:37:01 +02:00
'icons' => \App\Service\FollowUpService::getFollowUpIcons(),
'followup_labels' => $themes,
2025-07-05 15:25:33 +02:00
'geojson' => json_encode($geojson),
'overpass_query' => $overpass_query,
2025-07-05 15:25:33 +02:00
'josm_url' => $josm_url,
'center' => $center,
'maptiler_token' => $_ENV['MAPTILER_TOKEN'] ?? null,
'completion_tags' => \App\Service\FollowUpService::getFollowUpCompletionTags(),
2025-07-05 17:47:00 +02:00
'debug_info' => $debug_info,
2025-07-05 12:37:01 +02:00
]);
}
2025-06-01 23:35:15 +02:00
#[Route('/admin/placeType/{osm_kind}/{osm_id}', name: 'app_admin_by_osm_id')]
public function placeType(string $osm_kind, string $osm_id): Response
{
2025-07-05 16:15:56 +02:00
2025-06-01 23:35:15 +02:00
$place = $this->entityManager->getRepository(Place::class)->findOneBy(['osm_kind' => $osm_kind, 'osmId' => $osm_id]);
2025-06-27 00:35:11 +02:00
if ($place) {
2025-07-05 16:15:56 +02:00
$this->actionLogger->log('admin/placeType', [
'osm_kind' => $osm_kind,
'osm_id' => $osm_id,
'name' => $place->getName(),
'code_insee' => $place->getZipCode(),
'uuid' => $place->getUuidForUrl()
]);
2025-06-01 23:35:15 +02:00
return $this->redirectToRoute('app_admin_commerce', ['id' => $place->getId()]);
} else {
2025-07-05 12:37:01 +02:00
$this->actionLogger->log('ERROR_admin/placeType', ['osm_kind' => $osm_kind, 'osm_id' => $osm_id]);
2025-06-01 23:35:15 +02:00
$this->addFlash('error', 'Le lieu n\'existe pas.');
2025-06-27 00:35:11 +02:00
$this->actionLogger->log('ERROR_admin/placeType', ['osm_kind' => $osm_kind, 'osm_id' => $osm_id]);
2025-06-01 23:35:15 +02:00
return $this->redirectToRoute('app_public_index');
}
}
/**
* rediriger vers l'url unique quand on est admin
*/
2025-05-28 16:24:34 +02:00
#[Route('/admin/commerce/{id}', name: 'app_admin_commerce')]
public function commerce(int $id): Response
{
2025-06-27 00:35:11 +02:00
2025-05-28 16:24:34 +02:00
// 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é');
2025-06-27 00:35:11 +02:00
$this->actionLogger->log('ERROR_admin_show_commerce_form_id', ['id' => $id]);
2025-05-28 16:24:34 +02:00
}
$this->actionLogger->log('admin_show_commerce_form_id', [
2025-06-27 00:35:11 +02:00
'id' => $id,
'name' => $commerce->getName(),
'code_insee' => $commerce->getZipCode(),
'uuid' => $commerce->getUuidForUrl()
]);
2025-05-28 16:24:34 +02:00
// Redirection vers la page de modification avec les paramètres nécessaires
return $this->redirectToRoute('app_public_edit', [
'zipcode' => $commerce->getZipCode(),
2025-06-27 00:35:11 +02:00
'name' => $commerce->getName() != '' ? $commerce->getName() : '?',
2025-05-28 16:24:34 +02:00
'uuid' => $commerce->getUuidForUrl()
]);
}
/**
2025-06-17 18:27:19 +02:00
* récupérer les commerces de la zone selon le code INSEE, créer les nouveaux lieux, et mettre à jour les existants
*/
2025-06-17 18:27:19 +02:00
#[Route('/admin/labourer/{insee_code}', name: 'app_admin_labourer')]
2025-06-21 11:28:31 +02:00
public function labourer(Request $request, string $insee_code, bool $updateExisting = true): Response
{
2025-06-21 11:28:31 +02:00
$deleteMissing = $request->query->getBoolean('deleteMissing', true);
2025-07-05 10:29:53 +02:00
$disableFollowUpCleanup = $request->query->getBoolean('disableFollowUpCleanup', false);
2025-07-05 14:31:50 +02:00
$debug = $request->query->getBoolean('debug', false);
2025-06-26 23:40:37 +02:00
$this->actionLogger->log('labourer', ['insee_code' => $insee_code]);
// 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.');
2025-06-26 23:40:37 +02:00
$this->actionLogger->log('ERROR_labourer_bad_insee', ['insee_code' => $insee_code]);
return $this->redirectToRoute('app_public_index');
}
2025-07-14 18:17:41 +02:00
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
if (!$stats) {
2025-07-15 21:46:30 +02:00
$stats = new Stats();
$stats->setZone($insee_code);
$stats->setKind('request'); // Set the kind to 'request' as it's created from an admin request
2025-07-15 21:46:30 +02:00
// $this->addFlash('error', '3 Aucune stats trouvée pour ce code INSEE.');
// return $this->redirectToRoute('app_public_index');
2025-07-14 18:17:41 +02:00
}
2025-07-15 23:23:32 +02:00
// Compléter le nom si manquant
if (!$stats->getName()) {
$cityName = $this->motocultrice->get_city_osm_from_zip_code($insee_code);
if ($cityName) {
$stats->setName($cityName);
}
}
// Compléter la population si manquante
if (!$stats->getPopulation()) {
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'])) {
$stats->setPopulation((int)$data['population']);
}
}
2025-08-08 18:51:44 +02:00
} catch (\Exception $e) {
}
2025-07-15 23:23:32 +02:00
}
// Compléter le budget si manquant
if (!$stats->getBudgetAnnuel()) {
$budget = $this->budgetService->getBudgetAnnuel($insee_code);
if ($budget !== null) {
$stats->setBudgetAnnuel((string)$budget);
}
}
// Compléter les lieux d'intérêt si manquants (lat/lon)
if (!$stats->getLat() || !$stats->getLon()) {
// On tente de récupérer le centre de la ville via l'API geo.gouv.fr
try {
$apiUrl = 'https://geo.api.gouv.fr/communes/' . $insee_code . '?fields=centre';
$response = @file_get_contents($apiUrl);
if ($response !== false) {
$data = json_decode($response, true);
if (isset($data['centre']['coordinates']) && count($data['centre']['coordinates']) === 2) {
$stats->setLon((string)$data['centre']['coordinates'][0]);
$stats->setLat((string)$data['centre']['coordinates'][1]);
}
}
2025-08-08 18:51:44 +02:00
} catch (\Exception $e) {
}
2025-07-15 23:23:32 +02:00
}
2025-07-14 18:17:41 +02:00
// Mettre à jour la date de requête de labourage
$stats->setDateLabourageRequested(new \DateTime());
2025-08-02 11:19:57 +02:00
$stats->computeCompletionPercent();
2025-08-02 11:22:09 +02:00
2025-07-14 18:17:41 +02:00
$this->entityManager->persist($stats);
$this->entityManager->flush();
2025-06-27 00:35:11 +02:00
2025-07-14 18:17:41 +02:00
// Vérifier la RAM disponible (>= 1 Go)
$meminfo = @file_get_contents('/proc/meminfo');
$ram_ok = false;
if ($meminfo !== false && preg_match('/^MemAvailable:\s+(\d+)/m', $meminfo, $matches)) {
$mem_kb = (int)$matches[1];
$ram_ok = ($mem_kb >= 1024 * 1024); // 1 Go
}
if ($ram_ok) {
// Effectuer le labourage complet (objets Place)
// ... (reprendre ici la logique existante de création/màj des objets Place) ...
// À la fin, mettre à jour la date de fin de labourage
$stats->setDateLabourageDone(new \DateTime());
2025-06-05 16:20:20 +02:00
$this->entityManager->persist($stats);
$this->entityManager->flush();
2025-07-14 18:17:41 +02:00
$this->addFlash('success', 'Labourage effectué immédiatement (RAM disponible suffisante).');
} else {
// Ne pas toucher aux objets Place, juste message flash
$this->addFlash('warning', "Le serveur est trop sollicité actuellement (RAM insuffisante). La mise à jour des lieux sera effectuée plus tard automatiquement.");
}
2025-07-14 18:17:41 +02:00
// Toujours générer les CityFollowUp (mais ne jamais les supprimer)
2025-07-15 21:46:30 +02:00
// $themes = \App\Service\FollowUpService::getFollowUpThemes();
// foreach (array_keys($themes) as $theme) {
2025-08-08 18:51:44 +02:00
$this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager, true);
2025-07-15 21:46:30 +02:00
// }
2025-07-14 18:17:41 +02:00
$this->entityManager->flush();
2025-06-17 19:38:44 +02:00
return $this->redirectToRoute('app_admin_stats', ['insee_code' => $insee_code]);
}
#[Route('/admin/delete/{id}', name: 'app_admin_delete')]
public function delete(int $id): Response
{
2025-06-26 23:56:51 +02:00
$this->actionLogger->log('admin/delete_place', ['id' => $id]);
$commerce = $this->entityManager->getRepository(Place::class)->find($id);
2025-06-27 00:35:11 +02:00
if ($commerce) {
2025-05-28 16:24:34 +02:00
$this->entityManager->remove($commerce);
2025-06-27 00:35:11 +02:00
$this->entityManager->flush();
2025-06-27 00:35:11 +02:00
$this->addFlash('success', 'Le lieu ' . $commerce->getName() . ' a été supprimé avec succès de OSM Mes commerces, mais pas dans OpenStreetMap.');
2025-05-28 16:24:34 +02:00
} else {
$this->addFlash('error', 'Le lieu n\'existe pas.');
}
2025-05-28 16:24:34 +02:00
return $this->redirectToRoute('app_public_dashboard');
}
2025-06-17 18:27:19 +02:00
#[Route('/admin/delete_by_zone/{insee_code}', name: 'app_admin_delete_by_zone')]
public function delete_by_zone(string $insee_code): Response
{
2025-06-26 23:56:51 +02:00
$this->actionLogger->log('admin/delete_by_zone', ['insee_code' => $insee_code]);
2025-06-17 18:27:19 +02:00
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
2025-06-21 11:28:31 +02:00
if (!$stats) {
$this->addFlash('error', 'Aucune statistique trouvée pour la zone ' . $insee_code);
return $this->redirectToRoute('app_public_dashboard');
}
2025-06-21 11:28:31 +02:00
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());
}
2025-05-28 16:24:34 +02:00
return $this->redirectToRoute('app_public_dashboard');
}
#[Route('/admin/export', name: 'app_admin_export')]
public function export(): Response
{
2025-06-26 23:40:37 +02:00
$this->actionLogger->log('export_all_places', []);
$places = $this->entityManager->getRepository(Place::class)->findAll();
2025-06-27 00:35:11 +02:00
$csvData = [];
$csvData[] = [
'Nom',
2025-06-27 00:35:11 +02:00
'Email',
'Code postal',
'ID OSM',
'Type OSM',
'Date de modification',
'Date dernier contact',
'Note',
'Désabonné',
'Inactif',
'Support humain demandé',
'A des horaires',
2025-06-27 00:35:11 +02:00
'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',
2025-06-27 00:35:11 +02:00
$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);
2025-06-27 00:35:11 +02:00
return $response;
}
2025-08-08 18:51:44 +02:00
2025-06-17 18:27:19 +02:00
#[Route('/admin/export_csv/{insee_code}', name: 'app_admin_export_csv')]
public function export_csv(string $insee_code): Response
2025-06-27 00:35:11 +02:00
{
2025-06-26 23:56:51 +02:00
$this->actionLogger->log('admin/export_csv', ['insee_code' => $insee_code]);
2025-06-17 18:27:19 +02:00
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
$response = new Response($this->motocultrice->export($insee_code));
2025-06-03 13:04:09 +02:00
$response->headers->set('Content-Type', 'text/csv');
2025-06-27 00:35:11 +02:00
2025-06-03 13:04:09 +02:00
$slug_name = str_replace(' ', '-', $stats->getName());
2025-06-03 16:19:07 +02:00
2025-06-27 00:35:11 +02:00
$this->actionLogger->log('export_csv', ['insee_code' => $insee_code, 'slug_name' => $slug_name]);
2025-06-26 23:40:37 +02:00
2025-06-17 18:27:19 +02:00
$response->headers->set('Content-Disposition', 'attachment; filename="osm-commerces-export_' . $insee_code . '_' . $slug_name . '_' . date('Y-m-d_H-i-s') . '.csv"');
2025-06-03 13:04:09 +02:00
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
{
2025-06-26 23:56:51 +02:00
$this->actionLogger->log('admin/make_email_for_place', ['insee_code' => $place->getId()]);
2025-06-27 00:35:11 +02:00
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
{
2025-06-27 00:35:11 +02:00
$this->actionLogger->log('no_more_sollicitation_for_place', ['place_id' => $place->getId()]);
2025-06-26 23:40:37 +02:00
$place->setOptedOut(true);
$this->entityManager->persist($place);
$this->entityManager->flush();
2025-06-27 00:35:11 +02:00
$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
{
2025-06-27 00:35:11 +02:00
$this->actionLogger->log('send_email_to_place', ['place_id' => $place->getId()]);
// 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.');
2025-06-27 00:35:11 +02:00
$this->actionLogger->log('could_not_send_email_to_opted_out_place', ['place_id' => $place->getId()]);
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);
2025-06-26 23:40:37 +02:00
try {
$mailer->send($email);
} catch (\Throwable $e) {
$this->actionLogger->log('ERROR_envoi_email', [
'place_id' => $place->getId(),
'message' => $e->getMessage(),
]);
$this->addFlash('error', 'Erreur lors de l\'envoi de l\'email : ' . $e->getMessage());
return $this->redirectToRoute('app_public_index');
}
// Mettre à jour la date de dernier contact
$place->setLastContactAttemptDate(new \DateTime());
$this->entityManager->persist($place);
$this->entityManager->flush();
2025-06-26 23:40:37 +02:00
$place->setLastContactAttemptDate(new \DateTime());
2025-06-27 00:35:11 +02:00
$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');
2025-06-27 00:35:11 +02:00
}
2025-06-24 00:29:15 +02:00
#[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
{
2025-06-26 23:40:37 +02:00
// Ajout d'un log d'action avec le service ActionLogger
2025-06-27 00:35:11 +02:00
$this->actionLogger->log('fraicheur/calculate', []);
2025-06-24 00:29:15 +02:00
$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
];
2025-06-27 00:35:11 +02:00
$filesystem->dumpFile($jsonPath, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
2025-06-24 00:29:15 +02:00
// --- 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
];
2025-06-27 00:35:11 +02:00
$filesystem->dumpFile($distJsonPath, json_encode($distData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
2025-06-24 00:29:15 +02:00
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
];
2025-06-27 00:35:11 +02:00
$filesystem->dumpFile($jsonPath, json_encode($distData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
2025-06-24 00:29:15 +02:00
}
$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]);
}
#[Route('/admin/labourer-tous-les-budgets', name: 'app_admin_labourer_tous_les_budgets')]
public function labourerTousLesBudgets(): Response
{
$statsRepo = $this->entityManager->getRepository(Stats::class);
$query = $statsRepo->createQueryBuilder('s')->getQuery();
$allStats = $query->toIterable();
$budgetsMisAJour = 0;
foreach ($allStats as $stat) {
if (!$stat->getBudgetAnnuel() && $stat->getZone()) {
$budget = $this->budgetService->getBudgetAnnuel($stat->getZone());
if ($budget !== null) {
$stat->setBudgetAnnuel((string)$budget);
$this->entityManager->persist($stat);
$budgetsMisAJour++;
continue;
}
}
}
if ($budgetsMisAJour > 0) {
$this->entityManager->flush();
}
2025-06-27 00:35:11 +02:00
$this->addFlash('success', $budgetsMisAJour . ' budgets mis à jour.');
return $this->redirectToRoute('app_admin');
}
#[Route('/admin/import-stats-from-csv', name: 'app_admin_import_stats_from_csv')]
public function importStatsFromCsv(): Response
{
$this->actionLogger->log('admin/import_stats_from_csv', []);
$csvFile = 'communes_france.csv';
if (!file_exists($csvFile)) {
$this->addFlash('error', 'Le fichier CSV des communes n\'existe pas. Veuillez exécuter le script fetch_communes.py pour le générer.');
return $this->redirectToRoute('app_admin');
}
$statsRepo = $this->entityManager->getRepository(Stats::class);
$createdCount = 0;
$skippedCount = 0;
$errorCount = 0;
// Ouvrir le fichier CSV
$handle = fopen($csvFile, 'r');
if (!$handle) {
$this->addFlash('error', 'Impossible d\'ouvrir le fichier CSV des communes.');
return $this->redirectToRoute('app_admin');
}
// Lire l'en-tête pour déterminer les indices des colonnes
$header = fgetcsv($handle);
$indices = array_flip($header);
// Vérifier que les colonnes nécessaires existent
$requiredColumns = ['code', 'nom'];
foreach ($requiredColumns as $column) {
if (!isset($indices[$column])) {
$this->addFlash('error', "La colonne '$column' est manquante dans le fichier CSV.");
fclose($handle);
return $this->redirectToRoute('app_admin');
}
}
// Traiter chaque ligne du CSV
while (($data = fgetcsv($handle)) !== false) {
try {
$inseeCode = $data[$indices['code']];
// Vérifier si une Stats existe déjà pour ce code INSEE
$existingStat = $statsRepo->findOneBy(['zone' => $inseeCode]);
if ($existingStat) {
$skippedCount++;
continue;
}
// Créer un nouvel objet Stats
$stat = new Stats();
$stat->setZone($inseeCode)
->setDateCreated(new \DateTime())
->setDateModified(new \DateTime())
->setKind('request');
// Ajouter le nom si disponible
if (isset($indices['nom']) && !empty($data[$indices['nom']])) {
$stat->setName($data[$indices['nom']]);
}
// Ajouter la population si disponible
if (isset($indices['population']) && !empty($data[$indices['population']])) {
$stat->setPopulation((int)$data[$indices['population']]);
}
// Ajouter les codes postaux si disponibles
if (isset($indices['codesPostaux']) && !empty($data[$indices['codesPostaux']])) {
$stat->setCodesPostaux($data[$indices['codesPostaux']]);
}
// Ajouter le SIREN si disponible
if (isset($indices['siren']) && !empty($data[$indices['siren']])) {
$stat->setSiren((int)$data[$indices['siren']]);
}
// Ajouter le code EPCI si disponible
if (isset($indices['codeEpci']) && !empty($data[$indices['codeEpci']])) {
$stat->setCodeEpci((int)$data[$indices['codeEpci']]);
}
// Ajouter les coordonnées si disponibles
if (isset($indices['longitude']) && isset($indices['latitude']) &&
!empty($data[$indices['longitude']]) && !empty($data[$indices['latitude']])) {
$stat->setLon((string)$data[$indices['longitude']])
->setLat((string)$data[$indices['latitude']]);
}
// Persister l'objet Stats
$this->entityManager->persist($stat);
$createdCount++;
// Flush tous les 100 objets pour éviter de surcharger la mémoire
if ($createdCount % 100 === 0) {
$this->entityManager->flush();
$this->entityManager->clear(Stats::class);
}
} catch (\Exception $e) {
$errorCount++;
}
}
// Flush les derniers objets
$this->entityManager->flush();
fclose($handle);
$this->addFlash('success', "Import terminé : $createdCount communes ajoutées, $skippedCount déjà existantes, $errorCount erreurs.");
return $this->redirectToRoute('app_admin');
}
#[Route('/admin/create-missing-stats-from-csv', name: 'app_admin_create_missing_stats_from_csv')]
public function createMissingStatsFromCsv(): Response
{
$this->actionLogger->log('admin/create_missing_stats_from_csv', []);
$csvFile = 'communes_france.csv';
if (!file_exists($csvFile)) {
$this->addFlash('error', 'Le fichier CSV des communes n\'existe pas. Veuillez exécuter le script fetch_communes.py pour le générer.');
return $this->redirectToRoute('app_admin');
}
$statsRepo = $this->entityManager->getRepository(Stats::class);
$createdCount = 0;
$skippedCount = 0;
$errorCount = 0;
// Ouvrir le fichier CSV
$handle = fopen($csvFile, 'r');
if (!$handle) {
$this->addFlash('error', 'Impossible d\'ouvrir le fichier CSV des communes.');
return $this->redirectToRoute('app_admin');
}
// Lire l'en-tête pour déterminer les indices des colonnes
$header = fgetcsv($handle);
$indices = array_flip($header);
// Vérifier que les colonnes nécessaires existent
$requiredColumns = ['code', 'nom'];
foreach ($requiredColumns as $column) {
if (!isset($indices[$column])) {
$this->addFlash('error', "La colonne '$column' est manquante dans le fichier CSV.");
fclose($handle);
return $this->redirectToRoute('app_admin');
}
}
// Traiter chaque ligne du CSV
while (($data = fgetcsv($handle)) !== false) {
try {
$inseeCode = $data[$indices['code']];
// Vérifier si une Stats existe déjà pour ce code INSEE
$existingStat = $statsRepo->findOneBy(['zone' => $inseeCode]);
if ($existingStat) {
$skippedCount++;
continue;
}
// Créer un nouvel objet Stats
$stat = new Stats();
$stat->setZone($inseeCode)
->setDateCreated(new \DateTime())
->setDateModified(new \DateTime())
->setKind('command'); // Utiliser 'command' comme source
// Ajouter le nom si disponible
if (isset($indices['nom']) && !empty($data[$indices['nom']])) {
$stat->setName($data[$indices['nom']]);
}
// Ajouter la population si disponible
if (isset($indices['population']) && !empty($data[$indices['population']])) {
$stat->setPopulation((int)$data[$indices['population']]);
}
// Ajouter les codes postaux si disponibles
if (isset($indices['codesPostaux']) && !empty($data[$indices['codesPostaux']])) {
$stat->setCodesPostaux($data[$indices['codesPostaux']]);
}
// Ajouter le SIREN si disponible
if (isset($indices['siren']) && !empty($data[$indices['siren']])) {
$stat->setSiren((int)$data[$indices['siren']]);
}
// Ajouter le code EPCI si disponible
if (isset($indices['codeEpci']) && !empty($data[$indices['codeEpci']])) {
$stat->setCodeEpci((int)$data[$indices['codeEpci']]);
}
// Compléter les données manquantes (coordonnées, budget, etc.)
$this->completeStatsData($stat);
// Persister l'objet Stats
$this->entityManager->persist($stat);
$createdCount++;
// Flush tous les 100 objets pour éviter de surcharger la mémoire
if ($createdCount % 100 === 0) {
$this->entityManager->flush();
$this->entityManager->clear(Stats::class);
}
} catch (\Exception $e) {
$errorCount++;
$this->actionLogger->log('error_create_missing_stats_from_csv', [
'insee_code' => $inseeCode ?? 'unknown',
'error' => $e->getMessage()
]);
}
}
// Flush les derniers objets
$this->entityManager->flush();
fclose($handle);
$this->addFlash('success', "Création des Stats manquantes terminée : $createdCount communes ajoutées, $skippedCount déjà existantes, $errorCount erreurs.");
return $this->redirectToRoute('app_admin');
}
#[Route('/admin/create-stats-from-insee-csv', name: 'app_admin_create_stats_from_insee_csv')]
public function createStatsFromInseeCsv(): Response
{
$this->actionLogger->log('admin/create_stats_from_insee_csv', []);
$csvFile = 'communes_france.csv';
if (!file_exists($csvFile)) {
$this->addFlash('error', 'Le fichier CSV des communes n\'existe pas. Veuillez exécuter le script fetch_communes.py pour le générer.');
return $this->redirectToRoute('app_admin');
}
$statsRepo = $this->entityManager->getRepository(Stats::class);
$createdCount = 0;
$skippedCount = 0;
$errorCount = 0;
// Ouvrir le fichier CSV
$handle = fopen($csvFile, 'r');
if (!$handle) {
$this->addFlash('error', 'Impossible d\'ouvrir le fichier CSV des communes.');
return $this->redirectToRoute('app_admin');
}
// Lire l'en-tête pour déterminer les indices des colonnes
$header = fgetcsv($handle);
$indices = array_flip($header);
// Vérifier que les colonnes nécessaires existent
$requiredColumns = ['code', 'nom'];
foreach ($requiredColumns as $column) {
if (!isset($indices[$column])) {
$this->addFlash('error', "La colonne '$column' est manquante dans le fichier CSV.");
fclose($handle);
return $this->redirectToRoute('app_admin');
}
}
// Traiter chaque ligne du CSV
while (($data = fgetcsv($handle)) !== false) {
try {
$inseeCode = $data[$indices['code']];
// Vérifier si une Stats existe déjà pour ce code INSEE
$existingStat = $statsRepo->findOneBy(['zone' => $inseeCode]);
if ($existingStat) {
$skippedCount++;
continue;
}
// Créer un nouvel objet Stats
$stat = new Stats();
$stat->setZone($inseeCode)
->setDateCreated(new \DateTime())
->setDateModified(new \DateTime())
->setKind('insee_csv'); // Utiliser 'insee_csv' comme source
// Ajouter le nom si disponible
if (isset($indices['nom']) && !empty($data[$indices['nom']])) {
$stat->setName($data[$indices['nom']]);
}
// Ajouter la population si disponible
if (isset($indices['population']) && !empty($data[$indices['population']])) {
$stat->setPopulation((int)$data[$indices['population']]);
}
// Ajouter les codes postaux si disponibles
if (isset($indices['codesPostaux']) && !empty($data[$indices['codesPostaux']])) {
$stat->setCodesPostaux($data[$indices['codesPostaux']]);
}
// Ajouter le SIREN si disponible
if (isset($indices['siren']) && !empty($data[$indices['siren']])) {
$stat->setSiren((int)$data[$indices['siren']]);
}
// Ajouter le code EPCI si disponible
if (isset($indices['codeEpci']) && !empty($data[$indices['codeEpci']])) {
$stat->setCodeEpci((int)$data[$indices['codeEpci']]);
}
// Ne pas faire de labourage des objets avant la sauvegarde
// Persister l'objet Stats
$this->entityManager->persist($stat);
$createdCount++;
// Flush tous les 100 objets pour éviter de surcharger la mémoire
if ($createdCount % 100 === 0) {
$this->entityManager->flush();
$this->entityManager->clear(Stats::class);
}
} catch (\Exception $e) {
$errorCount++;
$this->actionLogger->log('error_create_stats_from_insee_csv', [
'insee_code' => $inseeCode ?? 'unknown',
'error' => $e->getMessage()
]);
}
}
// Flush les derniers objets
$this->entityManager->flush();
fclose($handle);
$this->addFlash('success', "Création des Stats depuis le CSV INSEE terminée : $createdCount communes ajoutées, $skippedCount déjà existantes, $errorCount erreurs.");
return $this->redirectToRoute('app_admin');
}
#[Route('/admin/retrieve-city-polygons', name: 'app_admin_retrieve_city_polygons')]
public function retrieveCityPolygons(): Response
{
$this->actionLogger->log('admin/retrieve_city_polygons', []);
// Vérifier que le dossier polygons existe, sinon le créer
$polygonsDir = __DIR__ . '/../../counting_osm_objects/polygons';
if (!is_dir($polygonsDir)) {
mkdir($polygonsDir, 0755, true);
}
// Récupérer toutes les Stats
$statsRepo = $this->entityManager->getRepository(Stats::class);
$allStats = $statsRepo->findAll();
$totalCount = count($allStats);
$existingCount = 0;
$createdCount = 0;
$errorCount = 0;
// Pour chaque Stats, récupérer le polygone si nécessaire
foreach ($allStats as $stat) {
$inseeCode = $stat->getZone();
if (!$inseeCode) {
continue;
}
$polygonFile = $polygonsDir . '/commune_' . $inseeCode . '.poly';
// Vérifier si le polygone existe déjà
if (file_exists($polygonFile)) {
$existingCount++;
continue;
}
try {
// Utiliser le script Python existant pour récupérer le polygone
$command = 'cd ' . __DIR__ . '/../../counting_osm_objects && python3 get_poly.py ' . $inseeCode;
$output = [];
$returnVar = 0;
exec($command, $output, $returnVar);
if ($returnVar === 0 && file_exists($polygonFile)) {
$createdCount++;
} else {
$errorCount++;
$this->actionLogger->log('error_retrieve_city_polygon', [
'insee_code' => $inseeCode,
'error' => 'Failed to retrieve polygon: ' . implode("\n", $output)
]);
}
} catch (\Exception $e) {
$errorCount++;
$this->actionLogger->log('error_retrieve_city_polygon', [
'insee_code' => $inseeCode,
'error' => $e->getMessage()
]);
}
}
$this->addFlash('success', "Récupération des polygones terminée : $createdCount polygones créés, $existingCount déjà existants, $errorCount erreurs sur un total de $totalCount communes.");
return $this->redirectToRoute('app_admin');
}
#[Route('/admin/extract-insee-zones', name: 'app_admin_extract_insee_zones')]
public function extractInseeZones(): Response
{
$this->actionLogger->log('admin/extract_insee_zones', []);
// Créer le dossier oss_data s'il n'existe pas
$ossDataDir = __DIR__ . '/../../oss_data';
if (!is_dir($ossDataDir)) {
mkdir($ossDataDir, 0755, true);
}
// Vérifier que le fichier france-latest.osm.pbf existe
$francePbfFile = $ossDataDir . '/france-latest.osm.pbf';
if (!file_exists($francePbfFile)) {
$this->actionLogger->log('admin/download_france_pbf', []);
$this->addFlash('info', 'Le fichier france-latest.osm.pbf n\'existe pas. Téléchargement en cours depuis Geofabrik...');
// URL de téléchargement
$downloadUrl = 'https://download.geofabrik.de/europe/france-latest.osm.pbf';
// Télécharger le fichier
try {
$context = stream_context_create([
'http' => [
'header' => "User-Agent: OSM-Commerces/1.0\r\n"
]
]);
// Utiliser file_get_contents pour télécharger le fichier
$fileContent = file_get_contents($downloadUrl, false, $context);
if ($fileContent === false) {
throw new \Exception('Échec du téléchargement');
}
// Sauvegarder le fichier
if (file_put_contents($francePbfFile, $fileContent) === false) {
throw new \Exception('Échec de l\'écriture du fichier');
}
$this->addFlash('success', 'Le fichier france-latest.osm.pbf a été téléchargé avec succès.');
} catch (\Exception $e) {
$this->actionLogger->log('error_download_france_pbf', [
'error' => $e->getMessage()
]);
$this->addFlash('error', 'Erreur lors du téléchargement du fichier france-latest.osm.pbf: ' . $e->getMessage());
return $this->redirectToRoute('app_admin');
}
}
// Vérifier que le dossier polygons existe
$polygonsDir = __DIR__ . '/../../counting_osm_objects/polygons';
if (!is_dir($polygonsDir)) {
$this->addFlash('error', 'Le dossier des polygones n\'existe pas. Veuillez d\'abord exécuter l\'action "Récupérer les polygones des villes".');
return $this->redirectToRoute('app_admin');
}
// Créer le dossier pour les extractions JSON si nécessaire
$extractsDir = __DIR__ . '/../../insee_extracts';
if (!is_dir($extractsDir)) {
mkdir($extractsDir, 0755, true);
}
// Récupérer toutes les Stats
$statsRepo = $this->entityManager->getRepository(Stats::class);
$allStats = $statsRepo->findAll();
$totalCount = count($allStats);
$existingCount = 0;
$createdCount = 0;
$errorCount = 0;
// Pour chaque Stats, extraire les données si nécessaire
foreach ($allStats as $stat) {
$inseeCode = $stat->getZone();
if (!$inseeCode) {
continue;
}
$polygonFile = $polygonsDir . '/commune_' . $inseeCode . '.poly';
$extractPbfFile = $extractsDir . '/commune_' . $inseeCode . '.osm.pbf';
$extractJsonFile = $extractsDir . '/commune_' . $inseeCode . '.json';
// Vérifier si le polygone existe
if (!file_exists($polygonFile)) {
$this->actionLogger->log('error_extract_insee_zone', [
'insee_code' => $inseeCode,
'error' => 'Polygon file does not exist'
]);
$errorCount++;
continue;
}
// Vérifier si l'extraction JSON existe déjà
if (file_exists($extractJsonFile)) {
$existingCount++;
continue;
}
try {
// Étape 1: Extraire les données de france-latest.osm.pbf vers un fichier PBF pour la zone
$extractCommand = 'osmium extract -p ' . $polygonFile . ' ' . $francePbfFile . ' -o ' . $extractPbfFile;
$output = [];
$returnVar = 0;
exec($extractCommand, $output, $returnVar);
if ($returnVar !== 0 || !file_exists($extractPbfFile)) {
$this->actionLogger->log('error_extract_insee_zone', [
'insee_code' => $inseeCode,
'error' => 'Failed to extract PBF: ' . implode("\n", $output)
]);
$errorCount++;
continue;
}
// Étape 2: Convertir le fichier PBF en JSON
$exportCommand = 'osmium export ' . $extractPbfFile . ' -f json -o ' . $extractJsonFile;
$output = [];
$returnVar = 0;
exec($exportCommand, $output, $returnVar);
if ($returnVar === 0 && file_exists($extractJsonFile)) {
$createdCount++;
} else {
$this->actionLogger->log('error_extract_insee_zone', [
'insee_code' => $inseeCode,
'error' => 'Failed to export to JSON: ' . implode("\n", $output)
]);
$errorCount++;
}
// Supprimer le fichier PBF intermédiaire pour économiser de l'espace
if (file_exists($extractPbfFile)) {
unlink($extractPbfFile);
}
} catch (\Exception $e) {
$errorCount++;
$this->actionLogger->log('error_extract_insee_zone', [
'insee_code' => $inseeCode,
'error' => $e->getMessage()
]);
}
}
$this->addFlash('success', "Extraction des zones INSEE terminée : $createdCount extractions créées, $existingCount déjà existantes, $errorCount erreurs sur un total de $totalCount communes.");
return $this->redirectToRoute('app_admin');
}
#[Route('/admin/process-insee-extracts', name: 'app_admin_process_insee_extracts')]
public function processInseeExtracts(): Response
{
$this->actionLogger->log('admin/process_insee_extracts', []);
// Vérifier que le dossier des extractions existe
$extractsDir = __DIR__ . '/../../insee_extracts';
if (!is_dir($extractsDir)) {
$this->addFlash('error', 'Le dossier des extractions n\'existe pas. Veuillez d\'abord exécuter l\'action "Extraire les données des zones INSEE".');
return $this->redirectToRoute('app_admin');
}
// Récupérer toutes les Stats
$statsRepo = $this->entityManager->getRepository(Stats::class);
$allStats = $statsRepo->findAll();
$totalCount = count($allStats);
$processedCount = 0;
$skippedCount = 0;
$errorCount = 0;
// Pour chaque Stats, traiter les données si nécessaire
foreach ($allStats as $stat) {
$inseeCode = $stat->getZone();
if (!$inseeCode) {
continue;
}
$extractJsonFile = $extractsDir . '/commune_' . $inseeCode . '.json';
// Vérifier si l'extraction JSON existe
if (!file_exists($extractJsonFile)) {
$this->actionLogger->log('error_process_insee_extract', [
'insee_code' => $inseeCode,
'error' => 'JSON extract file does not exist'
]);
$errorCount++;
continue;
}
try {
// Utiliser la Motocultrice pour traiter les données
$result = $this->motocultrice->labourer($inseeCode);
if ($result) {
// Mettre à jour la date de labourage
$stat->setDateLabourageDone(new \DateTime());
$this->entityManager->persist($stat);
$processedCount++;
// Flush tous les 10 objets pour éviter de surcharger la mémoire
if ($processedCount % 10 === 0) {
$this->entityManager->flush();
}
} else {
$this->actionLogger->log('error_process_insee_extract', [
'insee_code' => $inseeCode,
'error' => 'Failed to process extract with Motocultrice'
]);
$errorCount++;
}
} catch (\Exception $e) {
$errorCount++;
$this->actionLogger->log('error_process_insee_extract', [
'insee_code' => $inseeCode,
'error' => $e->getMessage()
]);
}
}
// Flush les derniers objets
$this->entityManager->flush();
$this->addFlash('success', "Traitement des extractions INSEE terminé : $processedCount communes traitées, $skippedCount ignorées, $errorCount erreurs sur un total de $totalCount communes.");
return $this->redirectToRoute('app_admin');
}
#[Route('/admin/completion-statistics', name: 'app_admin_completion_statistics')]
public function completionStatistics(Request $request): Response
{
$this->actionLogger->log('admin/completion_statistics', []);
// Récupérer le thème sélectionné (par défaut: 'places')
$theme = $request->query->get('theme', 'places');
// Récupérer le niveau géographique sélectionné (par défaut: 'department')
$level = $request->query->get('level', 'department');
// Récupérer tous les thèmes disponibles
$themes = $this->followUpService->getFollowUpThemes();
// Récupérer les données de complétion selon le niveau géographique
$completionData = [];
$chartLabels = [];
$chartData = [];
switch ($level) {
case 'department':
// Agréger les données par département
$completionData = $this->aggregateCompletionByDepartment($theme);
break;
case 'region':
// Agréger les données par région
$completionData = $this->aggregateCompletionByRegion($theme);
break;
case 'country':
// Agréger les données pour la France entière
$completionData = $this->aggregateCompletionForCountry($theme);
break;
case 'city':
// Récupérer les données pour les villes (limité aux 50 plus grandes)
$completionData = $this->aggregateCompletionForTopCities($theme, 50);
break;
}
// Préparer les données pour les graphiques
foreach ($completionData as $key => $value) {
$chartLabels[] = $key;
$chartData[] = $value;
}
return $this->render('admin/completion_statistics.html.twig', [
'theme' => $theme,
'level' => $level,
'themes' => $themes,
'chartLabels' => json_encode($chartLabels),
'chartData' => json_encode($chartData),
'completionData' => $completionData
]);
}
/**
* Agrège les données de complétion par département
*/
private function aggregateCompletionByDepartment(string $theme): array
{
$result = [];
// Récupérer toutes les Stats
$statsRepo = $this->entityManager->getRepository(Stats::class);
$allStats = $statsRepo->findAll();
// Grouper par département (2 premiers chiffres du code INSEE)
$departmentData = [];
$departmentCounts = [];
foreach ($allStats as $stat) {
$inseeCode = $stat->getZone();
if (!$inseeCode || strlen($inseeCode) < 2) {
continue;
}
// Extraire le code département (2 premiers chiffres du code INSEE)
$departmentCode = substr($inseeCode, 0, 2);
// Cas particuliers pour la Corse
if ($departmentCode === '2A' || $departmentCode === '2B') {
$departmentCode = substr($inseeCode, 0, 3);
}
// Récupérer les mesures pour le thème spécifié
$latestMeasure = $this->getLatestMeasureForTheme($stat, $theme);
if ($latestMeasure !== null) {
if (!isset($departmentData[$departmentCode])) {
$departmentData[$departmentCode] = 0;
$departmentCounts[$departmentCode] = 0;
}
$departmentData[$departmentCode] += $latestMeasure;
$departmentCounts[$departmentCode]++;
}
}
// Calculer les moyennes par département
foreach ($departmentData as $departmentCode => $total) {
if ($departmentCounts[$departmentCode] > 0) {
$result[$departmentCode] = round($total / $departmentCounts[$departmentCode], 2);
}
}
// Trier par code département
ksort($result);
return $result;
}
/**
* Agrège les données de complétion par région
*/
private function aggregateCompletionByRegion(string $theme): array
{
$result = [];
// Mapping des départements vers les régions
$departmentToRegion = $this->getDepartmentToRegionMapping();
// Récupérer les données par département
$departmentData = $this->aggregateCompletionByDepartment($theme);
// Grouper par région
$regionData = [];
$regionCounts = [];
foreach ($departmentData as $departmentCode => $value) {
// Trouver la région correspondante
$region = $departmentToRegion[$departmentCode] ?? 'Autre';
if (!isset($regionData[$region])) {
$regionData[$region] = 0;
$regionCounts[$region] = 0;
}
$regionData[$region] += $value;
$regionCounts[$region]++;
}
// Calculer les moyennes par région
foreach ($regionData as $region => $total) {
if ($regionCounts[$region] > 0) {
$result[$region] = round($total / $regionCounts[$region], 2);
}
}
// Trier par nom de région
ksort($result);
return $result;
}
/**
* Agrège les données de complétion pour la France entière
*/
private function aggregateCompletionForCountry(string $theme): array
{
$result = [];
// Récupérer toutes les Stats
$statsRepo = $this->entityManager->getRepository(Stats::class);
$allStats = $statsRepo->findAll();
// Récupérer les mesures pour le thème spécifié
$totalMeasure = 0;
$count = 0;
foreach ($allStats as $stat) {
$latestMeasure = $this->getLatestMeasureForTheme($stat, $theme);
if ($latestMeasure !== null) {
$totalMeasure += $latestMeasure;
$count++;
}
}
// Calculer la moyenne nationale
if ($count > 0) {
$result['France'] = round($totalMeasure / $count, 2);
}
return $result;
}
/**
* Agrège les données de complétion pour les principales villes
*/
private function aggregateCompletionForTopCities(string $theme, int $limit = 50): array
{
$result = [];
// Récupérer les Stats triées par population (descendant)
$statsRepo = $this->entityManager->getRepository(Stats::class);
$topCities = $statsRepo->findBy(
['population' => null], // Critère (tous)
['population' => 'DESC'], // Tri par population descendante
$limit // Limite
);
// Récupérer les mesures pour le thème spécifié
foreach ($topCities as $stat) {
$cityName = $stat->getName() ?? $stat->getZone();
$latestMeasure = $this->getLatestMeasureForTheme($stat, $theme);
if ($latestMeasure !== null) {
$result[$cityName] = $latestMeasure;
}
}
// Trier par valeur décroissante
arsort($result);
return $result;
}
/**
* Récupère la dernière mesure pour un thème donné
*/
private function getLatestMeasureForTheme(Stats $stat, string $theme): ?float
{
$cityFollowUps = $stat->getCityFollowUps();
// Filtrer les CityFollowUp par thème et trier par date (descendant)
$filteredFollowUps = $cityFollowUps->filter(function(CityFollowUp $followUp) use ($theme) {
return $followUp->getName() === $theme;
});
if ($filteredFollowUps->isEmpty()) {
return null;
}
// Trier par date (du plus récent au plus ancien)
$iterator = $filteredFollowUps->getIterator();
$iterator->uasort(function(CityFollowUp $a, CityFollowUp $b) {
return $b->getDate() <=> $a->getDate();
});
// Récupérer la mesure la plus récente
$latestFollowUp = $iterator->current();
return $latestFollowUp->getMeasure();
}
#[Route('/admin/osmose-issues-map/{inseeCode}', name: 'app_admin_osmose_issues_map')]
2025-08-31 12:29:41 +02:00
public function osmoseIssuesMap(Request $request, ?string $inseeCode = null): Response
{
$this->actionLogger->log('admin/osmose_issues_map', [
'insee_code' => $inseeCode
]);
// Si aucun code INSEE n'est fourni, rediriger vers la liste des villes
if (!$inseeCode) {
$this->addFlash('info', 'Veuillez sélectionner une ville pour afficher les problèmes Osmose.');
return $this->redirectToRoute('app_admin');
}
// Récupérer la ville correspondante
$statsRepo = $this->entityManager->getRepository(Stats::class);
$city = $statsRepo->findOneBy(['zone' => $inseeCode]);
if (!$city) {
2025-08-31 17:57:28 +02:00
// Si aucune stats n'existe, rechercher dans l'API geo.api.gouv.fr
$apiUrl = "https://geo.api.gouv.fr/communes/{$inseeCode}";
$response = @file_get_contents($apiUrl);
if ($response === false) {
$this->addFlash('error', 'Ville non trouvée pour le code INSEE ' . $inseeCode . ' et impossible de récupérer les informations depuis l\'API geo.api.gouv.fr.');
return $this->redirectToRoute('app_admin');
}
$communeData = json_decode($response, true);
if (!$communeData || !isset($communeData['nom'])) {
$this->addFlash('error', 'Aucune commune trouvée avec ce code INSEE dans l\'API geo.api.gouv.fr.');
return $this->redirectToRoute('app_admin');
}
// Créer un nouvel objet Stats avec les données de l'API
$city = new Stats();
$city->setZone($inseeCode)
->setName($communeData['nom'])
->setDateCreated(new \DateTime())
->setDateModified(new \DateTime())
->setKind('osmose_request');
// Ajouter la population si disponible
if (isset($communeData['population'])) {
$city->setPopulation($communeData['population']);
}
// Ajouter les coordonnées si disponibles
if (isset($communeData['centre']) && isset($communeData['centre']['coordinates'])) {
$city->setLon((string)$communeData['centre']['coordinates'][0]);
$city->setLat((string)$communeData['centre']['coordinates'][1]);
} else {
// Si les coordonnées ne sont pas dans la réponse initiale, faire une requête spécifique
try {
$apiUrl = 'https://geo.api.gouv.fr/communes/' . $inseeCode . '?fields=centre';
$response = @file_get_contents($apiUrl);
if ($response !== false) {
$data = json_decode($response, true);
if (isset($data['centre']['coordinates']) && count($data['centre']['coordinates']) === 2) {
$city->setLon((string)$data['centre']['coordinates'][0]);
$city->setLat((string)$data['centre']['coordinates'][1]);
}
}
} catch (\Exception $e) {
// Ignorer les erreurs lors de la récupération des coordonnées
}
}
// Ajouter les codes postaux si disponibles
if (isset($communeData['codesPostaux']) && !empty($communeData['codesPostaux'])) {
$city->setCodesPostaux(implode(',', $communeData['codesPostaux']));
}
// Ajouter le code EPCI si disponible
if (isset($communeData['codeEpci'])) {
$city->setCodeEpci((int)$communeData['codeEpci']);
}
// Ajouter le SIREN si disponible
if (isset($communeData['siren'])) {
$city->setSiren((int)$communeData['siren']);
}
// Ne pas faire de labourage des Places pour cette ville
// Persister l'objet Stats
$this->entityManager->persist($city);
$this->entityManager->flush();
}
// Récupérer le thème sélectionné (par défaut: tous)
$theme = $request->query->get('theme', 'all');
// Récupérer tous les thèmes disponibles
$themes = $this->followUpService->getFollowUpThemes();
2025-08-31 18:23:41 +02:00
// Liste complète des items Osmose à utiliser
$allOsmoseItems = '7051%2C7070%2C7100%2C7150%2C7160%2C7170%2C7190%2C7220%2C7240%2C7250%2C8010%2C8020%2C8021%2C8030%2C8031%2C8050%2C8051%2C8090%2C8091%2C8101%2C8110%2C8121%2C8150%2C8151%2C8180%2C8191%2C8201%2C8211%2C8221%2C8230%2C8240';
// Récupérer les problèmes Osmose pour cette ville
$osmoseIssues = $this->getOsmoseIssuesForCity($city, $theme);
2025-08-31 17:57:28 +02:00
// Créer un mapping inverse des items Osmose vers les thèmes
$itemToThemeMapping = [];
$themeToItemsMapping = [
'charging_station' => [8410, 8411],
'school' => [8031],
'healthcare' => [8211, 7220, 8331],
'laboratory' => [7240, 8351],
'police' => [8190, 8191],
'defibrillator' => [8370],
'places' => [7240, 8351, 8211, 7220, 8331, 8031],
'restaurants' => [8030, 8031, 8032],
'hotels' => [8040, 8041, 8042],
'tourism' => [8010, 8011, 8012, 8013],
'leisure' => [8050, 8051, 8052],
'transportation' => [4010, 4020, 4030, 4040],
'amenities' => [8080, 8081, 8082],
];
foreach ($themeToItemsMapping as $themeName => $itemIds) {
foreach ($itemIds as $itemId) {
if (!isset($itemToThemeMapping[$itemId])) {
$itemToThemeMapping[$itemId] = [];
}
$itemToThemeMapping[$itemId][] = $themeName;
}
}
// Compter les problèmes par thème
$issuesByTheme = [];
foreach ($themes as $themeKey => $themeLabel) {
$issuesByTheme[$themeKey] = 0;
}
// Ajouter un compteur pour "Autres" (problèmes qui ne correspondent à aucun thème)
$issuesByTheme['other'] = 0;
// Compter les problèmes par niveau de sévérité
$issuesByLevel = [
1 => 0, // Critique
2 => 0, // Important
3 => 0, // Avertissement
];
foreach ($osmoseIssues as $issue) {
// Compter par niveau de sévérité
$level = (int)$issue['level'];
if (isset($issuesByLevel[$level])) {
$issuesByLevel[$level]++;
}
// Compter par thème
$itemId = (int)$issue['item'];
$counted = false;
if (isset($itemToThemeMapping[$itemId])) {
foreach ($itemToThemeMapping[$itemId] as $themeName) {
if (isset($issuesByTheme[$themeName])) {
$issuesByTheme[$themeName]++;
$counted = true;
}
}
}
// Si le problème n'a été compté dans aucun thème, l'ajouter à "Autres"
if (!$counted) {
$issuesByTheme['other']++;
}
}
// Ajouter le libellé pour "Autres"
$themes['other'] = 'Autres problèmes';
2025-08-31 18:23:41 +02:00
// Calculer la bounding box pour la ville
$bbox = $this->calculateBoundingBox($city->getLat(), $city->getLon(), 5);
// Récupérer les items Osmose correspondant aux thèmes si un thème spécifique est sélectionné
$itemsParam = $theme !== 'all' ?
(!empty($this->getOsmoseItemIdsForTheme($theme)) ? implode('%2C', $this->getOsmoseItemIdsForTheme($theme)) : $allOsmoseItems) :
$allOsmoseItems;
// Construire l'URL de l'API Osmose pour le GeoJSON
$jsonOsmoseUrl = sprintf(
'https://osmose.openstreetmap.fr/api/0.3/issues.geojson?zoom=14&item=%s&level=1%%2C2%%2C3&class=&source=&limit=500&bbox=%f%%2C%f%%2C%f%%2C%f',
$itemsParam,
$bbox['min_lon'],
$bbox['min_lat'],
$bbox['max_lon'],
$bbox['max_lat']
);
return $this->render('admin/osmose_issues_map.html.twig', [
'city' => $city,
2025-08-31 22:53:28 +02:00
'stats' => $city,
'theme' => $theme,
'themes' => $themes,
2025-08-31 17:57:28 +02:00
'osmoseIssues' => $osmoseIssues,
'issuesByTheme' => $issuesByTheme,
'issuesByLevel' => $issuesByLevel,
2025-08-31 18:23:41 +02:00
'mapbox_token' => $_ENV['MAPBOX_TOKEN'] ?? null,
'maptiler_token' => $_ENV['MAPTILER_TOKEN'] ?? null,
'jsonOsmose' => $jsonOsmoseUrl,
'osmoseApiUrl' => sprintf(
'https://osmose.openstreetmap.fr/fr/map/#zoom=14&lat=%s&lon=%s&item=%s&level=1%%2C2%%2C3&loc=14/%s/%s',
$city->getLat(),
$city->getLon(),
$allOsmoseItems,
$city->getLat(),
$city->getLon()
),
]);
}
/**
* Récupère les problèmes Osmose pour une ville donnée
*/
private function getOsmoseIssuesForCity(Stats $city, string $theme = 'all'): array
{
$issues = [];
// Coordonnées de la ville
$lat = $city->getLat();
$lon = $city->getLon();
if (!$lat || !$lon) {
return $issues;
}
// Construire l'URL de l'API Osmose
$bbox = $this->calculateBoundingBox($lat, $lon, 5); // 5km autour du centre de la ville
$osmoseApiUrl = sprintf(
'https://osmose.openstreetmap.fr/api/0.3/issues?bbox=%f,%f,%f,%f&item=xxxx&limit=500',
$bbox['min_lon'],
$bbox['min_lat'],
$bbox['max_lon'],
$bbox['max_lat']
);
// Récupérer les items Osmose correspondant aux thèmes
$itemIds = $this->getOsmoseItemIdsForTheme($theme);
if (!empty($itemIds)) {
$osmoseApiUrl = str_replace('xxxx', implode(',', $itemIds), $osmoseApiUrl);
} else {
$osmoseApiUrl = str_replace('&item=xxxx', '', $osmoseApiUrl);
}
try {
// Appeler l'API Osmose
2025-08-31 18:23:41 +02:00
$context = stream_context_create([
'http' => [
'timeout' => 30, // Augmenter le timeout à 30 secondes
'user_agent' => 'OSM-Commerces/1.0'
]
]);
$response = file_get_contents($osmoseApiUrl, false, $context);
if ($response === false) {
throw new \Exception('Échec de la récupération des données Osmose');
}
$data = json_decode($response, true);
2025-08-31 18:23:41 +02:00
if (isset($data['issues']) && is_array($data['issues'])) {
foreach ($data['issues'] as $issue) {
2025-08-31 18:23:41 +02:00
// Vérifier si l'issue a les propriétés nécessaires
if (!isset($issue['lat']) || !isset($issue['lon'])) {
continue;
}
// Vérifier si l'issue est dans les limites de la ville (approximativement)
2025-08-31 18:23:41 +02:00
// Utiliser un rayon plus grand (10km) pour inclure plus d'issues
if ($this->isPointInCity($issue['lat'], $issue['lon'], $lat, $lon, 10)) {
$issues[] = [
2025-08-31 18:23:41 +02:00
'id' => $issue['id'] ?? '',
2025-08-31 18:34:19 +02:00
'title' => $issue['properties']['title'] ?? 'Problème sans titre',
'subtitle' => $issue['properties']['subtitle'] ?? '',
'lat' => $issue['lat'],
'lon' => $issue['lon'],
2025-08-31 18:23:41 +02:00
'item' => $issue['item'] ?? '',
'class' => $issue['class'] ?? '',
'level' => $issue['level'] ?? 2,
'update_timestamp' => $issue['update_timestamp'] ?? null,
2025-08-31 18:23:41 +02:00
'url' => isset($issue['uuid']) ? sprintf('https://osmose.openstreetmap.fr/fr/issue/%s', $issue['uuid']) : '#'
];
}
}
}
2025-08-31 18:23:41 +02:00
// Si aucune issue n'a été trouvée, ajouter un message de log
if (empty($issues)) {
$this->actionLogger->log('osmose_no_issues', [
'insee_code' => $city->getZone(),
'theme' => $theme,
'url' => $osmoseApiUrl
]);
}
} catch (\Exception $e) {
$this->actionLogger->log('error_osmose_api', [
'insee_code' => $city->getZone(),
2025-08-31 18:23:41 +02:00
'error' => $e->getMessage(),
'url' => $osmoseApiUrl
]);
2025-08-31 18:23:41 +02:00
// Ajouter un message flash pour informer l'utilisateur
$this->addFlash('warning', 'Impossible de récupérer les problèmes Osmose : ' . $e->getMessage());
}
return $issues;
}
/**
* Calcule une bounding box autour d'un point
*/
private function calculateBoundingBox(string $lat, string $lon, int $distanceKm = 5): array
{
$lat = (float)$lat;
$lon = (float)$lon;
// Approximation: 1 degré de latitude = 111 km
$latDelta = $distanceKm / 111.0;
// Approximation: 1 degré de longitude = 111 * cos(latitude) km
$lonDelta = $distanceKm / (111.0 * cos(deg2rad($lat)));
return [
'min_lat' => $lat - $latDelta,
'max_lat' => $lat + $latDelta,
'min_lon' => $lon - $lonDelta,
'max_lon' => $lon + $lonDelta
];
}
/**
* Vérifie si un point est dans un rayon donné autour d'une ville
*/
private function isPointInCity(float $pointLat, float $pointLon, string $cityLat, string $cityLon, int $radiusKm = 5): bool
{
$cityLat = (float)$cityLat;
$cityLon = (float)$cityLon;
// Calcul de la distance en km (formule de Haversine)
$earthRadius = 6371; // Rayon de la Terre en km
$dLat = deg2rad($pointLat - $cityLat);
$dLon = deg2rad($pointLon - $cityLon);
$a = sin($dLat/2) * sin($dLat/2) +
cos(deg2rad($cityLat)) * cos(deg2rad($pointLat)) *
sin($dLon/2) * sin($dLon/2);
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
$distance = $earthRadius * $c;
return $distance <= $radiusKm;
}
/**
* Retourne les IDs des items Osmose correspondant à un thème
*/
private function getOsmoseItemIdsForTheme(string $theme): array
{
// Mapping des thèmes vers les items Osmose
$themeToItemsMapping = [
2025-08-31 17:57:28 +02:00
'charging_station' => [8410, 8411],
'school' => [8031],
'healthcare' => [8211, 7220, 8331],
'laboratory' => [7240, 8351],
'police' => [8190, 8191],
'defibrillator' => [8370],
'places' => [7240, 8351, 8211, 7220, 8331, 8031],
'restaurants' => [8030, 8031, 8032], // Restaurants et cafés
'hotels' => [8040, 8041, 8042], // Hébergements
'tourism' => [8010, 8011, 8012, 8013], // Tourisme
'leisure' => [8050, 8051, 8052], // Loisirs
'transportation' => [4010, 4020, 4030, 4040], // Transport
'amenities' => [8080, 8081, 8082], // Équipements
// Si d'autres thèmes sont nécessaires, ajoutez-les ici
];
2025-08-31 17:57:28 +02:00
// Si le thème est 'all', retourner tous les items uniques de tous les thèmes
if ($theme === 'all') {
$allItems = [];
foreach ($themeToItemsMapping as $items) {
$allItems = array_merge($allItems, $items);
}
return array_unique($allItems);
}
// Si le thème n'existe pas dans le mapping, retourner un tableau vide
if (!isset($themeToItemsMapping[$theme])) {
return [];
}
return $themeToItemsMapping[$theme];
}
/**
* Retourne un mapping des départements vers les régions
*/
private function getDepartmentToRegionMapping(): array
{
return [
// Auvergne-Rhône-Alpes
'01' => 'Auvergne-Rhône-Alpes', // Ain
'03' => 'Auvergne-Rhône-Alpes', // Allier
'07' => 'Auvergne-Rhône-Alpes', // Ardèche
'15' => 'Auvergne-Rhône-Alpes', // Cantal
'26' => 'Auvergne-Rhône-Alpes', // Drôme
'38' => 'Auvergne-Rhône-Alpes', // Isère
'42' => 'Auvergne-Rhône-Alpes', // Loire
'43' => 'Auvergne-Rhône-Alpes', // Haute-Loire
'63' => 'Auvergne-Rhône-Alpes', // Puy-de-Dôme
'69' => 'Auvergne-Rhône-Alpes', // Rhône
'73' => 'Auvergne-Rhône-Alpes', // Savoie
'74' => 'Auvergne-Rhône-Alpes', // Haute-Savoie
// Bourgogne-Franche-Comté
'21' => 'Bourgogne-Franche-Comté', // Côte-d'Or
'25' => 'Bourgogne-Franche-Comté', // Doubs
'39' => 'Bourgogne-Franche-Comté', // Jura
'58' => 'Bourgogne-Franche-Comté', // Nièvre
'70' => 'Bourgogne-Franche-Comté', // Haute-Saône
'71' => 'Bourgogne-Franche-Comté', // Saône-et-Loire
'89' => 'Bourgogne-Franche-Comté', // Yonne
'90' => 'Bourgogne-Franche-Comté', // Territoire de Belfort
// Bretagne
'22' => 'Bretagne', // Côtes-d'Armor
'29' => 'Bretagne', // Finistère
'35' => 'Bretagne', // Ille-et-Vilaine
'56' => 'Bretagne', // Morbihan
// Centre-Val de Loire
'18' => 'Centre-Val de Loire', // Cher
'28' => 'Centre-Val de Loire', // Eure-et-Loir
'36' => 'Centre-Val de Loire', // Indre
'37' => 'Centre-Val de Loire', // Indre-et-Loire
'41' => 'Centre-Val de Loire', // Loir-et-Cher
'45' => 'Centre-Val de Loire', // Loiret
// Corse
'2A' => 'Corse', // Corse-du-Sud
'2B' => 'Corse', // Haute-Corse
// Grand Est
'08' => 'Grand Est', // Ardennes
'10' => 'Grand Est', // Aube
'51' => 'Grand Est', // Marne
'52' => 'Grand Est', // Haute-Marne
'54' => 'Grand Est', // Meurthe-et-Moselle
'55' => 'Grand Est', // Meuse
'57' => 'Grand Est', // Moselle
'67' => 'Grand Est', // Bas-Rhin
'68' => 'Grand Est', // Haut-Rhin
'88' => 'Grand Est', // Vosges
// Hauts-de-France
'02' => 'Hauts-de-France', // Aisne
'59' => 'Hauts-de-France', // Nord
'60' => 'Hauts-de-France', // Oise
'62' => 'Hauts-de-France', // Pas-de-Calais
'80' => 'Hauts-de-France', // Somme
// Île-de-France
'75' => 'Île-de-France', // Paris
'77' => 'Île-de-France', // Seine-et-Marne
'78' => 'Île-de-France', // Yvelines
'91' => 'Île-de-France', // Essonne
'92' => 'Île-de-France', // Hauts-de-Seine
'93' => 'Île-de-France', // Seine-Saint-Denis
'94' => 'Île-de-France', // Val-de-Marne
'95' => 'Île-de-France', // Val-d'Oise
// Normandie
'14' => 'Normandie', // Calvados
'27' => 'Normandie', // Eure
'50' => 'Normandie', // Manche
'61' => 'Normandie', // Orne
'76' => 'Normandie', // Seine-Maritime
// Nouvelle-Aquitaine
'16' => 'Nouvelle-Aquitaine', // Charente
'17' => 'Nouvelle-Aquitaine', // Charente-Maritime
'19' => 'Nouvelle-Aquitaine', // Corrèze
'23' => 'Nouvelle-Aquitaine', // Creuse
'24' => 'Nouvelle-Aquitaine', // Dordogne
'33' => 'Nouvelle-Aquitaine', // Gironde
'40' => 'Nouvelle-Aquitaine', // Landes
'47' => 'Nouvelle-Aquitaine', // Lot-et-Garonne
'64' => 'Nouvelle-Aquitaine', // Pyrénées-Atlantiques
'79' => 'Nouvelle-Aquitaine', // Deux-Sèvres
'86' => 'Nouvelle-Aquitaine', // Vienne
'87' => 'Nouvelle-Aquitaine', // Haute-Vienne
// Occitanie
'09' => 'Occitanie', // Ariège
'11' => 'Occitanie', // Aude
'12' => 'Occitanie', // Aveyron
'30' => 'Occitanie', // Gard
'31' => 'Occitanie', // Haute-Garonne
'32' => 'Occitanie', // Gers
'34' => 'Occitanie', // Hérault
'46' => 'Occitanie', // Lot
'48' => 'Occitanie', // Lozère
'65' => 'Occitanie', // Hautes-Pyrénées
'66' => 'Occitanie', // Pyrénées-Orientales
'81' => 'Occitanie', // Tarn
'82' => 'Occitanie', // Tarn-et-Garonne
// Pays de la Loire
'44' => 'Pays de la Loire', // Loire-Atlantique
'49' => 'Pays de la Loire', // Maine-et-Loire
'53' => 'Pays de la Loire', // Mayenne
'72' => 'Pays de la Loire', // Sarthe
'85' => 'Pays de la Loire', // Vendée
// Provence-Alpes-Côte d'Azur
'04' => 'Provence-Alpes-Côte d\'Azur', // Alpes-de-Haute-Provence
'05' => 'Provence-Alpes-Côte d\'Azur', // Hautes-Alpes
'06' => 'Provence-Alpes-Côte d\'Azur', // Alpes-Maritimes
'13' => 'Provence-Alpes-Côte d\'Azur', // Bouches-du-Rhône
'83' => 'Provence-Alpes-Côte d\'Azur', // Var
'84' => 'Provence-Alpes-Côte d\'Azur', // Vaucluse
// Départements d'outre-mer
'971' => 'Guadeloupe',
'972' => 'Martinique',
'973' => 'Guyane',
'974' => 'La Réunion',
'976' => 'Mayotte'
];
}
/**
* Complète les données manquantes d'un objet Stats (coordonnées, budget, etc.)
*/
private function completeStatsData(Stats $stat): void
{
$insee_code = $stat->getZone();
// Compléter les coordonnées si manquantes
if (!$stat->getLat() || !$stat->getLon()) {
try {
$apiUrl = 'https://geo.api.gouv.fr/communes/' . $insee_code . '?fields=centre';
$response = @file_get_contents($apiUrl);
if ($response !== false) {
$data = json_decode($response, true);
if (isset($data['centre']['coordinates']) && count($data['centre']['coordinates']) === 2) {
$stat->setLon((string)$data['centre']['coordinates'][0]);
$stat->setLat((string)$data['centre']['coordinates'][1]);
}
}
} catch (\Exception $e) {
$this->actionLogger->log('error_complete_stats_data', [
'insee_code' => $insee_code,
'error' => 'Failed to fetch coordinates: ' . $e->getMessage()
]);
}
}
// Compléter le budget si manquant
if (!$stat->getBudgetAnnuel() && property_exists($this, 'budgetService') && $this->budgetService !== null) {
try {
$budget = $this->budgetService->getBudgetAnnuel($insee_code);
if ($budget !== null) {
$stat->setBudgetAnnuel((string)$budget);
}
} catch (\Exception $e) {
$this->actionLogger->log('error_complete_stats_data', [
'insee_code' => $insee_code,
'error' => 'Failed to fetch budget: ' . $e->getMessage()
]);
}
}
// Calculer le pourcentage de complétion
$stat->computeCompletionPercent();
}
2025-06-26 18:20:43 +02:00
2025-06-27 00:35:11 +02:00
#[Route('/admin/podium-contributeurs-osm', name: 'app_admin_podium_contributeurs_osm')]
public function podiumContributeursOsm(): Response
{
2025-07-05 10:59:37 +02:00
$this->actionLogger->log('admin/podium_contributeurs_osm', []);
2025-07-05 16:15:56 +02:00
2025-07-05 10:59:37 +02:00
// Récupérer tous les lieux avec un utilisateur OSM
$places = $this->entityManager->getRepository(Place::class)->findBy(['osm_user' => null], ['osm_user' => 'ASC']);
$places = array_filter($places, fn($place) => $place->getOsmUser() !== null);
2025-07-05 16:15:56 +02:00
2025-07-05 10:59:37 +02:00
// Compter les contributions par utilisateur
$contributions = [];
foreach ($places as $place) {
$user = $place->getOsmUser();
if ($user) {
if (!isset($contributions[$user])) {
$contributions[$user] = 0;
}
$contributions[$user]++;
}
}
2025-07-05 16:15:56 +02:00
2025-07-05 10:59:37 +02:00
// Trier par nombre de contributions décroissant
arsort($contributions);
2025-07-05 16:15:56 +02:00
2025-07-05 10:59:37 +02:00
// Prendre les 10 premiers
$topContributors = array_slice($contributions, 0, 10, true);
2025-07-05 16:15:56 +02:00
2025-07-05 10:59:37 +02:00
return $this->render('admin/podium_contributeurs_osm.html.twig', [
'podium' => $topContributors
2025-07-05 10:59:37 +02:00
]);
}
2025-06-27 00:35:11 +02:00
2025-07-05 10:59:37 +02:00
#[Route('/admin/import-stats', name: 'app_admin_import_stats', methods: ['GET', 'POST'])]
public function importStats(Request $request): Response
{
$this->actionLogger->log('admin/import_stats', []);
if ($request->isMethod('POST')) {
$uploadedFile = $request->files->get('json_file');
2025-07-05 16:15:56 +02:00
2025-07-05 10:59:37 +02:00
if (!$uploadedFile) {
$this->addFlash('error', 'Aucun fichier JSON n\'a été fourni.');
return $this->redirectToRoute('app_admin_import_stats');
}
2025-06-27 00:35:11 +02:00
2025-07-05 10:59:37 +02:00
// Vérifier le type de fichier
2025-07-05 16:15:56 +02:00
if (
$uploadedFile->getClientMimeType() !== 'application/json' &&
$uploadedFile->getClientOriginalExtension() !== 'json'
) {
2025-07-05 10:59:37 +02:00
$this->addFlash('error', 'Le fichier doit être au format JSON.');
return $this->redirectToRoute('app_admin_import_stats');
}
2025-06-27 00:35:11 +02:00
2025-07-05 10:59:37 +02:00
try {
// Lire le contenu du fichier
$jsonContent = file_get_contents($uploadedFile->getPathname());
$data = json_decode($jsonContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('Erreur lors du décodage JSON: ' . json_last_error_msg());
}
if (!is_array($data)) {
throw new \Exception('Le fichier JSON doit contenir un tableau d\'objets Stats.');
}
$createdCount = 0;
$skippedCount = 0;
$errors = [];
foreach ($data as $index => $statData) {
try {
// Vérifier que les champs requis sont présents
if (!isset($statData['zone']) || !isset($statData['name'])) {
$errors[] = "Ligne " . ($index + 1) . ": Champs 'zone' et 'name' requis";
continue;
}
$zone = $statData['zone'];
$name = $statData['name'];
// Vérifier si l'objet Stats existe déjà
$existingStats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $zone]);
2025-07-05 16:15:56 +02:00
2025-07-05 10:59:37 +02:00
if ($existingStats) {
$skippedCount++;
continue; // Ignorer les objets existants
}
// Créer un nouvel objet Stats
$stats = new Stats();
$stats->setZone($zone)
2025-07-05 16:15:56 +02:00
->setName($name)
->setDateCreated(new \DateTime())
->setDateModified(new \DateTime())
->setKind('request'); // Set the kind to 'request' as it's created from an admin import
2025-07-05 10:59:37 +02:00
// Remplir les champs optionnels
if (isset($statData['population'])) {
$stats->setPopulation($statData['population']);
}
if (isset($statData['budgetAnnuel'])) {
$stats->setBudgetAnnuel($statData['budgetAnnuel']);
}
if (isset($statData['siren'])) {
$stats->setSiren($statData['siren']);
}
if (isset($statData['codeEpci'])) {
$stats->setCodeEpci($statData['codeEpci']);
}
if (isset($statData['codesPostaux'])) {
$stats->setCodesPostaux($statData['codesPostaux']);
}
// Remplir les décomptes si disponibles
if (isset($statData['decomptes'])) {
$decomptes = $statData['decomptes'];
if (isset($decomptes['placesCount'])) {
$stats->setPlacesCount($decomptes['placesCount']);
}
if (isset($decomptes['avecHoraires'])) {
$stats->setAvecHoraires($decomptes['avecHoraires']);
}
if (isset($decomptes['avecAdresse'])) {
$stats->setAvecAdresse($decomptes['avecAdresse']);
}
if (isset($decomptes['avecSite'])) {
$stats->setAvecSite($decomptes['avecSite']);
}
if (isset($decomptes['avecAccessibilite'])) {
$stats->setAvecAccessibilite($decomptes['avecAccessibilite']);
}
if (isset($decomptes['avecNote'])) {
$stats->setAvecNote($decomptes['avecNote']);
}
if (isset($decomptes['completionPercent'])) {
$stats->setCompletionPercent($decomptes['completionPercent']);
}
}
$this->entityManager->persist($stats);
$createdCount++;
} catch (\Exception $e) {
$errors[] = "Ligne " . ($index + 1) . ": " . $e->getMessage();
}
}
// Sauvegarder les changements
$this->entityManager->flush();
// Afficher les résultats
$message = "Import terminé : $createdCount objet(s) créé(s), $skippedCount objet(s) ignoré(s) (déjà existants).";
if (!empty($errors)) {
$message .= " Erreurs : " . count($errors);
foreach ($errors as $error) {
$this->addFlash('warning', $error);
}
}
$this->addFlash('success', $message);
$this->actionLogger->log('admin/import_stats_success', [
'created' => $createdCount,
'skipped' => $skippedCount,
'errors' => count($errors)
]);
} catch (\Exception $e) {
$this->addFlash('error', 'Erreur lors de l\'import : ' . $e->getMessage());
$this->actionLogger->log('admin/import_stats_error', ['error' => $e->getMessage()]);
}
2025-07-05 10:59:37 +02:00
return $this->redirectToRoute('app_admin_import_stats');
}
2025-07-05 10:59:37 +02:00
return $this->render('admin/import_stats.html.twig');
}
#[Route('/admin/export-overpass-csv/{insee_code}', name: 'app_admin_export_overpass_csv')]
public function exportOverpassCsv($insee_code): Response
{
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
2025-07-05 16:15:56 +02:00
2025-07-05 10:59:37 +02:00
if (!$stats) {
throw $this->createNotFoundException('Stats non trouvées pour ce code INSEE');
2025-06-27 00:35:11 +02:00
}
2025-07-05 10:59:37 +02:00
// Construire la requête Overpass
$overpassQuery = '[out:csv(::id,::type,name,amenity,shop,office,craft,leisure,healthcare,emergency,man_made,power,highway,railway,public_transport,landuse,historic,barrier,tourism,sport,place,waterway,natural,geological,route,military,traffic_sign,traffic_calming,seamark,route_master,water,airway,aerialway,building,other;true;false;false)]' . "\n";
$overpassQuery .= 'area["ref:INSEE"="' . $insee_code . '"]->.searchArea;' . "\n";
$overpassQuery .= 'nwr["amenity"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["shop"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["office"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["craft"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["leisure"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["healthcare"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["emergency"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["man_made"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["power"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["highway"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["railway"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["public_transport"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["landuse"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["historic"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["barrier"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["tourism"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["sport"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["place"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["waterway"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["natural"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["geological"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["route"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["military"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["traffic_sign"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["traffic_calming"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["seamark"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["route_master"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["water"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["airway"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["aerialway"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["building"]["name"](area.searchArea);' . "\n";
$overpassQuery .= 'nwr["other"]["name"](area.searchArea);' . "\n";
$url = 'https://overpass-api.de/api/interpreter?data=' . urlencode($overpassQuery);
// Rediriger vers l'API Overpass
return $this->redirect($url);
}
#[Route('/admin/export-table-csv/{insee_code}', name: 'app_admin_export_table_csv')]
public function exportTableCsv($insee_code): Response
{
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
2025-07-05 16:15:56 +02:00
2025-07-05 10:59:37 +02:00
if (!$stats) {
throw $this->createNotFoundException('Stats non trouvées pour ce code INSEE');
}
2025-06-27 00:35:11 +02:00
2025-07-05 10:59:37 +02:00
$response = new Response();
$response->headers->set('Content-Type', 'text/csv; charset=utf-8');
$response->headers->set('Content-Disposition', 'attachment; filename="lieux_' . $insee_code . '_' . date('Y-m-d') . '.csv"');
$output = fopen('php://temp', 'r+');
// En-têtes CSV
fputcsv($output, [
'Nom',
'Email',
'Contenu email',
'Completion %',
'Type',
'Adresse',
'Numéro',
'Rue',
'Site web',
'Accès PMR',
'Note',
'Texte de la note',
'SIRET',
'SIRET clos',
'Dernière modif. OSM',
'Utilisateur OSM',
'Lien OSM'
], ';');
// Données
foreach ($stats->getPlaces() as $place) {
$osmKind = $place->getOsmKind();
$osmId = $place->getOsmId();
$osmLink = ($osmKind && $osmId) ? 'https://www.openstreetmap.org/' . $osmKind . '/' . $osmId : '';
2025-07-05 16:15:56 +02:00
2025-07-05 10:59:37 +02:00
// Construire l'adresse complète
$address = '';
if ($place->getHousenumber() && $place->getStreet()) {
$address = $place->getHousenumber() . ' ' . $place->getStreet();
} elseif ($place->getStreet()) {
$address = $place->getStreet();
}
2025-07-05 16:15:56 +02:00
2025-07-05 10:59:37 +02:00
fputcsv($output, [
$place->getName() ?: '(sans nom)',
$place->getEmail() ?: '',
$place->getEmailContent() ?: '',
$place->getCompletionPercentage(),
$place->getMainTag() ?: '',
$address,
$place->getHousenumber() ?: '',
$place->getStreet() ?: '',
$place->hasWebsite() ? 'Oui' : 'Non',
$place->hasWheelchair() ? 'Oui' : 'Non',
$place->getNote() ? 'Oui' : 'Non',
$place->getNoteContent() ?: '',
$place->getSiret() ?: '',
'', // SIRET clos - à implémenter si nécessaire
$place->getOsmDataDate() ? $place->getOsmDataDate()->format('Y-m-d H:i') : '',
$place->getOsmUser() ?: '',
$osmLink
], ';');
}
rewind($output);
$csv = stream_get_contents($output);
fclose($output);
$response->setContent($csv);
return $response;
}
2025-07-05 14:31:50 +02:00
#[Route('/admin/test-ctc/{insee_code}', name: 'admin_test_ctc', requirements: ['insee_code' => '\d+'], defaults: ['insee_code' => null])]
public function testCTC(Request $request, ?string $insee_code = null): Response
{
$json = null;
$url = null;
$error = null;
$stats = null;
if ($insee_code) {
$stats = $this->entityManager->getRepository(\App\Entity\Stats::class)->findOneBy(['zone' => $insee_code]);
if ($stats) {
$url = $stats->getCTCurlBase();
try {
$json = file_get_contents($url . '_last_stats.json');
} catch (\Exception $e) {
$error = $e->getMessage();
}
} else {
2025-07-15 21:22:02 +02:00
$error = "4 Aucune stats trouvée pour ce code INSEE.";
2025-07-05 14:31:50 +02:00
}
}
return $this->render('admin/test_ctc.html.twig', [
'insee_code' => $insee_code,
'url' => $url ? $url . '_last_stats.json' : null,
'json' => $json,
'error' => $error,
'stats' => $stats
]);
}
2025-07-05 16:53:12 +02:00
#[Route('/admin/export_csv', name: 'app_admin_export_csv_all')]
public function export_csv_all(): Response
{
$statsList = $this->entityManager->getRepository(\App\Entity\Stats::class)->findAll();
$handle = fopen('php://temp', 'r+');
// En-tête CSV
fputcsv($handle, [
'zone', 'name', 'lat', 'lon', 'population', 'budgetAnnuel', 'completionPercent', 'placesCount', 'avecHoraires', 'avecAdresse', 'avecSite', 'avecAccessibilite', 'avecNote', 'siren', 'codeEpci', 'codesPostaux'
]);
foreach ($statsList as $stat) {
fputcsv($handle, [
$stat->getZone(),
$stat->getName(),
$stat->getLat(),
$stat->getLon(),
$stat->getPopulation(),
$stat->getBudgetAnnuel(),
$stat->getCompletionPercent(),
$stat->getPlacesCount(),
$stat->getAvecHoraires(),
$stat->getAvecAdresse(),
$stat->getAvecSite(),
$stat->getAvecAccessibilite(),
$stat->getAvecNote(),
$stat->getSiren(),
$stat->getCodeEpci(),
$stat->getCodesPostaux(),
]);
}
rewind($handle);
$csv = stream_get_contents($handle);
fclose($handle);
$response = new Response($csv);
$response->headers->set('Content-Type', 'text/csv');
$response->headers->set('Content-Disposition', 'attachment; filename="osm-commerces-villes-export_' . date('Y-m-d_H-i-s') . '.csv"');
return $response;
}
public static function getTagEmoji(string $mainTag): string
{
2025-07-12 13:32:08 +02:00
// Si c'est un tag clé=valeur, on garde le match existant
if (str_contains($mainTag, '=')) {
return match ($mainTag) {
'amenity=restaurant', 'amenity=bar', 'amenity=cafe' => '🍽️',
'amenity=townhall', 'amenity=community_centre' => '🏛️',
'amenity=bank', 'amenity=atm' => '🏦',
'amenity=pharmacy', 'amenity=hospital', 'amenity=clinic' => '🏥',
'amenity=school', 'amenity=kindergarten', 'amenity=university' => '🎓',
'amenity=library', 'amenity=museum', 'amenity=artwork' => '📚',
'shop=car_repair', 'shop=car_parts', 'shop=car_wash' => '🚗',
'amenity=post_office' => '📮',
'shop=convenience' => '🏪',
'shop=supermarket' => '🛒',
'shop=clothes' => '👕',
default => '🏷️',
};
}
// Sinon, on regarde si c'est un tag principal simple
2025-07-05 16:53:12 +02:00
return match ($mainTag) {
2025-07-12 13:32:08 +02:00
'bicycle_parking' => '🚲',
'building' => '🏢',
'email' => '📧',
'fire_hydrant' => '🚒',
'charging_station' => '⚡',
'toilets' => '🚻',
'bus_stop' => '🚌',
'defibrillator' => '❤️‍🩹',
'camera' => '📷',
'recycling' => '♻️',
'substation' => '🏭',
'laboratory' => '🧪',
'school' => '🏫',
'police' => '👮',
'healthcare' => '🏥',
'advertising_board' => '🪧',
'bench' => '🪑',
'waste_basket' => '🗑️',
'street_lamp' => '💡',
'drinking_water' => '🚰',
'tree' => '🌳',
'places' => '📍',
'power_pole' => '⚡',
2025-07-05 16:53:12 +02:00
default => '🏷️',
};
}
public function followupEmbedGraph(Request $request, string $insee_code, string $theme): Response
{
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
if (!$stats) {
2025-07-15 21:22:02 +02:00
$this->addFlash('error', '5 Aucune stats trouvée pour ce code INSEE.');
return $this->redirectToRoute('app_admin');
}
$themes = \App\Service\FollowUpService::getFollowUpThemes();
if (!isset($themes[$theme])) {
$this->addFlash('error', 'Thème non reconnu.');
return $this->redirectToRoute('app_admin_stats', ['insee_code' => $insee_code]);
}
// Récupérer toutes les données de followup pour ce thème
$followups = $stats->getCityFollowUps();
$countData = [];
$completionData = [];
foreach ($followups as $fu) {
if ($fu->getName() === $theme . '_count') {
$countData[] = [
'date' => $fu->getDate()->format('Y-m-d'),
'value' => $fu->getMeasure()
];
}
if ($fu->getName() === $theme . '_completion') {
$completionData[] = [
'date' => $fu->getDate()->format('Y-m-d'),
'value' => $fu->getMeasure()
];
}
}
// Trier par date
usort($countData, fn($a, $b) => $a['date'] <=> $b['date']);
usort($completionData, fn($a, $b) => $a['date'] <=> $b['date']);
// Récupérer les objets du thème (Place) pour la ville
$places = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]);
$motocultrice = $this->motocultrice;
$objects = [];
// Récupérer la correspondance thème <-> requête Overpass
$themeQueries = \App\Service\FollowUpService::getFollowUpOverpassQueries();
$overpass_type_query = $themeQueries[$theme] ?? '';
if ($overpass_type_query) {
$overpass_query = "[out:json][timeout:60];\narea[\"ref:INSEE\"=\"$insee_code\"]->.searchArea;\n($overpass_type_query);\n(._;>;);\nout meta;\n>;";
$josm_url = 'http://127.0.0.1:8111/import?url=https://overpass-api.de/api/interpreter?data=' . urlencode($overpass_query);
} else {
$josm_url = null;
}
// Fonction utilitaire pour extraire clé/valeur de la requête Overpass
$extractTag = function ($query) {
if (preg_match('/\\[([a-zA-Z0-9:_-]+)\\]="([^"]+)"/', $query, $matches)) {
return [$matches[1], $matches[2]];
}
return [null, null];
};
list($tagKey, $tagValue) = $extractTag($themeQueries[$theme] ?? '');
foreach ($places as $place) {
$match = false;
2025-07-05 17:35:20 +02:00
$main_tag = $place->getMainTag();
// Cas particuliers multi-valeurs (ex: healthcare)
if ($theme === 'healthcare') {
if ($main_tag && (
2025-08-08 18:51:44 +02:00
str_starts_with($main_tag, 'healthcare=') ||
in_array($main_tag, [
'amenity=doctors',
'amenity=pharmacy',
'amenity=hospital',
'amenity=clinic',
'amenity=social_facility'
])
)) {
$match = true;
}
2025-07-05 17:35:20 +02:00
} else {
// Détection générique : si le mainTag correspond à la clé/valeur du thème
if ($tagKey && $tagValue && $main_tag === "$tagKey=$tagValue") {
$match = true;
}
}
2025-07-05 17:35:20 +02:00
// Ajouter l'objet si match
if ($match && $place->getLat() && $place->getLon()) {
$objects[] = [
'id' => $place->getOsmId(),
'osm_kind' => $place->getOsmKind(),
'lat' => $place->getLat(),
'lon' => $place->getLon(),
'name' => $place->getName(),
'tags' => ['main_tag' => $place->getMainTag()],
'is_complete' => !empty($place->getName()),
'osm_url' => 'https://www.openstreetmap.org/' . $place->getOsmKind() . '/' . $place->getOsmId(),
'uuid' => $place->getUuidForUrl(),
'zip_code' => $place->getZipCode(),
];
}
}
$geojson = [
'type' => 'FeatureCollection',
'features' => array_map(function ($obj) {
return [
'type' => 'Feature',
'geometry' => [
'type' => 'Point',
'coordinates' => [$obj['lon'], $obj['lat']]
],
'properties' => $obj
];
}, $objects)
];
// Centre de la carte : centre géographique des objets ou de la ville
$center = null;
if (count($objects) > 0) {
$lat = array_sum(array_column($objects, 'lat')) / count($objects);
$lon = array_sum(array_column($objects, 'lon')) / count($objects);
$center = [$lon, $lat];
} elseif ($stats->getPlaces()->count() > 0) {
$first = $stats->getPlaces()->first();
$center = [$first->getLon(), $first->getLat()];
}
return $this->render('admin/followup_embed_graph.html.twig', [
'stats' => $stats,
'theme' => $theme,
'theme_label' => $themes[$theme],
'count_data' => json_encode($countData),
'completion_data' => json_encode($completionData),
'icons' => \App\Service\FollowUpService::getFollowUpIcons(),
'geojson' => json_encode($geojson),
'overpass_query' => $overpass_query,
'josm_url' => $josm_url,
'center' => $center,
'maptiler_token' => $_ENV['MAPTILER_TOKEN'] ?? null,
'completion_tags' => \App\Service\FollowUpService::getFollowUpCompletionTags(),
]);
}
#[Route('/admin/followup-graph/{insee_code}', name: 'admin_followup_graph', requirements: ['insee_code' => '\d+'])]
public function followupGraph(Request $request, string $insee_code): Response
{
2025-07-14 18:17:41 +02:00
$ctc_completion_series = [];
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
if (!$stats) {
2025-07-15 21:22:02 +02:00
$this->addFlash('error', '6 Aucune stats trouvée pour ce code INSEE.');
2025-07-14 18:17:41 +02:00
return $this->render('admin/followup_graph.html.twig', [
'stats' => null,
'completion_tags' => \App\Service\FollowUpService::getFollowUpCompletionTags(),
'followup_labels' => \App\Service\FollowUpService::getFollowUpThemes(),
'followup_icons' => \App\Service\FollowUpService::getFollowUpIcons(),
'ctc_completion_series' => $ctc_completion_series,
]);
}
$themes = \App\Service\FollowUpService::getFollowUpThemes();
2025-07-14 18:17:41 +02:00
// Ajout : mesures CTC CityFollowUp pour le graphique séparé
foreach ($stats->getCityFollowUps() as $fu) {
if (preg_match('/^(name|hours|website|address|siret)_count$/', $fu->getName())) {
$ctc_completion_series[$fu->getName()][] = [
'date' => $fu->getDate()->format('Y-m-d'),
'value' => $fu->getMeasure(),
];
}
}
foreach ($ctc_completion_series as &$points) {
2025-08-08 18:51:44 +02:00
usort($points, function ($a, $b) {
2025-07-14 18:17:41 +02:00
return strcmp($a['date'], $b['date']);
});
}
unset($points);
$now = new \DateTime();
$last_week = new \DateTime();
$last_week->sub(new \DateInterval('P7D'));
$adiff_query = '[timeout:60]
[adiff:"' . $now->format('Y-m-d') . 'T' . $now->format('H:i:s') . 'Z","2025-08-05T00:00:00Z"][out:xml];
area["ref:INSEE"="91111"]->.searchArea;
(
nwr(area.searchArea);
);
out meta;';
return $this->render('admin/followup_graph.html.twig', [
'adiff_query' => $adiff_query,
'stats' => $stats,
'completion_tags' => \App\Service\FollowUpService::getFollowUpCompletionTags(),
'followup_labels' => \App\Service\FollowUpService::getFollowUpThemes(),
2025-07-05 17:35:20 +02:00
'followup_icons' => \App\Service\FollowUpService::getFollowUpIcons(),
2025-07-14 18:17:41 +02:00
'ctc_completion_series' => $ctc_completion_series,
]);
}
2025-07-07 23:30:09 +02:00
// Dans la méthode de suppression de ville (ex: deleteCity ou similaire)
public function deleteCityAction(Request $request, $id): Response
{
if (!$this->allowDeleteCity) {
$this->addFlash('danger', "La suppression de ville est désactivée par configuration.");
return $this->redirectToRoute('admin_dashboard');
}
// ... logique de suppression existante ...
// Pour éviter l'erreur, on retourne une redirection par défaut si rien n'est fait
return $this->redirectToRoute('admin_dashboard');
}
#[Route('/admin/stats/{insee_code}/street-completion', name: 'admin_street_completion', requirements: ['insee_code' => '\d+'])]
public function streetCompletion(string $insee_code): Response
{
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
if (!$stats) {
2025-07-15 21:22:02 +02:00
$this->addFlash('error', '7 Aucune stats trouvée pour ce code INSEE.');
return $this->redirectToRoute('app_admin');
}
$places = $stats->getPlaces();
$rues = [];
foreach ($places as $place) {
$rue = $place->getStreet() ?: '(sans nom)';
if (!isset($rues[$rue])) {
2025-08-08 18:51:44 +02:00
$rues[$rue] = ['places' => [], 'completion_sum' => 0];
}
$rues[$rue]['places'][] = $place;
$rues[$rue]['completion_sum'] += $place->getCompletionPercentage();
}
$rues_data = [];
foreach ($rues as $nom => $data) {
$count = count($data['places']);
$avg = $count > 0 ? round($data['completion_sum'] / $count, 1) : 0;
$rues_data[] = [
'name' => $nom,
'count' => $count,
'avg_completion' => $avg,
];
}
// Tri décroissant par complétion moyenne
usort($rues_data, fn($a, $b) => $b['avg_completion'] <=> $a['avg_completion']);
return $this->render('admin/street_completion.html.twig', [
'stats' => $stats,
'rues' => $rues_data,
'insee_code' => $insee_code,
]);
}
2025-07-14 19:27:07 +02:00
#[Route('/admin/speed-limit/{insee_code}', name: 'admin_speed_limit', requirements: ['insee_code' => '\d+'])]
public function speedLimit(string $insee_code): Response
{
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
if (!$stats) {
2025-07-15 21:22:02 +02:00
$this->addFlash('error', '8 Aucune stats trouvée pour ce code INSEE. Veuillez d\'abord ajouter la ville.');
2025-07-14 19:27:07 +02:00
return $this->redirectToRoute('app_admin_import_stats');
}
// Tags attendus pour la complétion
$expected_tags = ['maxspeed', 'highway'];
// On transmet le code INSEE et le nom de la ville au template
return $this->render('admin/speed_limit.html.twig', [
'stats' => $stats,
'insee_code' => $insee_code,
'expected_tags' => $expected_tags,
'maptiler_token' => $_ENV['MAPTILER_TOKEN'] ?? null,
]);
}
2025-07-16 17:00:09 +02:00
#[Route('/admin/demandes', name: 'app_admin_demandes')]
public function listDemandes(Request $request): Response
{
$status = $request->query->get('status');
$repository = $this->entityManager->getRepository(\App\Entity\Demande::class);
if ($status) {
$demandes = $repository->findByStatus($status);
} else {
$demandes = $repository->findAllOrderedByCreatedAt();
}
// Get all possible statuses for the filter
$allStatuses = ['new', 'email_provided', 'ready', 'email_sent', 'email_failed', 'email_opened', 'edit_form_opened', 'place_modified', 'linked_to_place'];
// Count demandes for each status
$statusCounts = [];
foreach ($allStatuses as $statusValue) {
$statusCounts[$statusValue] = $repository->findByStatus($statusValue);
}
// Get total count
$totalCount = $repository->findAllOrderedByCreatedAt();
return $this->render('admin/demandes/list.html.twig', [
'demandes' => $demandes,
'current_status' => $status,
'all_statuses' => $allStatuses,
'status_counts' => $statusCounts,
'total_count' => count($totalCount)
]);
}
#[Route('/admin/demandes/{id}/edit', name: 'app_admin_demande_edit')]
public function editDemande(int $id, Request $request): Response
{
$demande = $this->entityManager->getRepository(\App\Entity\Demande::class)->find($id);
if (!$demande) {
$this->addFlash('error', 'Demande non trouvée');
return $this->redirectToRoute('app_admin_demandes');
}
if ($request->isMethod('POST')) {
$placeUuid = $request->request->get('placeUuid');
if ($placeUuid) {
// Check if the Place exists
$place = $this->entityManager->getRepository(Place::class)->findOneBy(['uuid_for_url' => $placeUuid]);
if ($place) {
$demande->setPlaceUuid($placeUuid);
$demande->setPlace($place);
$demande->setStatus('linked_to_place');
// Set OSM object type and OSM ID from the Place
$demande->setOsmObjectType($place->getOsmKind());
2025-07-18 14:21:55 +02:00
$demande->setOsmId($place->getOsmId());
2025-07-16 17:00:09 +02:00
$this->entityManager->persist($demande);
$this->entityManager->flush();
$this->addFlash('success', 'Demande mise à jour avec succès');
} else {
$this->addFlash('error', 'Place non trouvée avec cet UUID');
}
}
}
return $this->render('admin/demandes/edit.html.twig', [
'demande' => $demande
]);
}
#[Route('/admin/contacted-places', name: 'app_admin_contacted_places')]
public function listContactedPlaces(): Response
{
$demandes = $this->entityManager->getRepository(\App\Entity\Demande::class)->findPlacesWithContactAttempt();
return $this->render('admin/demandes/contacted_places.html.twig', [
'demandes' => $demandes
]);
}
#[Route('/admin/demandes/{id}/send-email', name: 'app_admin_demande_send_email')]
public function sendEmailToDemande(int $id, \Symfony\Component\Mailer\MailerInterface $mailer): Response
{
$demande = $this->entityManager->getRepository(\App\Entity\Demande::class)->find($id);
if (!$demande) {
$this->addFlash('error', 'Demande non trouvée');
return $this->redirectToRoute('app_admin_demandes');
}
$place = $demande->getPlace();
if (!$place) {
$this->addFlash('error', 'Aucune place associée à cette demande');
return $this->redirectToRoute('app_admin_demande_edit', ['id' => $id]);
}
// Check if the place has an email
if (!$place->getEmail() && !$demande->getEmail()) {
$this->addFlash('error', 'Aucun email associé à cette place ou à cette demande');
return $this->redirectToRoute('app_admin_demande_edit', ['id' => $id]);
}
// Use the email from the place if available, otherwise use the email from the demande
$email = $place->getEmail() ?: $demande->getEmail();
// Generate the email content
$emailContent = $this->renderView('admin/email_content.html.twig', [
'place' => $place
]);
// Only send the email in production environment
if ($this->getParameter('kernel.environment') === 'prod') {
$message = (new \Symfony\Component\Mime\Email())
->from('contact@osm-commerce.fr')
->to($email)
->subject('Votre lien de modification OpenStreetMap')
->html($emailContent);
try {
$mailer->send($message);
} catch (\Throwable $e) {
$this->actionLogger->log('ERROR_envoi_email', [
'demande_id' => $demande->getId(),
'place_id' => $place->getId(),
'message' => $e->getMessage(),
]);
$this->addFlash('error', 'Erreur lors de l\'envoi de l\'email : ' . $e->getMessage());
return $this->redirectToRoute('app_admin_demande_edit', ['id' => $id]);
}
} else {
// In non-production environments, just log the attempt
$this->actionLogger->log('email_would_be_sent', [
'demande_id' => $demande->getId(),
'place_id' => $place->getId(),
'email' => $email,
'content' => $emailContent
]);
$this->addFlash('info', 'En environnement de production, un email serait envoyé à ' . $email);
}
// Update the last contact attempt date and set status to email_sent
$now = new \DateTime();
$demande->setLastContactAttempt($now);
$demande->setStatus('email_sent');
$place->setLastContactAttemptDate($now);
$this->entityManager->persist($demande);
$this->entityManager->persist($place);
$this->entityManager->flush();
$this->addFlash('success', 'Email envoyé avec succès');
return $this->redirectToRoute('app_admin_contacted_places');
}
2025-08-08 19:04:42 +02:00
/**
* Remove duplicate places from a Stats entity
* Duplicates are identified by having the same osm_id and osm_kind
*/
private function removeDuplicatePlaces(Stats $stats): void
{
$placeRepo = $this->entityManager->getRepository(Place::class);
// Get all places for this stats
$places = $placeRepo->findBy(['stats' => $stats]);
// Group places by osm_id and osm_kind
$groupedPlaces = [];
foreach ($places as $place) {
$key = $place->getOsmKind() . '_' . $place->getOsmId();
if (!isset($groupedPlaces[$key])) {
$groupedPlaces[$key] = [];
}
$groupedPlaces[$key][] = $place;
}
// For each group with more than one place, keep the first one and remove the rest
$removedCount = 0;
foreach ($groupedPlaces as $key => $group) {
if (count($group) > 1) {
// Keep the first place
$keepPlace = array_shift($group);
// Remove the rest
foreach ($group as $duplicatePlace) {
$stats->removePlace($duplicatePlace);
$this->entityManager->remove($duplicatePlace);
$removedCount++;
}
}
}
// If places were removed, update the places_count
if ($removedCount > 0) {
$stats->setPlacesCount($stats->getPlaces()->count());
$this->entityManager->persist($stats);
}
}
2025-05-26 11:55:44 +02:00
}