diff --git a/assets/styles/app.css b/assets/styles/app.css index d66ce9d..fc7f949 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -20,7 +20,7 @@ body { } .filled { - background-color: #b0dfa0 !important; + background-color: rgba(0, 255, 0, 0.2) !important; } .filled:hover { diff --git a/migrations/Version20250619074501.php b/migrations/Version20250619074501.php new file mode 100644 index 0000000..2dffe19 --- /dev/null +++ b/migrations/Version20250619074501.php @@ -0,0 +1,35 @@ +addSql(<<<'SQL' + ALTER TABLE place ADD osm_data_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE place DROP osm_data_date + SQL); + } +} diff --git a/migrations/Version20250619074657.php b/migrations/Version20250619074657.php new file mode 100644 index 0000000..59d6d8a --- /dev/null +++ b/migrations/Version20250619074657.php @@ -0,0 +1,53 @@ +addSql(<<<'SQL' + ALTER TABLE place ADD osm_data_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE stats ADD osm_data_date_min TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE stats ADD osm_data_date_avg TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE stats ADD osm_data_date_max TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + ALTER TABLE place DROP osm_data_date + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE stats DROP osm_data_date_min + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE stats DROP osm_data_date_avg + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE stats DROP osm_data_date_max + SQL); + } +} diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php index c554415..39427b1 100644 --- a/src/Controller/AdminController.php +++ b/src/Controller/AdminController.php @@ -82,8 +82,11 @@ final class AdminController extends AbstractController ->setSiret($this->motocultrice->find_siret($placeData['tags']) ?? '') ->setAskedHumainsSupport(false) ->setLastContactAttemptDate(null) - ->setNote('') - ->setPlaceCount(0); + ->setNote($this->motocultrice->find_tag($placeData['tags'], 'note') ? true : false) + ->setNoteContent($this->motocultrice->find_tag($placeData['tags'], 'note') ?? '') + ->setPlaceCount(0) + // ->setOsmData($placeData['modified'] ?? null) + ; // Mettre à jour les données depuis Overpass $place->update_place_from_overpass_data($placeData); @@ -139,8 +142,74 @@ final class AdminController extends AbstractController } $stats->computeCompletionPercent(); - $this->entityManager->persist($stats); + + // Calculer les statistiques de fraîcheur des données OSM + $timestamps = []; + foreach ($stats->getPlaces() as $place) { + if ($place->getOsmDataDate()) { + $timestamps[] = $place->getOsmDataDate()->getTimestamp(); + } + } + if (!empty($timestamps)) { + // Date la plus ancienne (min) + $minTimestamp = min($timestamps); + $stats->setOsmDataDateMin(new \DateTime('@' . $minTimestamp)); + + // Date la plus récente (max) + $maxTimestamp = max($timestamps); + $stats->setOsmDataDateMax(new \DateTime('@' . $maxTimestamp)); + + // Date moyenne + $avgTimestamp = array_sum($timestamps) / count($timestamps); + $stats->setOsmDataDateAvg(new \DateTime('@' . (int)$avgTimestamp)); + } + + if($stats->getDateCreated() == null) { + $stats->setDateCreated(new \DateTime()); + } + + $stats->setDateModified(new \DateTime()); + + // Créer un historique des statistiques + $statsHistory = new StatsHistory(); + $statsHistory->setDate(new \DateTime()) + ->setStats($stats); + + // Compter les Places avec email et SIRET + $placesWithEmail = 0; + $placesWithSiret = 0; + foreach ($stats->getPlaces() as $place) { + if ($place->getEmail() && $place->getEmail() !== '') { + $placesWithEmail++; + } + if ($place->getSiret() && $place->getSiret() !== '') { + $placesWithSiret++; + } + } + + $statsHistory->setPlacesCount($stats->getPlaces()->count()) + ->setOpeningHoursCount($stats->getAvecHoraires()) + ->setAddressCount($stats->getAvecAdresse()) + ->setWebsiteCount($stats->getAvecSite()) + ->setSiretCount($placesWithSiret) + ->setEmailsCount($placesWithEmail) + // ->setAccessibiliteCount($stats->getAvecAccessibilite()) + // ->setNoteCount($stats->getAvecNote()) + ->setCompletionPercent($stats->getCompletionPercent()) + ->setStats($stats); + + $this->entityManager->persist($statsHistory); + + + $this->entityManager->persist($stats); + $this->entityManager->flush(); + + $message = 'Labourage terminé avec succès. ' . $processedCount . ' nouveaux lieux traités.'; + if ($updateExisting) { + $message .= ' ' . $updatedCount . ' lieux existants mis à jour pour la zone '.$stats->getName().' ('.$stats->getZone().').'; + } + $this->addFlash('success', $message); } $this->entityManager->flush(); @@ -175,15 +244,6 @@ final class AdminController extends AbstractController $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); @@ -206,9 +266,77 @@ final class AdminController extends AbstractController $this->entityManager->flush(); $stats->computeCompletionPercent(); + + // Calculer les statistiques de fraîcheur des données OSM + $timestamps = []; + foreach ($stats->getPlaces() as $place) { + if ($place->getOsmDataDate()) { + $timestamps[] = $place->getOsmDataDate()->getTimestamp(); + } + } + + if (!empty($timestamps)) { + // Date la plus ancienne (min) + $minTimestamp = min($timestamps); + $stats->setOsmDataDateMin(new \DateTime('@' . $minTimestamp)); + + // Date la plus récente (max) + $maxTimestamp = max($timestamps); + $stats->setOsmDataDateMax(new \DateTime('@' . $maxTimestamp)); + + // Date moyenne + $avgTimestamp = array_sum($timestamps) / count($timestamps); + $stats->setOsmDataDateAvg(new \DateTime('@' . (int)$avgTimestamp)); + } + + if($stats->getDateCreated() == null) { + $stats->setDateCreated(new \DateTime()); + } + + $stats->setDateModified(new \DateTime()); + + // Créer un historique des statistiques + $statsHistory = new StatsHistory(); + $statsHistory->setDate(new \DateTime()) + ->setStats($stats); + + // Compter les Places avec email et SIRET + $placesWithEmail = 0; + $placesWithSiret = 0; + foreach ($stats->getPlaces() as $place) { + if ($place->getEmail() && $place->getEmail() !== '') { + $placesWithEmail++; + } + if ($place->getSiret() && $place->getSiret() !== '') { + $placesWithSiret++; + } + } + + $statsHistory->setPlacesCount($stats->getPlaces()->count()) + ->setOpeningHoursCount($stats->getAvecHoraires()) + ->setAddressCount($stats->getAvecAdresse()) + ->setWebsiteCount($stats->getAvecSite()) + ->setSiretCount($placesWithSiret) + ->setEmailsCount($placesWithEmail) + // ->setAccessibiliteCount($stats->getAvecAccessibilite()) + // ->setNoteCount($stats->getAvecNote()) + ->setCompletionPercent($stats->getCompletionPercent()) + ->setStats($stats); + + $this->entityManager->persist($statsHistory); + + $this->entityManager->persist($stats); $this->entityManager->flush(); - + + $statsHistory = $this->entityManager->getRepository(StatsHistory::class) + ->createQueryBuilder('sh') + ->where('sh.stats = :stats') + ->setParameter('stats', $stats) + ->orderBy('sh.id', 'DESC') + ->setMaxResults(365) + ->getQuery() + ->getResult(); return $this->render('admin/stats.html.twig', [ 'stats' => $stats, @@ -269,6 +397,12 @@ final class AdminController extends AbstractController #[Route('/admin/labourer/{insee_code}', name: 'app_admin_labourer')] public function labourer(string $insee_code, bool $updateExisting = true): Response { + + // Vérifier si le code INSEE est valide (composé uniquement de chiffres) + if (!ctype_digit($insee_code) || $insee_code == 'undefined' || $insee_code == '') { + $this->addFlash('error', 'Code INSEE invalide : il doit être composé uniquement de chiffres.'); + return $this->redirectToRoute('app_public_index'); + } try { // Récupérer ou créer les stats pour cette zone $stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]); @@ -366,6 +500,28 @@ final class AdminController extends AbstractController // Mettre à jour les statistiques finales $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()); } @@ -377,11 +533,24 @@ final class AdminController extends AbstractController $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($stats->getAvecSiret()) + ->setSiretCount($placesWithSiret) + ->setEmailsCount($placesWithEmail) // ->setAccessibiliteCount($stats->getAvecAccessibilite()) // ->setNoteCount($stats->getAvecNote()) ->setCompletionPercent($stats->getCompletionPercent()) @@ -514,4 +683,62 @@ final class AdminController extends AbstractController return $response; } + + #[Route('/admin/make_email_for_place/{id}', name: 'app_admin_make_email_for_place')] + public function make_email_for_place(Place $place): Response + { + + return $this->render('admin/view_email_for_place.html.twig', ['place' => $place]); + } + + #[Route('/admin/no_more_sollicitation_for_place/{id}', name: 'app_admin_no_more_sollicitation_for_place')] + public function no_more_sollicitation_for_place(Place $place): Response + { + $place->setOptedOut(true); + $this->entityManager->persist($place); + $this->entityManager->flush(); + + $this->addFlash('success', 'Votre lieu '.$place->getName().' ne sera plus sollicité pour mettre à jour ses informations.'); + + return $this->redirectToRoute('app_public_index'); + } + + #[Route('/admin/send_email_to_place/{id}', name: 'app_admin_send_email_to_place')] + public function send_email_to_place(Place $place, \Symfony\Component\Mailer\MailerInterface $mailer): Response + { + + // Vérifier si le lieu est opted out + if ($place->isOptedOut()) { + $this->addFlash('error', 'Ce lieu a demandé à ne plus être sollicité pour mettre à jour ses informations.'); + return $this->redirectToRoute('app_public_index'); + } + // Vérifier si le lieu a déjà été contacté + if ($place->getLastContactAttemptDate() !== null) { + $this->addFlash('error', 'Ce lieu a déjà été contacté le ' . $place->getLastContactAttemptDate()->format('d/m/Y H:i:s')); + return $this->redirectToRoute('app_public_index'); + } + + // Générer le contenu de l'email avec le template + $emailContent = $this->renderView('admin/email_content.html.twig', [ + 'place' => $place + ]); + + // Envoyer l'email + $email = (new \Symfony\Component\Mime\Email()) + ->from('contact@openstreetmap.fr') + ->to('contact+send_email@cipherbliss.com') + ->subject('Mise à jour des informations de votre établissement dans OpenStreetMap') + ->html($emailContent); + + $mailer->send($email); + + + // Mettre à jour la date de dernier contact + $place->setLastContactAttemptDate(new \DateTime()); + $this->entityManager->persist($place); + $this->entityManager->flush(); + + $this->addFlash('success', 'Email envoyé avec succès à ' . $place->getName() . ' le ' . $place->getLastContactAttemptDate()->format('d/m/Y H:i:s')); + return $this->redirectToRoute('app_public_index'); + } } diff --git a/src/Entity/Place.php b/src/Entity/Place.php index f7b1c25..0baebc1 100644 --- a/src/Entity/Place.php +++ b/src/Entity/Place.php @@ -103,6 +103,54 @@ class Place #[ORM\Column(nullable: true)] private ?int $habitants = null; + #[ORM\Column(nullable: true)] + private ?\DateTime $osm_data_date = null; + + #[ORM\Column(nullable: true)] + private ?int $osm_version = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $osm_user = null; + + #[ORM\Column(nullable: true)] + private ?int $osm_uid = null; + + #[ORM\Column(nullable: true)] + private ?int $osm_changeset = null; + + public function getPlaceTypeName(): ?string + { + if ($this->main_tag == 'amenity=restaurant') { + return 'restaurant'; + } + if ($this->main_tag == 'amenity=bar') { + return 'bar'; + } + if ($this->main_tag == 'amenity=cafe') { + return 'café'; + } + if ($this->main_tag == 'amenity=hotel') { + return 'hôtel'; + } + if ($this->main_tag == 'amenity=supermarket') { + return 'supermarché'; + } + if ($this->main_tag == 'amenity=pharmacy') { + return 'pharmacie'; + } + if ($this->main_tag == 'amenity=bank') { + return 'banque'; + } + if ($this->main_tag == 'amenity=post_office') { + return 'poste'; + } + if ($this->main_tag == 'amenity=school') { + return 'école'; + } + + return 'établissement'; + } + public function getMainTag(): ?string { return $this->main_tag; @@ -161,7 +209,7 @@ class Place /** * mettre à jour le lieu selon les tags osm */ - public function update_place_from_overpass_data(array $overpass_data) { + public function update_place_from_overpass_data(array $overpass_data) { if ( ! isset($overpass_data['tags']) || $overpass_data['tags'] == null) { return; @@ -179,7 +227,8 @@ class Place 'addr:street' => '', 'website' => '', 'wheelchair' => '', - 'note' => '' + 'note' => '', + 'fixme' => '', ], $overpass_data['tags'] ); @@ -190,19 +239,41 @@ class Place $this ->setOsmId( $orignal_overpass_data['id']) ->setOsmKind($orignal_overpass_data['type'] ) - ->setLat($orignal_overpass_data['lat']) - ->setLon($orignal_overpass_data['lon']) + ->setLat((float) $orignal_overpass_data['lat']) + ->setLon((float) $orignal_overpass_data['lon']) ->setName(isset($overpass_data['name']) && $overpass_data['name'] != '' ? $overpass_data['name'] : null); + // Traiter le timestamp OSM si disponible + if (isset($orignal_overpass_data['timestamp']) && $orignal_overpass_data['timestamp']) { + try { + $osmDate = new \DateTime($orignal_overpass_data['timestamp']); + $this->setOsmDataDate($osmDate); + } catch (\Exception $e) { + // En cas d'erreur de parsing de la date, on ignore + } + } + + // Traiter les autres métadonnées OSM + if (isset($orignal_overpass_data['version'])) { + $this->setOsmVersion($orignal_overpass_data['version']); + } + if (isset($orignal_overpass_data['user'])) { + $this->setOsmUser($orignal_overpass_data['user']); + } + if (isset($orignal_overpass_data['uid'])) { + $this->setOsmUid($orignal_overpass_data['uid']); + } + if (isset($orignal_overpass_data['changeset'])) { + $this->setOsmChangeset($orignal_overpass_data['changeset']); + } + $mapping = [ ['key' => 'postcode', 'setter' => 'setZipCode', 'source' => $overpass_data], ['key' => 'email', 'setter' => 'setEmail', 'source' => $overpass_data], ['key' => 'opening_hours', 'setter' => 'setHasOpeningHours', 'source' => $overpass_data['tags'] ?? []], - ['key' => 'note', 'setter' => 'setNote', 'source' => $overpass_data['tags'] ?? []], ['key' => 'addr:housenumber', 'setter' => 'setHasAddress', 'source' => $overpass_data['tags'] ?? []], ['key' => 'website', 'setter' => 'setHasWebsite', 'source' => $overpass_data['tags'] ?? []], ['key' => 'wheelchair', 'setter' => 'setHasWheelchair', 'source' => $overpass_data['tags'] ?? []], - ['key' => 'note', 'setter' => 'setHasNote', 'source' => $overpass_data['tags'] ?? []], ['key' => 'siret', 'setter' => 'setSiret', 'source' => $overpass_data['tags'] ?? []], ['key' => 'addr:street', 'setter' => 'setStreet', 'source' => $overpass_data['tags'] ?? []], ['key' => 'addr:housenumber', 'setter' => 'setHousenumber', 'source' => $overpass_data['tags'] ?? []], @@ -214,6 +285,26 @@ class Place } } + // Traiter les notes et fixme + $noteContent = ''; + $hasNote = false; + + if (isset($orignal_overpass_data['tags']['note']) && $orignal_overpass_data['tags']['note'] !== '') { + $noteContent .= $orignal_overpass_data['tags']['note']; + $hasNote = true; + } + + if (isset($orignal_overpass_data['tags']['fixme']) && $orignal_overpass_data['tags']['fixme'] !== '') { + if ($noteContent !== '') { + $noteContent .= "\n\n"; + } + $noteContent .= "FIXME: " . $orignal_overpass_data['tags']['fixme']; + $hasNote = true; + } + + $this->setNoteContent($noteContent); + $this->setHasNote($hasNote); + $this // ->setOsmId($overpass_data['id']) // ->setOsmKind($overpass_data['type']) @@ -226,8 +317,7 @@ class Place ->setHasOpeningHours($overpass_data['opening_hours'] ? true : false) ->setHasAddress($overpass_data['addr:housenumber'] && $overpass_data['addr:street'] ? true : false) ->setHasWebsite($overpass_data['website'] ? true : false) - ->setHasWheelchair($overpass_data['wheelchair'] and $overpass_data['wheelchair'] != '' ? true : false) - ->setHasNote($overpass_data['note'] and $overpass_data['note'] != '' ? true : false); + ->setHasWheelchair($overpass_data['wheelchair'] and $overpass_data['wheelchair'] != '' ? true : false); } public function __construct() @@ -523,24 +613,24 @@ class Place return $this; } - public function getLat(): ?int + public function getLat(): ?float { return $this->lat; } - public function setLat(?int $lat): static + public function setLat(?float $lat): static { $this->lat = $lat; return $this; } - public function getLon(): ?int + public function getLon(): ?float { return $this->lon; } - public function setLon(?int $lon): static + public function setLon(?float $lon): static { $this->lon = $lon; @@ -594,4 +684,64 @@ class Place return $this; } + + public function getOsmDataDate(): ?\DateTime + { + return $this->osm_data_date; + } + + public function setOsmDataDate(?\DateTime $osm_data_date): static + { + $this->osm_data_date = $osm_data_date; + + return $this; + } + + public function getOsmVersion(): ?int + { + return $this->osm_version; + } + + public function setOsmVersion(?int $osm_version): static + { + $this->osm_version = $osm_version; + + return $this; + } + + public function getOsmUser(): ?string + { + return $this->osm_user; + } + + public function setOsmUser(?string $osm_user): static + { + $this->osm_user = $osm_user; + + return $this; + } + + public function getOsmUid(): ?int + { + return $this->osm_uid; + } + + public function setOsmUid(?int $osm_uid): static + { + $this->osm_uid = $osm_uid; + + return $this; + } + + public function getOsmChangeset(): ?int + { + return $this->osm_changeset; + } + + public function setOsmChangeset(?int $osm_changeset): static + { + $this->osm_changeset = $osm_changeset; + + return $this; + } } diff --git a/src/Entity/Stats.php b/src/Entity/Stats.php index 2295028..cc59cf7 100644 --- a/src/Entity/Stats.php +++ b/src/Entity/Stats.php @@ -86,6 +86,15 @@ class Stats #[ORM\Column(nullable: true)] private ?int $avec_name = null; + #[ORM\Column(nullable: true)] + private ?\DateTime $osm_data_date_min = null; + + #[ORM\Column(nullable: true)] + private ?\DateTime $osm_data_date_avg = null; + + #[ORM\Column(nullable: true)] + private ?\DateTime $osm_data_date_max = null; + public function getCTCurlBase(): ?string { $base = 'https://complete-tes-commerces.fr/'; @@ -493,6 +502,42 @@ class Stats return $this; } + + public function getOsmDataDateMin(): ?\DateTime + { + return $this->osm_data_date_min; + } + + public function setOsmDataDateMin(?\DateTime $osm_data_date_min): static + { + $this->osm_data_date_min = $osm_data_date_min; + + return $this; + } + + public function getOsmDataDateAvg(): ?\DateTime + { + return $this->osm_data_date_avg; + } + + public function setOsmDataDateAvg(?\DateTime $osm_data_date_avg): static + { + $this->osm_data_date_avg = $osm_data_date_avg; + + return $this; + } + + public function getOsmDataDateMax(): ?\DateTime + { + return $this->osm_data_date_max; + } + + public function setOsmDataDateMax(?\DateTime $osm_data_date_max): static + { + $this->osm_data_date_max = $osm_data_date_max; + + return $this; + } } diff --git a/src/Service/Motocultrice.php b/src/Service/Motocultrice.php index c92fd42..b75f001 100644 --- a/src/Service/Motocultrice.php +++ b/src/Service/Motocultrice.php @@ -101,10 +101,10 @@ public function find_siret($tags) { public function get_export_query($zone) { return <<.searchArea; {$this->overpass_base_places} -out skel qt; +out meta; QUERY; } @@ -112,7 +112,7 @@ QUERY; return '[out:json][timeout:25]; area["ref:INSEE"="'.$zone.'"]->.searchArea; '.$this->overpass_base_places.' -out center tags;'; +out meta;'; } private $more_tags = ['image', 'ref:FR:SIRET']; @@ -180,7 +180,14 @@ out center tags;'; 'name' => $element['tags']['name'] ?? '', 'lat' => $element['lat'] ?? null, 'lon' => $element['lon'] ?? null, - 'tags' => $element['tags'] + 'tags' => $element['tags'], + // Métadonnées OSM + 'timestamp' => $element['timestamp'] ?? null, + 'version' => $element['version'] ?? null, + 'user' => $element['user'] ?? null, + 'uid' => $element['uid'] ?? null, + 'changeset' => $element['changeset'] ?? null, + 'modified' => $element['timestamp'] ?? null ]; } } @@ -216,6 +223,13 @@ out center tags;'; return null; } + public function find_tag($tags, $tag) { + if(isset($tags[$tag]) && $tags[$tag] != '') { + return $tags[$tag]; + } + return null; + } + public function get_city_osm_from_zip_code($zip_code) { // Requête Overpass pour obtenir la zone administrative de niveau 8 avec un nom $query = "[out:json][timeout:25]; diff --git a/templates/admin/email_content.html.twig b/templates/admin/email_content.html.twig new file mode 100644 index 0000000..069507e --- /dev/null +++ b/templates/admin/email_content.html.twig @@ -0,0 +1,32 @@ +
+ +

Bonjour, votre {{place.getPlaceTypeName()}} "{{place.name }}" est présent dans la base de données mondiale OpenStreetMap (OSM) avec 650 000 autres en France. +
+Ces informations sont utilisées dans des milliers de sites web et annuaires, par Île de France mobilités, TomTom, Geovelo, Cartes IGN, Facebook, Instagram, Apple Plans et bien d'autres. + +
+ Plus les informations seront à jour et plus vous aurez de chances d'avoir des clients satisfaits.

+ +

Vous pouvez le modifier en cliquant sur le bouton ci-dessous, c'est gratuit et sans engagement.

+ + + + Compléter les informations de mon commerce + + +
+
+ Les bénévoles de l'association OpenStreetMap France ont mis en place cet outil pour faciliter la mise à jour des informations de vos commerces et améliorer la souveraineté numérique. Si vous avez besoin d'aide, n'hésitez pas à nous contacter à l'adresse contact@openstreetmap.fr. +
+
+ Pour des besoins de prestation de services concernant l'intégration ou l'exportation de données depuis OSM, vous pouvez contacter la fédération des pros d'OpenStreetMap France sur https://fposm.fr. +
+
+En vous souhaitant une bonne journée. +
+- Les bénévoles de l'association OpenStreetMap France. +
+
+ + Ne plus être sollicité pour mettre à jour mon commerce +
\ No newline at end of file diff --git a/templates/admin/stats.html.twig b/templates/admin/stats.html.twig index e1c1abe..7418afc 100644 --- a/templates/admin/stats.html.twig +++ b/templates/admin/stats.html.twig @@ -19,6 +19,28 @@ .completion-info { margin-bottom: 2rem; } + .osm-modification-info { + font-size: 0.85rem; + line-height: 1.3; + } + .osm-modification-info .text-muted { + font-size: 0.75rem; + } + .osm-modification-info a { + text-decoration: none; + color: #0d6efd; + } + .osm-modification-info a:hover { + text-decoration: underline; + } + .osm-freshness-info { + font-size: 0.95rem; + line-height: 1.4; + } + .osm-freshness-info .alert { + border-left: 4px solid #0dcaf0; + background-color: #f8f9fa; + } {% endblock %} @@ -59,6 +81,74 @@ {% endif %} + + {# Affichage de la fraîcheur des données OSM #} + {% if stats.osmDataDateMin and stats.osmDataDateMax and stats.osmDataDateAvg %} + {% set now = "now"|date("U") %} + {% set minDate = stats.osmDataDateMin|date("U") %} + {% set maxDate = stats.osmDataDateMax|date("U") %} + {% set avgDate = stats.osmDataDateAvg|date("U") %} + + {% set minDiff = now - minDate %} + {% set maxDiff = now - maxDate %} + {% set avgDiff = now - avgDate %} + + {% set minYears = (minDiff / 31536000)|round(0, 'floor') %} + {% set minMonths = ((minDiff % 31536000) / 2592000)|round(0, 'floor') %} + {% set maxYears = (maxDiff / 31536000)|round(0, 'floor') %} + {% set maxMonths = ((maxDiff % 31536000) / 2592000)|round(0, 'floor') %} + {% set avgYears = (avgDiff / 31536000)|round(0, 'floor') %} + {% set avgMonths = ((avgDiff % 31536000) / 2592000)|round(0, 'floor') %} + +
+
+
+ + Fraîcheur des données OSM : + {% if minYears == maxYears and minMonths == maxMonths %} + Toutes les modifications ont été faites il y a + {% if minYears > 0 %} + {{ minYears }} an{{ minYears > 1 ? 's' : '' }} + {% if minMonths > 0 %}, {{ minMonths }} mois{% endif %} + {% elseif minMonths > 0 %} + {{ minMonths }} mois + {% else %} + moins d'un mois + {% endif %} + {% else %} + Les modifications ont été faites entre il y a + {% if maxYears > 0 %} + {{ maxYears }} an{{ maxYears > 1 ? 's' : '' }} + {% if maxMonths > 0 %}, {{ maxMonths }} mois{% endif %} + {% elseif maxMonths > 0 %} + {{ maxMonths }} mois + {% else %} + moins d'un mois + {% endif %} + et il y a + {% if minYears > 0 %} + {{ minYears }} an{{ minYears > 1 ? 's' : '' }} + {% if minMonths > 0 %}, {{ minMonths }} mois{% endif %} + {% elseif minMonths > 0 %} + {{ minMonths }} mois + {% else %} + moins d'un mois + {% endif %}, + en moyenne il y a + {% if avgYears > 0 %} + {{ avgYears }} an{{ avgYears > 1 ? 's' : '' }} + {% if avgMonths > 0 %}, {{ avgMonths }} mois{% endif %} + {% elseif avgMonths > 0 %} + {{ avgMonths }} mois + {% else %} + moins d'un mois + {% endif %} + {% endif %} +
+
+
+ {% endif %} +
@@ -954,8 +1044,8 @@ window.updateMarkers = updateMarkers; {# markClosedSiretsOnTable(); #} function makeDonutGraphOfTags() { - // Récupérer tous les tags de la colonne 2 - const tags = Array.from(document.querySelectorAll('table tbody tr td:nth-child(3)')) + // Récupérer tous les tags de la colonne 4 (Type) + const tags = Array.from(document.querySelectorAll('table tbody tr td:nth-child(4)')) .map(cell => cell.textContent.trim()) .filter(tag => tag.includes('=')) // Filtrer les cellules qui ne contiennent pas de = .filter(tag => tag); // Filtrer les cellules vides diff --git a/templates/admin/stats/row.html.twig b/templates/admin/stats/row.html.twig index cbabaed..8910345 100644 --- a/templates/admin/stats/row.html.twig +++ b/templates/admin/stats/row.html.twig @@ -10,6 +10,12 @@ {% endif %} + + + voir email + + + {{ commerce.wheelchair }} {{ commerce.note }} {{ commerce.noteContent }} - {{ commerce.siret }} + + + {% if commerce.siret %} + {% set sirets = commerce.siret|split(';')|map(siret => siret|trim)|filter(siret => siret) %} + {% for siret in sirets %} + {% if not loop.first %}, {% endif %} + {{ siret }} + {% endfor %} + {% else %} + + {% endif %} + + {# (si siret clos) #} + {% if commerce.osmDataDate %} + {% set now = "now"|date("U") %} + {% set osmDate = commerce.osmDataDate|date("U") %} + {% set diff = now - osmDate %} + {% set years = (diff / 31536000)|round(0, 'floor') %} + {% set months = ((diff % 31536000) / 2592000)|round(0, 'floor') %} + {% set days = ((diff % 2592000) / 86400)|round(0, 'floor') %} + +
+
+ + {{ commerce.osmDataDate|date('d/m/Y H:i') }} +
+ {% if commerce.osmUser %} +
+ {% endif %} +
+ + {% if diff < 86400 %} + Aujourd'hui + {% elseif years > 0 %} + {{ years }} an{{ years > 1 ? 's' : '' }} + {% if months > 0 %}, {{ months }} mois{% endif %} + {% elseif months > 0 %} + {{ months }} mois + {% if days > 0 %}, {{ days }} jour{{ days > 1 ? 's' : '' }}{% endif %} + {% elseif days > 0 %} + {{ days }} jour{{ days > 1 ? 's' : '' }} + {% else %} + Aujourd'hui + {% endif %} + +
+
+ {% else %} + Non disponible + {% endif %} + + diff --git a/templates/admin/stats/table-head.html.twig b/templates/admin/stats/table-head.html.twig index f378bb0..6fbf92a 100644 --- a/templates/admin/stats/table-head.html.twig +++ b/templates/admin/stats/table-head.html.twig @@ -2,6 +2,10 @@ Nom ({{ stats.places|length }}) + + Email + + Completion % @@ -46,6 +50,10 @@ Siret clos + + Dernière modif. OSM + + Osm id diff --git a/templates/admin/stats_history.html.twig b/templates/admin/stats_history.html.twig index ab5508e..2ffecdb 100644 --- a/templates/admin/stats_history.html.twig +++ b/templates/admin/stats_history.html.twig @@ -11,33 +11,143 @@ document.addEventListener('DOMContentLoaded', function() { const ctx = document.getElementById('completionHistoryChart').getContext('2d'); + // Préparer les données pour chaque aspect + const labels = [ + {% for stat in statsHistory|reverse %} + '{{ stat.date|date('d/m/Y') }}'{% if not loop.last %},{% endif %} + {% endfor %} + ]; + + const completionData = [ + {% for stat in statsHistory|reverse %} + {{ stat.completionPercent }}{% if not loop.last %},{% endif %} + {% endfor %} + ]; + + const openingHoursData = [ + {% for stat in statsHistory|reverse %} + {% if stat.placesCount > 0 %} + {{ ((stat.openingHoursCount / stat.placesCount) * 100)|round(1) }}{% if not loop.last %},{% endif %} + {% else %} + 0{% if not loop.last %},{% endif %} + {% endif %} + {% endfor %} + ]; + + const addressData = [ + {% for stat in statsHistory|reverse %} + {% if stat.placesCount > 0 %} + {{ ((stat.addressCount / stat.placesCount) * 100)|round(1) }}{% if not loop.last %},{% endif %} + {% else %} + 0{% if not loop.last %},{% endif %} + {% endif %} + {% endfor %} + ]; + + const websiteData = [ + {% for stat in statsHistory|reverse %} + {% if stat.placesCount > 0 %} + {{ ((stat.websiteCount / stat.placesCount) * 100)|round(1) }}{% if not loop.last %},{% endif %} + {% else %} + 0{% if not loop.last %},{% endif %} + {% endif %} + {% endfor %} + ]; + + const siretData = [ + {% for stat in statsHistory|reverse %} + {% if stat.placesCount > 0 %} + {{ ((stat.siretCount / stat.placesCount) * 100)|round(1) }}{% if not loop.last %},{% endif %} + {% else %} + 0{% if not loop.last %},{% endif %} + {% endif %} + {% endfor %} + ]; + + const emailData = [ + {% for stat in statsHistory|reverse %} + {% if stat.placesCount > 0 %} + {{ ((stat.emailsCount / stat.placesCount) * 100)|round(1) }}{% if not loop.last %},{% endif %} + {% else %} + 0{% if not loop.last %},{% endif %} + {% endif %} + {% endfor %} + ]; + new Chart(ctx, { type: 'line', data: { - labels: [ - {% for stat in statsHistory %} - '{{ stat.date|date('d/m/Y') }}'{% if not loop.last %},{% endif %} - {% endfor %} - ], - datasets: [{ - label: 'Taux de complétion (%)', - data: [ - {% for stat in statsHistory %} - {{ stat.completionPercent }}{% if not loop.last %},{% endif %} - {% endfor %} - ], - borderColor: 'rgb(75, 192, 192)', - backgroundColor: 'rgba(75, 192, 192, 0.2)', - tension: 0.3, - fill: true - }] + labels: labels, + datasets: [ + { + label: 'Taux de complétion global (%)', + data: completionData, + borderColor: 'rgb(75, 192, 192)', + backgroundColor: 'rgba(75, 192, 192, 0.1)', + tension: 0.3, + fill: false, + borderWidth: 3 + }, + { + label: 'Horaires d\'ouverture (%)', + data: openingHoursData, + borderColor: 'rgb(255, 99, 132)', + backgroundColor: 'rgba(255, 99, 132, 0.1)', + tension: 0.3, + fill: false, + borderWidth: 2 + }, + { + label: 'Adresses (%)', + data: addressData, + borderColor: 'rgb(54, 162, 235)', + backgroundColor: 'rgba(54, 162, 235, 0.1)', + tension: 0.3, + fill: false, + borderWidth: 2 + }, + { + label: 'Sites web (%)', + data: websiteData, + borderColor: 'rgb(255, 205, 86)', + backgroundColor: 'rgba(255, 205, 86, 0.1)', + tension: 0.3, + fill: false, + borderWidth: 2 + }, + { + label: 'SIRET (%)', + data: siretData, + borderColor: 'rgb(153, 102, 255)', + backgroundColor: 'rgba(153, 102, 255, 0.1)', + tension: 0.3, + fill: false, + borderWidth: 2 + }, + { + label: 'Emails (%)', + data: emailData, + borderColor: 'rgb(199, 199, 199)', + backgroundColor: 'rgba(199, 199, 199, 0.1)', + tension: 0.3, + fill: false, + borderWidth: 2 + } + ] }, options: { responsive: true, plugins: { title: { display: true, - text: 'Évolution du taux de complétion au fil du temps' + text: 'Évolution des taux de complétion par aspect au fil du temps' + }, + legend: { + position: 'top', + labels: { + usePointStyle: true, + padding: 20 + } } }, scales: { @@ -46,7 +156,7 @@ document.addEventListener('DOMContentLoaded', function() { max: 100, title: { display: true, - text: 'Complétion (%)' + text: 'Pourcentage (%)' } }, x: { @@ -55,6 +165,10 @@ document.addEventListener('DOMContentLoaded', function() { text: 'Date' } } + }, + interaction: { + intersect: false, + mode: 'index' } } }); diff --git a/templates/admin/view_email_for_place.html.twig b/templates/admin/view_email_for_place.html.twig new file mode 100644 index 0000000..5272475 --- /dev/null +++ b/templates/admin/view_email_for_place.html.twig @@ -0,0 +1,22 @@ +{% extends 'base.html.twig' %} + +{% block title %}Email pour {{ place.name }}{% endblock %} + +{% block body %} +
+

Email pour {{ place.name }}

+
+ {% include 'admin/email_content.html.twig' with {'place': place} %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/base.html.twig b/templates/base.html.twig index c53b2d1..f2fb948 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -36,7 +36,7 @@
{% for label, messages in app.flashes %} {% for message in messages %} -