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 Symfony\Bundle\FrameworkBundle\Controller\AbstractController ;
use Symfony\Component\HttpFoundation\Response ;
use Symfony\Component\Routing\Attribute\Route ;
2025-05-26 12:57:10 +02:00
use App\Entity\Place ;
2025-05-26 23:51:46 +02:00
use App\Entity\Stats ;
2025-06-17 19:38:44 +02:00
use App\Entity\StatsHistory ;
2025-06-27 00:35:11 +02:00
use App\Service\Motocultrice ;
2025-06-24 12:30:39 +02:00
use App\Service\BudgetService ;
2025-05-26 12:57:10 +02:00
use Doctrine\ORM\EntityManagerInterface ;
2025-06-21 11:28:31 +02:00
use Symfony\Component\HttpFoundation\Request ;
2025-05-26 12:57:10 +02:00
use function uuid_create ;
2025-06-24 00:29:15 +02:00
use Symfony\Component\Filesystem\Filesystem ;
use Symfony\Component\HttpFoundation\JsonResponse ;
2025-06-24 13:16:48 +02:00
use Twig\Environment ;
2025-06-26 23:14:22 +02:00
use App\Service\ActionLogger ;
2025-06-26 23:40:37 +02:00
use DateTime ;
2025-06-29 19:24:00 +02:00
use App\Service\FollowUpService ;
2025-07-15 21:22:02 +02:00
use phpDocumentor\Reflection\DocBlock\Tags\Var_ ;
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
2025-05-26 12:57:10 +02:00
public function __construct (
private EntityManagerInterface $entityManager ,
2025-06-24 12:30:39 +02:00
private Motocultrice $motocultrice ,
2025-06-24 13:16:48 +02:00
private BudgetService $budgetService ,
2025-06-26 23:14:22 +02:00
private Environment $twig ,
2025-06-29 19:24:00 +02:00
private ActionLogger $actionLogger ,
FollowUpService $followUpService
) {
$this -> followUpService = $followUpService ;
}
2025-05-26 12:57:10 +02:00
2025-06-19 01:04:44 +02:00
#[Route('/admin/labourer-toutes-les-zones', name: 'app_admin_labourer_tout')]
public function labourer_tout () : Response
{
2025-06-26 23:40:37 +02:00
2025-06-27 00:35:11 +02:00
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-19 01:04:44 +02:00
2025-06-27 00:35:11 +02:00
$stats_all = $this -> entityManager -> getRepository ( Stats :: class ) -> findAll ();
2025-06-19 01:04:44 +02:00
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 () . ' ' ;
2025-06-19 01:04:44 +02:00
$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>' ;
2025-06-19 01:04:44 +02:00
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 ;
}
2025-06-19 01:04:44 +02:00
$places_overpass = $this -> motocultrice -> labourer ( $stats -> getZone ());
$places = $places_overpass ;
foreach ( $places as $placeData ) {
2025-06-27 00:35:11 +02:00
2025-06-19 01:04:44 +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
2025-06-19 01:04:44 +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 )
-> setPlaceCount ( 0 )
2025-06-19 10:20:40 +02:00
// ->setOsmData($placeData['modified'] ?? null)
2025-06-27 00:35:11 +02:00
;
2025-06-19 01:04:44 +02:00
// Mettre à jour les données depuis Overpass
$place -> update_place_from_overpass_data ( $placeData );
2025-06-27 00:35:11 +02:00
2025-06-19 01:04:44 +02:00
$this -> entityManager -> persist ( $place );
$stats -> addPlace ( $place );
$processedCount ++ ;
2025-06-24 13:16:48 +02:00
// 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 );
2025-06-19 01:04:44 +02:00
} 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 );
2025-06-19 01:04:44 +02:00
$this -> entityManager -> persist ( $existingPlace );
$updatedCount ++ ;
}
2025-06-27 00:35:11 +02:00
}
2025-06-19 01:04:44 +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 {
2025-06-19 01:04:44 +02:00
$stats = new Stats ();
2025-07-15 21:22:02 +02:00
dump ( 'nouvelle stat' , $insee_code );
die ();
2025-06-19 01:04:44 +02:00
$stats -> setZone ( $insee_code );
}
2025-06-27 00:35:11 +02:00
$urls = $stats -> getAllCTCUrlsMap ();
2025-06-19 01:04:44 +02:00
$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
2025-06-19 01:04:44 +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
2025-06-19 01:04:44 +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 ;
2025-06-19 01:04:44 +02:00
$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
2025-06-19 01:04:44 +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
2025-07-14 18:55:53 +02:00
$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
}
2025-07-14 18:55:53 +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
}
}
2025-07-14 18:55:53 +02:00
return $this -> redirectToRoute ( 'app_admin_stats' , [ 'insee_code' => $insee_code ]);
2025-06-19 01:04:44 +02:00
}
2025-06-27 00:35:11 +02:00
2025-06-19 01:04:44 +02:00
$this -> entityManager -> flush ();
2025-06-27 00:35:11 +02:00
2025-06-19 01:04:44 +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-05-26 12:57:10 +02:00
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-05-26 23:51:46 +02:00
{
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.' );
2025-07-14 19:18:34 +02:00
return $this -> redirectToRoute ( 'app_admin_import_stats' );
2025-06-26 18:20:43 +02:00
}
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-06-21 12:36:09 +02:00
$commerces = $stats -> getPlaces ();
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
2025-06-19 01:04:44 +02:00
$stats = new Stats ();
$stats -> setZone ( $insee_code );
2025-06-21 11:28:31 +02:00
$stats -> setName ( 'Nouvelle zone non labourée' );
2025-06-19 01:04:44 +02:00
}
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-06-21 12:36:09 +02:00
foreach ( $commerces as $commerce ) {
2025-06-21 11:28:31 +02:00
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 ] ++ ;
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
2025-06-21 12:36:09 +02:00
$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 ()
]
];
}
}
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) +'
. ' (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 );
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 );
});
2025-06-29 16:41:18 +02:00
// 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 ());
2025-06-29 16:41:18 +02:00
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 ()) {
2025-06-29 16:41:18 +02:00
$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 ()) {
2025-06-29 16:41:18 +02:00
$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 ()) {
2025-06-29 16:41:18 +02:00
$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 ()) {
2025-06-29 16:41:18 +02:00
$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 ) {
usort ( $points , function ( $a , $b ) {
return strcmp ( $a [ 'date' ], $b [ 'date' ]);
});
}
unset ( $points );
2025-07-05 10:29:53 +02:00
2025-05-26 23:51:46 +02:00
return $this -> render ( 'admin/stats.html.twig' , [
'stats' => $stats ,
2025-06-21 11:28:31 +02:00
'commerces' => $commerces ,
'urls' => $urls ,
2025-06-21 12:36:09 +02:00
'geojson' => json_encode ( $geojson ),
2025-06-21 11:28:31 +02:00
'modificationsByQuarter' => json_encode ( $modificationsByQuarter ),
2025-06-01 18:56:01 +02:00
'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' => '' ,
2025-06-29 16:41:18 +02:00
'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-07-05 16:15:56 +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' ],
2025-07-05 17:21:18 +02:00
'completion_tags' => \App\Service\FollowUpService :: getFollowUpCompletionTags (),
2025-07-14 18:17:41 +02:00
'ctc_completion_series' => $ctc_completion_series ,
2025-05-26 23:51:46 +02:00
]);
}
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 ) {
2025-07-15 21:22:02 +02:00
$this -> addFlash ( 'error' , '2 Aucune stats trouvée pour ce code INSEE.' );
2025-07-05 12:37:01 +02:00
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 = [];
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]; \n area[ \" 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 && (
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 " ) {
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 (),
2025-07-05 16:34:37 +02:00
'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
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 (),
2025-07-14 18:55:53 +02:00
'followup_labels' => $themes ,
2025-07-05 15:25:33 +02:00
'geojson' => json_encode ( $geojson ),
2025-07-14 18:55:53 +02:00
'overpass_query' => $overpass_query ,
2025-07-05 15:25:33 +02:00
'josm_url' => $josm_url ,
'center' => $center ,
'maptiler_token' => $_ENV [ 'MAPTILER_TOKEN' ] ? ? null ,
2025-07-05 17:21:18 +02:00
'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' );
}
}
2025-06-04 00:16:56 +02:00
/**
* 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
}
2025-06-27 00:36:55 +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-04 00:16:56 +02:00
/**
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-04 00:16:56 +02:00
*/
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-05-26 12:57:10 +02:00
{
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-19 10:37:29 +02:00
2025-06-26 23:40:37 +02:00
$this -> actionLogger -> log ( 'labourer' , [ 'insee_code' => $insee_code ]);
2025-06-19 10:37:29 +02:00
// 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 ]);
2025-06-19 10:37:29 +02:00
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 );
// $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' ]);
}
}
} 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 ) {}
}
2025-07-14 18:17:41 +02:00
// Mettre à jour la date de requête de labourage
$stats -> setDateLabourageRequested ( new \DateTime ());
$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-05-26 12:57:10 +02:00
}
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-07-15 23:23:32 +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 ]);
2025-05-26 12:57:10 +02:00
}
2025-05-26 23:51:46 +02:00
#[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 ]);
2025-05-26 23:51:46 +02:00
$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-05-26 23:51:46 +02:00
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-26 23:51:46 +02:00
2025-05-28 16:24:34 +02:00
return $this -> redirectToRoute ( 'app_public_dashboard' );
2025-05-26 23:51:46 +02:00
}
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-05-26 23:51:46 +02:00
{
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-05-26 23:51:46 +02:00
}
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-26 23:51:46 +02:00
2025-05-28 16:24:34 +02:00
return $this -> redirectToRoute ( 'app_public_dashboard' );
}
2025-05-26 23:51:46 +02:00
#[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' , []);
2025-05-26 23:51:46 +02:00
$places = $this -> entityManager -> getRepository ( Place :: class ) -> findAll ();
2025-06-27 00:35:11 +02:00
2025-05-26 23:51:46 +02:00
$csvData = [];
$csvData [] = [
'Nom' ,
2025-06-27 00:35:11 +02:00
'Email' ,
2025-05-26 23:51:46 +02:00
'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' ,
2025-05-26 23:51:46 +02:00
'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' ,
2025-05-26 23:51:46 +02:00
$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
2025-06-26 23:40:37 +02:00
2025-05-26 23:51:46 +02:00
return $response ;
}
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 ;
}
2025-06-19 10:20:40 +02:00
#[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
2025-06-19 10:20:40 +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
2025-06-19 10:20:40 +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.' );
2025-06-19 10:20:40 +02:00
return $this -> redirectToRoute ( 'app_public_index' );
}
2025-06-19 10:37:29 +02:00
#[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 ()]);
2025-06-19 10:37:29 +02:00
// 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 ()]);
2025-06-19 10:37:29 +02:00
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' );
}
2025-06-19 10:37:29 +02:00
// 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
2025-06-19 10:37:29 +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 ]);
}
2025-06-24 12:30:39 +02:00
#[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.' );
2025-06-24 12:30:39 +02:00
return $this -> redirectToRoute ( 'app_admin' );
}
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' , [
'contributors' => $topContributors
]);
}
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 ());
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-06-26 19:18:29 +02:00
}
2025-07-05 10:59:37 +02:00
return $this -> redirectToRoute ( 'app_admin_import_stats' );
2025-06-26 19:18:29 +02:00
}
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-06-26 19:18:29 +02:00
}
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 => '🏷️' ,
};
}
2025-07-05 17:21:18 +02:00
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.' );
2025-07-05 17:21:18 +02:00
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]; \n area[ \" 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)
2025-07-05 17:21:18 +02:00
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 ;
}
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 17:21:18 +02:00
$match = true ;
}
}
2025-07-05 17:35:20 +02:00
// Ajouter l'objet si match
2025-07-05 17:21:18 +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 (),
'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 (),
]);
}
2025-07-12 12:53:06 +02:00
#[Route('/admin/followup-graph/{insee_code}', name: 'admin_followup_graph', requirements: ['insee_code' => '\d+'])]
2025-07-05 17:21:18 +02:00
public function followupGraph ( Request $request , string $insee_code ) : Response
{
2025-07-14 18:17:41 +02:00
$ctc_completion_series = [];
2025-07-05 17:21:18 +02:00
$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 ,
]);
2025-07-05 17:21:18 +02:00
}
$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 ) {
usort ( $points , function ( $a , $b ) {
return strcmp ( $a [ 'date' ], $b [ 'date' ]);
});
}
unset ( $points );
2025-07-05 17:21:18 +02:00
return $this -> render ( 'admin/followup_graph.html.twig' , [
'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-05 17:21:18 +02:00
]);
}
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' );
}
2025-07-12 12:53:06 +02:00
#[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.' );
2025-07-12 12:53:06 +02:00
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 ,
]);
}
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 ());
$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' );
}
2025-05-26 11:55:44 +02:00
}