followUpService = $followUpService;
}
#[Route('/admin/labourer-toutes-les-zones', name: 'app_admin_labourer_tout')]
public function labourer_tout(): Response
{
$this->actionLogger->log('labourer_toutes_les_zones', []);
$updateExisting = true;
$stats_all = $this->entityManager->getRepository(Stats::class)->findAll();
echo 'on a trouvé ' . count($stats_all) . ' zones à labourer
';
foreach ($stats_all as $stats) {
echo '
on laboure la zone ' . $stats->getZone() . ' ';
$processedCount = 0;
$updatedCount = 0;
$insee_code = $stats->getZone();
// Vérifier si le code INSEE est un nombre valide
// Vérifier si les stats ont été modifiées il y a moins de 24h
if ($stats->getDateModified() !== null) {
$now = new \DateTime();
$diff = $now->diff($stats->getDateModified());
$hours = $diff->h + ($diff->days * 24);
if ($hours < 24) {
echo 'Stats modifiées il y a moins de 24h - on passe au suivant
';
continue;
}
}
if (!is_numeric($insee_code) || $insee_code == 'undefined' || $insee_code == '') {
echo 'Code INSEE invalide : ' . $insee_code . ' - on passe au suivant
';
continue;
}
$places_overpass = $this->motocultrice->labourer($stats->getZone());
$places = $places_overpass;
foreach ($places as $placeData) {
// Vérifier si le lieu existe déjà
$existingPlace = $this->entityManager->getRepository(Place::class)
->findOneBy(['osmId' => $placeData['id']]);
if (!$existingPlace) {
$place = new Place();
$place->setOsmId($placeData['id'])
->setOsmKind($placeData['type'])
->setZipCode($insee_code)
->setUuidForUrl($this->motocultrice->uuid_create())
->setModifiedDate(new \DateTime())
->setStats($stats)
->setDead(false)
->setOptedOut(false)
->setMainTag($this->motocultrice->find_main_tag($placeData['tags']) ?? '')
->setStreet($this->motocultrice->find_street($placeData['tags']) ?? '')
->setHousenumber($this->motocultrice->find_housenumber($placeData['tags']) ?? '')
->setSiret($this->motocultrice->find_siret($placeData['tags']) ?? '')
->setAskedHumainsSupport(false)
->setLastContactAttemptDate(null)
->setPlaceCount(0)
// ->setOsmData($placeData['modified'] ?? null)
;
// Mettre à jour les données depuis Overpass
$place->update_place_from_overpass_data($placeData);
$this->entityManager->persist($place);
$stats->addPlace($place);
$processedCount++;
// 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);
$stats->addPlace($existingPlace);
$this->entityManager->persist($existingPlace);
$updatedCount++;
}
}
// mettre à jour les stats
// Récupérer tous les commerces de la zone
$commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]);
// Récupérer les stats existantes pour la zone
$stats_exist = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
if ($stats_exist) {
$stats = $stats_exist;
} else {
$stats = new Stats();
dump('nouvelle stat', $insee_code);
die();
$stats->setZone($insee_code);
}
$urls = $stats->getAllCTCUrlsMap();
$statsHistory = $this->entityManager->getRepository(StatsHistory::class)
->createQueryBuilder('sh')
->where('sh.stats = :stats')
->setParameter('stats', $stats)
->orderBy('sh.id', 'DESC')
->setMaxResults(365)
->getQuery()
->getResult();
// Calculer les statistiques
$calculatedStats = $this->motocultrice->calculateStats($commerces);
// Mettre à jour les stats pour la zone donnée
$stats->setPlacesCount($calculatedStats['places_count']);
$stats->setAvecHoraires($calculatedStats['counters']['avec_horaires']);
$stats->setAvecAdresse($calculatedStats['counters']['avec_adresse']);
$stats->setAvecSite($calculatedStats['counters']['avec_site']);
$stats->setAvecAccessibilite($calculatedStats['counters']['avec_accessibilite']);
$stats->setAvecNote($calculatedStats['counters']['avec_note']);
$stats->setCompletionPercent($calculatedStats['completion_percent']);
// Associer les stats à chaque commerce
foreach ($commerces as $commerce) {
$commerce->setStats($stats);
// Injection de l'emoji pour le template
$mainTag = $commerce->getMainTag();
$emoji = self::getTagEmoji($mainTag);
$commerce->tagEmoji = $emoji;
$this->entityManager->persist($commerce);
}
$stats->computeCompletionPercent();
// Calculer les statistiques de fraîcheur des données OSM
$timestamps = [];
foreach ($stats->getPlaces() as $place) {
if ($place->getOsmDataDate()) {
$timestamps[] = $place->getOsmDataDate()->getTimestamp();
}
}
if (!empty($timestamps)) {
// Date la plus ancienne (min)
$minTimestamp = min($timestamps);
$stats->setOsmDataDateMin(new \DateTime('@' . $minTimestamp));
// Date la plus récente (max)
$maxTimestamp = max($timestamps);
$stats->setOsmDataDateMax(new \DateTime('@' . $maxTimestamp));
// Date moyenne
$avgTimestamp = array_sum($timestamps) / count($timestamps);
$stats->setOsmDataDateAvg(new \DateTime('@' . (int)$avgTimestamp));
}
if ($stats->getDateCreated() == null) {
$stats->setDateCreated(new \DateTime());
}
$stats->setDateModified(new \DateTime());
// Créer un historique des statistiques
$statsHistory = new StatsHistory();
$statsHistory->setDate(new \DateTime())
->setStats($stats);
// Compter les Places avec email et SIRET
$placesWithEmail = 0;
$placesWithSiret = 0;
foreach ($stats->getPlaces() as $place) {
if ($place->getEmail() && $place->getEmail() !== '') {
$placesWithEmail++;
}
if ($place->getSiret() && $place->getSiret() !== '') {
$placesWithSiret++;
}
}
$statsHistory->setPlacesCount($stats->getPlaces()->count())
->setOpeningHoursCount($stats->getAvecHoraires())
->setAddressCount($stats->getAvecAdresse())
->setWebsiteCount($stats->getAvecSite())
->setSiretCount($placesWithSiret)
->setEmailsCount($placesWithEmail)
->setCompletionPercent($stats->getCompletionPercent())
->setStats($stats);
$this->entityManager->persist($statsHistory);
$this->entityManager->persist($stats);
$this->entityManager->flush();
// 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();
// 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);
}
$this->entityManager->flush();
// 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]);
}
$this->entityManager->flush();
$this->addFlash('success', 'Labourage des ' . count($stats_all) . ' zones terminé avec succès.');
return $this->redirectToRoute('app_public_dashboard');
}
#[Route('/admin', name: 'app_admin')]
public function index(): Response
{
return $this->render('admin/index.html.twig', [
'controller_name' => 'AdminController',
]);
}
#[Route('/admin/stats/{insee_code}', name: 'app_admin_stats', requirements: ['insee_code' => '\\d+'])]
public function stats(string $insee_code): Response
{
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
if (!$stats) {
$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');
}
$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) {
$this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager);
$followups = $stats->getCityFollowUps();
}
$commerces = $stats->getPlaces();
$this->actionLogger->log('stats_de_ville', ['insee_code' => $insee_code, 'nom' => $stats->getZone()]);
// Récupérer tous les commerces de la zone
// $commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code, 'dead' => false]);
if (!$stats) {
// Si aucune stat n'existe, on en crée une vide pour éviter les erreurs, mais sans la sauvegarder
$stats = new Stats();
$stats->setZone($insee_code);
$stats->setName('Nouvelle zone non labourée');
}
$urls = $stats->getAllCTCUrlsMap();
$statsHistory = $this->entityManager->getRepository(StatsHistory::class)
->createQueryBuilder('sh')
->where('sh.stats = :stats')
->setParameter('stats', $stats)
->orderBy('sh.id', 'DESC')
->setMaxResults(100)
->getQuery()
->getResult();
// Données pour le graphique des modifications par trimestre
$modificationsByQuarter = [];
foreach ($commerces as $commerce) {
if ($commerce->getOsmDataDate()) {
$date = $commerce->getOsmDataDate();
$year = $date->format('Y');
$quarter = ceil($date->format('n') / 3);
$key = $year . '-Q' . $quarter;
if (!isset($modificationsByQuarter[$key])) {
$modificationsByQuarter[$key] = 0;
}
$modificationsByQuarter[$key]++;
}
}
ksort($modificationsByQuarter); // Trier par clé (année-trimestre)
$geojson = [
'type' => 'FeatureCollection',
'features' => []
];
foreach ($commerces as $commerce) {
if ($commerce->getLat() && $commerce->getLon()) {
$geojson['features'][] = [
'type' => 'Feature',
'geometry' => [
'type' => 'Point',
'coordinates' => [$commerce->getLon(), $commerce->getLat()]
],
'properties' => [
'id' => $commerce->getOsmId(),
'name' => $commerce->getName(),
'main_tag' => $commerce->getMainTag(),
'address' => $commerce->getStreet() . ' ' . $commerce->getHousenumber(),
'note' => $commerce->getNoteContent(),
'osm_url' => 'https://www.openstreetmap.org/' . $commerce->getOsmKind() . '/' . $commerce->getOsmId()
]
];
}
}
// 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) +'
. ' (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'
)
->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);
// 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;
}
}
unset($row);
// Tri décroissant sur le score normalisé
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 = [];
$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') {
if ($count === null) {
$count = $fu;
} else if ($fu->getDate() > $count->getDate()) {
$count = $fu;
}
}
if ($fu->getName() === $type . '_completion') {
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') {
if ($count === null) {
$count = $fu;
} else if ($fu->getDate() > $count->getDate()) {
$count = $fu;
}
}
if ($fu->getName() === 'places_completion') {
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;
// 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');
// --- 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) {
usort($points, function($a, $b) {
return strcmp($a['date'], $b['date']);
});
}
unset($points);
return $this->render('admin/stats.html.twig', [
'stats' => $stats,
'commerces' => $commerces,
'urls' => $urls,
'geojson' => json_encode($geojson),
'modificationsByQuarter' => json_encode($modificationsByQuarter),
'maptiler_token' => $_ENV['MAPTILER_TOKEN'],
'statsHistory' => $statsHistory,
'CTC_urls' => $urls,
'overpass' => '',
'podium_local' => $podium_local,
'latestFollowups' => $latestFollowups,
'followup_labels' => \App\Service\FollowUpService::getFollowUpThemes(),
'followup_icons' => \App\Service\FollowUpService::getFollowUpIcons(),
'progression7Days' => $progression7Days,
'all_types' => \App\Service\FollowUpService::getFollowUpThemes(),
'getTagEmoji' => [self::class, 'getTagEmoji'],
'completion_tags' => \App\Service\FollowUpService::getFollowUpCompletionTags(),
'ctc_completion_series' => $ctc_completion_series,
]);
}
#[Route('/admin/stats/{insee_code}/followup-graph/{theme}', name: 'admin_followup_theme_graph', requirements: ['insee_code' => '\d+'])]
public function followupThemeGraph(string $insee_code, string $theme): Response
{
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
if (!$stats) {
$this->addFlash('error', '2 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] ?? '');
// 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 = [];
foreach ($places as $place) {
$match = false;
$main_tag = $place->getMainTag();
// Cas particuliers multi-valeurs (ex: healthcare)
if ($theme === 'healthcare') {
if ($main_tag && (
str_starts_with($main_tag, 'healthcare=') ||
in_array($main_tag, [
'amenity=doctors',
'amenity=pharmacy',
'amenity=hospital',
'amenity=clinic',
'amenity=social_facility'
])
)) {
$match = true;
}
} 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;
}
}
// 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(),
];
$debug_filtered[] = $main_tag;
}
}
$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));
}
$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_theme_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(),
'followup_labels' => $themes,
'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(),
'debug_info' => $debug_info,
]);
}
#[Route('/admin/placeType/{osm_kind}/{osm_id}', name: 'app_admin_by_osm_id')]
public function placeType(string $osm_kind, string $osm_id): Response
{
$place = $this->entityManager->getRepository(Place::class)->findOneBy(['osm_kind' => $osm_kind, 'osmId' => $osm_id]);
if ($place) {
$this->actionLogger->log('admin/placeType', [
'osm_kind' => $osm_kind,
'osm_id' => $osm_id,
'name' => $place->getName(),
'code_insee' => $place->getZipCode(),
'uuid' => $place->getUuidForUrl()
]);
return $this->redirectToRoute('app_admin_commerce', ['id' => $place->getId()]);
} else {
$this->actionLogger->log('ERROR_admin/placeType', ['osm_kind' => $osm_kind, 'osm_id' => $osm_id]);
$this->addFlash('error', 'Le lieu n\'existe pas.');
$this->actionLogger->log('ERROR_admin/placeType', ['osm_kind' => $osm_kind, 'osm_id' => $osm_id]);
return $this->redirectToRoute('app_public_index');
}
}
/**
* rediriger vers l'url unique quand on est admin
*/
#[Route('/admin/commerce/{id}', name: 'app_admin_commerce')]
public function commerce(int $id): Response
{
// Vérifier si on est en prod
if ($this->getParameter('kernel.environment') === 'prod') {
$this->addFlash('error', 'Vous n\'avez pas accès à cette page en production.');
return $this->redirectToRoute('app_public_index');
}
$commerce = $this->entityManager->getRepository(Place::class)->find($id);
if (!$commerce) {
throw $this->createNotFoundException('Commerce non trouvé');
$this->actionLogger->log('ERROR_admin_show_commerce_form_id', ['id' => $id]);
}
$this->actionLogger->log('admin_show_commerce_form_id', [
'id' => $id,
'name' => $commerce->getName(),
'code_insee' => $commerce->getZipCode(),
'uuid' => $commerce->getUuidForUrl()
]);
// Redirection vers la page de modification avec les paramètres nécessaires
return $this->redirectToRoute('app_public_edit', [
'zipcode' => $commerce->getZipCode(),
'name' => $commerce->getName() != '' ? $commerce->getName() : '?',
'uuid' => $commerce->getUuidForUrl()
]);
}
/**
* récupérer les commerces de la zone selon le code INSEE, créer les nouveaux lieux, et mettre à jour les existants
*/
#[Route('/admin/labourer/{insee_code}', name: 'app_admin_labourer')]
public function labourer(Request $request, string $insee_code, bool $updateExisting = true): Response
{
$deleteMissing = $request->query->getBoolean('deleteMissing', true);
$disableFollowUpCleanup = $request->query->getBoolean('disableFollowUpCleanup', false);
$debug = $request->query->getBoolean('debug', false);
$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.');
$this->actionLogger->log('ERROR_labourer_bad_insee', ['insee_code' => $insee_code]);
return $this->redirectToRoute('app_public_index');
}
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
if (!$stats) {
$stats = new Stats();
$stats->setZone($insee_code);
// $this->addFlash('error', '3 Aucune stats trouvée pour ce code INSEE.');
// return $this->redirectToRoute('app_public_index');
}
// 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']);
}
}
} catch (\Exception $e) {}
}
// 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]);
}
}
} catch (\Exception $e) {}
}
// Mettre à jour la date de requête de labourage
$stats->setDateLabourageRequested(new \DateTime());
$this->entityManager->persist($stats);
$this->entityManager->flush();
// 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());
$this->entityManager->persist($stats);
$this->entityManager->flush();
$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.");
}
// Toujours générer les CityFollowUp (mais ne jamais les supprimer)
// $themes = \App\Service\FollowUpService::getFollowUpThemes();
// foreach (array_keys($themes) as $theme) {
$this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager, true);
// }
$this->entityManager->flush();
return $this->redirectToRoute('app_admin_stats', ['insee_code' => $insee_code]);
}
#[Route('/admin/delete/{id}', name: 'app_admin_delete')]
public function delete(int $id): Response
{
$this->actionLogger->log('admin/delete_place', ['id' => $id]);
$commerce = $this->entityManager->getRepository(Place::class)->find($id);
if ($commerce) {
$this->entityManager->remove($commerce);
$this->entityManager->flush();
$this->addFlash('success', 'Le lieu ' . $commerce->getName() . ' a été supprimé avec succès de OSM Mes commerces, mais pas dans OpenStreetMap.');
} else {
$this->addFlash('error', 'Le lieu n\'existe pas.');
}
return $this->redirectToRoute('app_public_dashboard');
}
#[Route('/admin/delete_by_zone/{insee_code}', name: 'app_admin_delete_by_zone')]
public function delete_by_zone(string $insee_code): Response
{
$this->actionLogger->log('admin/delete_by_zone', ['insee_code' => $insee_code]);
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
if (!$stats) {
$this->addFlash('error', 'Aucune statistique trouvée pour la zone ' . $insee_code);
return $this->redirectToRoute('app_public_dashboard');
}
try {
// 1. Supprimer tous les StatsHistory associés
foreach ($stats->getStatsHistories() as $history) {
$this->entityManager->remove($history);
}
// 2. Supprimer tous les Places associées
foreach ($stats->getPlaces() as $place) {
$this->entityManager->remove($place);
}
// 3. Supprimer l'objet Stats lui-même
$this->entityManager->remove($stats);
// 4. Appliquer les changements à la base de données
$this->entityManager->flush();
$this->addFlash('success', 'La zone ' . $insee_code . ' et toutes les données associées ont été supprimées avec succès.');
} catch (\Exception $e) {
$this->addFlash('error', 'Une erreur est survenue lors de la suppression de la zone ' . $insee_code . ': ' . $e->getMessage());
}
return $this->redirectToRoute('app_public_dashboard');
}
#[Route('/admin/export', name: 'app_admin_export')]
public function export(): Response
{
$this->actionLogger->log('export_all_places', []);
$places = $this->entityManager->getRepository(Place::class)->findAll();
$csvData = [];
$csvData[] = [
'Nom',
'Email',
'Code postal',
'ID OSM',
'Type OSM',
'Date de modification',
'Date dernier contact',
'Note',
'Désabonné',
'Inactif',
'Support humain demandé',
'A des horaires',
'A une adresse',
'A un site web',
'A accessibilité',
'A une note'
];
foreach ($places as $place) {
$csvData[] = [
$place->getName(),
$place->getEmail(),
$place->getZipCode(),
$place->getOsmId(),
$place->getOsmKind(),
$place->getModifiedDate() ? $place->getModifiedDate()->format('Y-m-d H:i:s') : '',
$place->getLastContactAttemptDate() ? $place->getLastContactAttemptDate()->format('Y-m-d H:i:s') : '',
$place->getNote(),
$place->isOptedOut() ? 'Oui' : 'Non',
$place->isDead() ? 'Oui' : 'Non',
$place->isAskedHumainsSupport() ? 'Oui' : 'Non',
$place->hasOpeningHours() ? 'Oui' : 'Non',
$place->hasAddress() ? 'Oui' : 'Non',
$place->hasWebsite() ? 'Oui' : 'Non',
$place->hasWheelchair() ? 'Oui' : 'Non',
$place->hasNote() ? 'Oui' : 'Non'
];
}
$response = new Response();
$response->headers->set('Content-Type', 'text/csv');
$response->headers->set('Content-Disposition', 'attachment; filename="export_places.csv"');
$handle = fopen('php://temp', 'r+');
foreach ($csvData as $row) {
fputcsv($handle, $row, ';');
}
rewind($handle);
$response->setContent(stream_get_contents($handle));
fclose($handle);
return $response;
}
#[Route('/admin/export_csv/{insee_code}', name: 'app_admin_export_csv')]
public function export_csv(string $insee_code): Response
{
$this->actionLogger->log('admin/export_csv', ['insee_code' => $insee_code]);
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
$response = new Response($this->motocultrice->export($insee_code));
$response->headers->set('Content-Type', 'text/csv');
$slug_name = str_replace(' ', '-', $stats->getName());
$this->actionLogger->log('export_csv', ['insee_code' => $insee_code, 'slug_name' => $slug_name]);
$response->headers->set('Content-Disposition', 'attachment; filename="osm-commerces-export_' . $insee_code . '_' . $slug_name . '_' . date('Y-m-d_H-i-s') . '.csv"');
return $response;
}
#[Route('/admin/make_email_for_place/{id}', name: 'app_admin_make_email_for_place')]
public function make_email_for_place(Place $place): Response
{
$this->actionLogger->log('admin/make_email_for_place', ['insee_code' => $place->getId()]);
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
{
$this->actionLogger->log('no_more_sollicitation_for_place', ['place_id' => $place->getId()]);
$place->setOptedOut(true);
$this->entityManager->persist($place);
$this->entityManager->flush();
$this->addFlash('success', 'Votre lieu ' . $place->getName() . ' ne sera plus sollicité pour mettre à jour ses informations.');
return $this->redirectToRoute('app_public_index');
}
#[Route('/admin/send_email_to_place/{id}', name: 'app_admin_send_email_to_place')]
public function send_email_to_place(Place $place, \Symfony\Component\Mailer\MailerInterface $mailer): Response
{
$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.');
$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);
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();
$place->setLastContactAttemptDate(new \DateTime());
$this->addFlash('success', 'Email envoyé avec succès à ' . $place->getName() . ' le ' . $place->getLastContactAttemptDate()->format('d/m/Y H:i:s'));
return $this->redirectToRoute('app_public_index');
}
#[Route('/admin/fraicheur/histogramme', name: 'admin_fraicheur_histogramme')]
public function showFraicheurHistogramme(): Response
{
$jsonPath = $this->getParameter('kernel.project_dir') . '/var/fraicheur_osm.json';
if (!file_exists($jsonPath)) {
// Générer le fichier si absent
$this->calculateFraicheur();
}
return $this->render('admin/fraicheur_histogramme.html.twig');
}
#[Route('/admin/fraicheur/calculate', name: 'admin_fraicheur_calculate')]
public function calculateFraicheur(): Response
{
// Ajout d'un log d'action avec le service ActionLogger
$this->actionLogger->log('fraicheur/calculate', []);
$filesystem = new Filesystem();
$jsonPath = $this->getParameter('kernel.project_dir') . '/var/fraicheur_osm.json';
$now = new \DateTime();
// Si le fichier existe et a moins de 12h, on ne régénère pas
if ($filesystem->exists($jsonPath)) {
$fileMTime = filemtime($jsonPath);
if ($fileMTime && ($now->getTimestamp() - $fileMTime) < 43200) { // 12h = 43200s
return $this->redirectToRoute('admin_fraicheur_histogramme');
}
}
$places = $this->entityManager->getRepository(Place::class)->findAll();
$histogram = [];
$total = 0;
foreach ($places as $place) {
$date = $place->getOsmDataDate();
if ($date) {
$key = $date->format('Y-m');
if (!isset($histogram[$key])) {
$histogram[$key] = 0;
}
$histogram[$key]++;
$total++;
}
}
ksort($histogram);
$data = [
'generated_at' => $now->format('c'),
'total' => $total,
'histogram' => $histogram
];
$filesystem->dumpFile($jsonPath, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
// --- Distribution villes selon lieux/habitants ---
$distJsonPath = $this->getParameter('kernel.project_dir') . '/var/distribution_villes_lieux_par_habitant.json';
// Toujours régénérer
$statsRepo = $this->entityManager->getRepository(Stats::class);
$allStats = $statsRepo->findAll();
$histogram_lieux_par_habitant = [];
$histogram_habitants_par_lieu = [];
$totalVilles = 0;
foreach ($allStats as $stat) {
$places = $stat->getPlacesCount();
$population = $stat->getPopulation();
if ($places && $population && $population > 0) {
// lieux par habitant (pas de 0.01)
$ratio_lph = round($places / $population, 4);
$bin_lph = round(floor($ratio_lph / 0.01) * 0.01, 2);
if (!isset($histogram_lieux_par_habitant[$bin_lph])) $histogram_lieux_par_habitant[$bin_lph] = 0;
$histogram_lieux_par_habitant[$bin_lph]++;
// habitants par lieu (pas de 10)
$ratio_hpl = ceil($population / $places);
$bin_hpl = ceil($ratio_hpl / 10) * 10;
if (!isset($histogram_habitants_par_lieu[$bin_hpl])) $histogram_habitants_par_lieu[$bin_hpl] = 0;
$histogram_habitants_par_lieu[$bin_hpl]++;
$totalVilles++;
}
}
ksort($histogram_lieux_par_habitant);
ksort($histogram_habitants_par_lieu);
$distData = [
'generated_at' => $now->format('c'),
'total_villes' => $totalVilles,
'histogram_001' => $histogram_lieux_par_habitant,
'histogram_10' => $histogram_habitants_par_lieu
];
$filesystem->dumpFile($distJsonPath, json_encode($distData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
return $this->redirectToRoute('admin_fraicheur_histogramme');
}
#[Route('/admin/fraicheur/download', name: 'admin_fraicheur_download')]
public function downloadFraicheur(): JsonResponse
{
$jsonPath = $this->getParameter('kernel.project_dir') . '/var/fraicheur_osm.json';
if (!file_exists($jsonPath)) {
return new JsonResponse(['error' => 'Fichier non généré'], 404);
}
$content = file_get_contents($jsonPath);
$data = json_decode($content, true);
return new JsonResponse($data);
}
#[Route('/admin/distribution_villes_lieux_par_habitant_download', name: 'admin_distribution_villes_lieux_par_habitant_download')]
public function downloadDistributionVillesLieuxParHabitant(): JsonResponse
{
$jsonPath = $this->getParameter('kernel.project_dir') . '/var/distribution_villes_lieux_par_habitant.json';
if (!file_exists($jsonPath)) {
// Générer à la volée si absent
$now = new \DateTime();
$filesystem = new \Symfony\Component\Filesystem\Filesystem();
$statsRepo = $this->entityManager->getRepository(\App\Entity\Stats::class);
$allStats = $statsRepo->findAll();
$distribution = [];
$histogram = [];
$totalVilles = 0;
foreach ($allStats as $stat) {
$places = $stat->getPlacesCount();
$population = $stat->getPopulation();
if ($places && $population && $population > 0) {
$ratio = round($places / $population, 4); // lieux par habitant
$bin = round(floor($ratio / 0.01) * 0.01, 2); // pas de 0.01
if (!isset($histogram[$bin])) $histogram[$bin] = 0;
$histogram[$bin]++;
$totalVilles++;
}
}
ksort($histogram);
$distData = [
'generated_at' => $now->format('c'),
'total_villes' => $totalVilles,
'histogram_001' => $histogram
];
$filesystem->dumpFile($jsonPath, json_encode($distData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
$content = file_get_contents($jsonPath);
$data = json_decode($content, true);
return new JsonResponse($data);
}
#[Route('/admin/distribution_villes_lieux_par_habitant_villes', name: 'admin_distribution_villes_lieux_par_habitant_villes')]
public function downloadDistributionVillesLieuxParHabitantVilles(): JsonResponse
{
$statsRepo = $this->entityManager->getRepository(\App\Entity\Stats::class);
$allStats = $statsRepo->findAll();
$villesByBin = [];
foreach ($allStats as $stat) {
$places = $stat->getPlacesCount();
$population = $stat->getPopulation();
$name = $stat->getName();
if ($places && $population && $population > 0 && $name) {
$ratio = round($places / $population, 4); // lieux par habitant
$bin = round(floor($ratio / 0.01) * 0.01, 2); // pas de 0.01
if (!isset($villesByBin[$bin])) $villesByBin[$bin] = [];
$villesByBin[$bin][] = $name;
}
}
ksort($villesByBin);
return new JsonResponse(['villes_by_bin' => $villesByBin]);
}
#[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();
}
$this->addFlash('success', $budgetsMisAJour . ' budgets mis à jour.');
return $this->redirectToRoute('app_admin');
}
#[Route('/admin/podium-contributeurs-osm', name: 'app_admin_podium_contributeurs_osm')]
public function podiumContributeursOsm(): Response
{
$this->actionLogger->log('admin/podium_contributeurs_osm', []);
// 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);
// Compter les contributions par utilisateur
$contributions = [];
foreach ($places as $place) {
$user = $place->getOsmUser();
if ($user) {
if (!isset($contributions[$user])) {
$contributions[$user] = 0;
}
$contributions[$user]++;
}
}
// Trier par nombre de contributions décroissant
arsort($contributions);
// Prendre les 10 premiers
$topContributors = array_slice($contributions, 0, 10, true);
return $this->render('admin/podium_contributeurs_osm.html.twig', [
'contributors' => $topContributors
]);
}
#[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');
if (!$uploadedFile) {
$this->addFlash('error', 'Aucun fichier JSON n\'a été fourni.');
return $this->redirectToRoute('app_admin_import_stats');
}
// Vérifier le type de fichier
if (
$uploadedFile->getClientMimeType() !== 'application/json' &&
$uploadedFile->getClientOriginalExtension() !== 'json'
) {
$this->addFlash('error', 'Le fichier doit être au format JSON.');
return $this->redirectToRoute('app_admin_import_stats');
}
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]);
if ($existingStats) {
$skippedCount++;
continue; // Ignorer les objets existants
}
// Créer un nouvel objet Stats
$stats = new Stats();
$stats->setZone($zone)
->setName($name)
->setDateCreated(new \DateTime())
->setDateModified(new \DateTime());
// 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()]);
}
return $this->redirectToRoute('app_admin_import_stats');
}
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]);
if (!$stats) {
throw $this->createNotFoundException('Stats non trouvées pour ce code INSEE');
}
// 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]);
if (!$stats) {
throw $this->createNotFoundException('Stats non trouvées pour ce code INSEE');
}
$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 : '';
// Construire l'adresse complète
$address = '';
if ($place->getHousenumber() && $place->getStreet()) {
$address = $place->getHousenumber() . ' ' . $place->getStreet();
} elseif ($place->getStreet()) {
$address = $place->getStreet();
}
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;
}
#[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 {
$error = "4 Aucune stats trouvée pour ce code INSEE.";
}
}
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
]);
}
#[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
{
// 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
return match ($mainTag) {
'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' => '⚡',
default => '🏷️',
};
}
public function followupEmbedGraph(Request $request, string $insee_code, string $theme): Response
{
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
if (!$stats) {
$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;
$main_tag = $place->getMainTag();
// Cas particuliers multi-valeurs (ex: healthcare)
if ($theme === 'healthcare') {
if ($main_tag && (
str_starts_with($main_tag, 'healthcare=') ||
in_array($main_tag, [
'amenity=doctors',
'amenity=pharmacy',
'amenity=hospital',
'amenity=clinic',
'amenity=social_facility'
])
)) {
$match = true;
}
} 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;
}
}
// 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
{
$ctc_completion_series = [];
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
if (!$stats) {
$this->addFlash('error', '6 Aucune stats trouvée pour ce code INSEE.');
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();
// 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) {
usort($points, function($a, $b) {
return strcmp($a['date'], $b['date']);
});
}
unset($points);
return $this->render('admin/followup_graph.html.twig', [
'stats' => $stats,
'completion_tags' => \App\Service\FollowUpService::getFollowUpCompletionTags(),
'followup_labels' => \App\Service\FollowUpService::getFollowUpThemes(),
'followup_icons' => \App\Service\FollowUpService::getFollowUpIcons(),
'ctc_completion_series' => $ctc_completion_series,
]);
}
// 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) {
$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])) {
$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,
]);
}
#[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) {
$this->addFlash('error', '8 Aucune stats trouvée pour ce code INSEE. Veuillez d\'abord ajouter la ville.');
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,
]);
}
#[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());
$demande->setOsmId((int)$place->getOsmId());
$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');
}
}