From f69b7824af2cc20932aeea2ce19bb4aa5c442b69 Mon Sep 17 00:00:00 2001 From: Tykayn Date: Mon, 26 May 2025 23:51:46 +0200 Subject: [PATCH] compute stats for completion by zone, have base tags, split categories --- migrations/Version20250526200731.php | 59 ++++ migrations/Version20250526203604.php | 59 ++++ public/assets/css/global.css | 328 ++++++++++++++++++ src/Controller/AdminController.php | 212 ++++++++++- src/Controller/PublicController.php | 33 +- src/DataFixtures/HistoryFixtures.php | 4 +- src/Entity/Place.php | 88 +++++ src/Entity/Stats.php | 111 +++++- src/Service/Motocultrice.php | 67 +++- templates/admin/labourage_results.html.twig | 38 +- templates/admin/stats.html.twig | 192 ++++++++++ templates/base.html.twig | 38 +- templates/public/edit.html.twig | 81 ++--- templates/public/edit/opening_hours.html.twig | 16 +- templates/public/edit/tags.html.twig | 15 +- templates/public/index.html.twig | 34 +- 16 files changed, 1257 insertions(+), 118 deletions(-) create mode 100644 migrations/Version20250526200731.php create mode 100644 migrations/Version20250526203604.php create mode 100644 public/assets/css/global.css create mode 100644 templates/admin/stats.html.twig diff --git a/migrations/Version20250526200731.php b/migrations/Version20250526200731.php new file mode 100644 index 0000000..7114454 --- /dev/null +++ b/migrations/Version20250526200731.php @@ -0,0 +1,59 @@ +addSql(<<<'SQL' + ALTER TABLE place ADD has_opening_hours BOOLEAN DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE place ADD has_address BOOLEAN DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE place ADD has_website BOOLEAN DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE place ADD has_wheelchair BOOLEAN DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE place ADD has_note BOOLEAN 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 has_opening_hours + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE place DROP has_address + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE place DROP has_website + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE place DROP has_wheelchair + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE place DROP has_note + SQL); + } +} diff --git a/migrations/Version20250526203604.php b/migrations/Version20250526203604.php new file mode 100644 index 0000000..bb7d7f5 --- /dev/null +++ b/migrations/Version20250526203604.php @@ -0,0 +1,59 @@ +addSql(<<<'SQL' + ALTER TABLE stats ADD avec_horaires SMALLINT NOT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE stats ADD avec_adresse SMALLINT NOT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE stats ADD avec_site SMALLINT NOT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE stats ADD avec_accessibilite SMALLINT NOT NULL + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE stats ADD avec_note SMALLINT NOT 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 stats DROP avec_horaires + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE stats DROP avec_adresse + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE stats DROP avec_site + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE stats DROP avec_accessibilite + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE stats DROP avec_note + SQL); + } +} diff --git a/public/assets/css/global.css b/public/assets/css/global.css new file mode 100644 index 0000000..c069fcb --- /dev/null +++ b/public/assets/css/global.css @@ -0,0 +1,328 @@ +:root { + --primary-color: #2c3e50; + --secondary-color: #3498db; + --accent-color: #e74c3c; + --text-color: #333; + --light-gray: #f5f6fa; + --border-color: #dcdde1; + --success-color: #27ae60; + --warning-color: #f1c40f; + --error-color: #c0392b; +} + +/* Reset et styles de base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + line-height: 1.6; + color: var(--text-color); + background-color: var(--light-gray); +} + +/* Layout */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +/* Navigation */ +.navbar { + background-color: var(--primary-color); + padding: 1rem 0; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.navbar-container { + display: flex; + justify-content: space-between; + align-items: center; +} + +.navbar-brand { + color: white; + text-decoration: none; + font-size: 1.5rem; + font-weight: bold; +} + +.navbar-menu { + display: flex; + gap: 1.5rem; +} + +.navbar-link { + color: white; + text-decoration: none; + padding: 0.5rem 1rem; + border-radius: 4px; + transition: background-color 0.3s; +} + +.navbar-link:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +/* Cards */ +.card { + background: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 1.5rem; + margin-bottom: 1.5rem; +} + +.card-title { + font-size: 1.25rem; + margin-bottom: 1rem; + color: var(--primary-color); +} + +/* Forms */ +.form-group { + margin-bottom: 1rem; +} + +.form-label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; +} + +.form-control { + width: 100%; + padding: 0.75rem; + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 1rem; + transition: border-color 0.3s; +} + +.form-control:focus { + outline: none; + border-color: var(--secondary-color); +} + +/* Buttons */ +.btn { + display: inline-block; + padding: 0.75rem 1.5rem; + border: none; + border-radius: 4px; + font-size: 1rem; + font-weight: 500; + text-align: center; + text-decoration: none; + cursor: pointer; + transition: background-color 0.3s, transform 0.2s; +} + +.btn-primary { + background-color: var(--secondary-color); + color: white; +} + +.btn-primary:hover { + background-color: #2980b9; + transform: translateY(-1px); +} + +.btn-secondary { + background-color: var(--primary-color); + color: white; +} + +.btn-secondary:hover { + background-color: #2c3e50; + transform: translateY(-1px); +} + +/* Alerts */ +.alert { + padding: 1rem; + border-radius: 4px; + margin-bottom: 1rem; +} + +.alert-success { + background-color: #d4edda; + color: var(--success-color); + border: 1px solid #c3e6cb; +} + +.alert-warning { + background-color: #fff3cd; + color: var(--warning-color); + border: 1px solid #ffeeba; +} + +.alert-error { + background-color: #f8d7da; + color: var(--error-color); + border: 1px solid #f5c6cb; +} + +/* Grid */ +.grid { + display: grid; + gap: 1.5rem; +} + +.grid-2 { + grid-template-columns: repeat(2, 1fr); +} + +.grid-3 { + grid-template-columns: repeat(3, 1fr); +} + +.grid-4 { + grid-template-columns: repeat(4, 1fr); +} + +/* Responsive */ +@media (max-width: 768px) { + + .grid-2, + .grid-3, + .grid-4 { + grid-template-columns: 1fr; + } + + .navbar-menu { + display: none; + } + + .navbar-menu.active { + display: flex; + flex-direction: column; + position: absolute; + top: 100%; + left: 0; + right: 0; + background-color: var(--primary-color); + padding: 1rem; + } +} + +/* Utilities */ +.text-center { + text-align: center; +} + +.text-right { + text-align: right; +} + +.text-left { + text-align: left; +} + +.mt-1 { + margin-top: 0.5rem; +} + +.mt-2 { + margin-top: 1rem; +} + +.mt-3 { + margin-top: 1.5rem; +} + +.mt-4 { + margin-top: 2rem; +} + +.mb-1 { + margin-bottom: 0.5rem; +} + +.mb-2 { + margin-bottom: 1rem; +} + +.mb-3 { + margin-bottom: 1.5rem; +} + +.mb-4 { + margin-bottom: 2rem; +} + +.p-1 { + padding: 0.5rem; +} + +.p-2 { + padding: 1rem; +} + +.p-3 { + padding: 1.5rem; +} + +.p-4 { + padding: 2rem; +} + +/* Map container */ +.map-container { + width: 100%; + height: 400px; + border-radius: 8px; + overflow: hidden; + margin-bottom: 1.5rem; +} + +/* Tags display */ +.tags-container { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 1rem 0; +} + +.tag { + background-color: var(--light-gray); + padding: 0.25rem 0.75rem; + border-radius: 16px; + font-size: 0.875rem; + color: var(--primary-color); + border: 1px solid var(--border-color); +} + +/* Footer */ +.footer { + background-color: var(--primary-color); + color: white; + padding: 2rem 0; + margin-top: 3rem; +} + +.footer-content { + display: flex; + justify-content: space-between; + align-items: center; +} + +.footer-links { + display: flex; + gap: 1.5rem; +} + +.footer-link { + color: white; + text-decoration: none; + opacity: 0.8; + transition: opacity 0.3s; +} + +.footer-link:hover { + opacity: 1; +} \ No newline at end of file diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php index 478369e..2541cf1 100644 --- a/src/Controller/AdminController.php +++ b/src/Controller/AdminController.php @@ -6,7 +6,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use App\Entity\Place; - +use App\Entity\Stats; use App\Service\Motocultrice; use Doctrine\ORM\EntityManagerInterface; use function uuid_create; @@ -29,6 +29,48 @@ final class AdminController extends AbstractController ]); } + #[Route('/admin/stats/{zip_code}', name: 'app_admin_stats')] + public function calculer_stats(string $zip_code): Response + { + // Récupérer tous les commerces de la zone + $commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $zip_code]); + + // Récupérer les stats existantes pour la zone + $stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $zip_code]); + + if(!$stats) { + $stats = new Stats(); + $stats->setZone($zip_code); + } + + // Calculer les statistiques + $calculatedStats = $this->motocultrice->calculateStats($commerces); + + // Mettre à jour les stats pour la zone donnée + $stats->setPlacesCount($calculatedStats['places_count']); + $stats->setAvecHoraires($calculatedStats['counters']['avec_horaires']); + $stats->setAvecAdresse($calculatedStats['counters']['avec_adresse']); + $stats->setAvecSite($calculatedStats['counters']['avec_site']); + $stats->setAvecAccessibilite($calculatedStats['counters']['avec_accessibilite']); + $stats->setAvecNote($calculatedStats['counters']['avec_note']); + $stats->setCompletionPercent($calculatedStats['completion_percent']); + + // Associer les stats à chaque commerce + foreach ($commerces as $commerce) { + $commerce->setStats($stats); + $this->entityManager->persist($commerce); + } + + $this->entityManager->persist($stats); + $this->entityManager->flush(); + + return $this->render('admin/stats.html.twig', [ + 'stats' => $stats, + 'zip_code' => $zip_code, + 'counters' => $calculatedStats['counters'] + ]); + } + #[Route('/admin/labourer/{zip_code}', name: 'app_admin_labourer')] public function labourer_zone(string $zip_code): Response { @@ -39,10 +81,71 @@ final class AdminController extends AbstractController // Récupérer les commerces existants dans la base de données pour cette zone $commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $zip_code]); + + // Récupérer ou créer les stats pour cette zone + $stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $zip_code]); + + if (!$stats) { + $stats = new Stats(); + $stats->setZone($zip_code); + + // for commerce, set stats + foreach ($commerces as $commerce) { + $commerce->setStats($stats); + $this->entityManager->persist($commerce); + $stats->addPlace($commerce); + } + + // rebuild et persist + $stats->computeCompletionPercent(); + $this->entityManager->persist($stats); + $this->entityManager->flush(); + } + + // Initialiser les compteurs + $counters = [ + 'avec_horaires' => 0, + 'avec_adresse' => 0, + 'avec_site' => 0, + 'avec_accessibilite' => 0, + 'avec_note' => 0 + ]; + + // Compter les différents critères pour chaque commerce + foreach ($commerces as $commerce) { + if ($commerce->hasOpeningHours()) { + $counters['avec_horaires']++; + } + if ($commerce->hasAddress()) { + $counters['avec_adresse']++; + } + if ($commerce->hasWebsite()) { + $counters['avec_site']++; + } + if ($commerce->hasWheelchair()) { + $counters['avec_accessibilite']++; + } + if ($commerce->hasNote()) { + $counters['avec_note']++; + } + $commerce->setStats($stats); + } + + // Mettre à jour les statistiques + $stats->setPlacesCount(count($commerces)); + $stats->setAvecHoraires($counters['avec_horaires']); + $stats->setAvecAdresse($counters['avec_adresse']); + $stats->setAvecSite($counters['avec_site']); + $stats->setAvecAccessibilite($counters['avec_accessibilite']); + $stats->setAvecNote($counters['avec_note']); + $stats->computeCompletionPercent(); + + $this->entityManager->persist($stats); + $this->entityManager->flush(); $osm_object_ids = []; - if ($commerces) { + if ($commerces) { // Extraire les osm_object_ids des commerces existants $osm_object_ids = array_map(function($commerce) { return $commerce->getOsmId(); @@ -53,7 +156,7 @@ final class AdminController extends AbstractController $results = array_filter($results, function($commerce) use ($osm_object_ids) { return !in_array($commerce['id'], $osm_object_ids); }); - + // on crée un commerce pour chaque résultat qui reste foreach ($results as $result) { $commerce = new Place(); @@ -65,11 +168,18 @@ final class AdminController extends AbstractController ->setUuidForUrl($this->motocultrice->uuid_create()) ->setOptedOut(false) ->setDead(false) - ->setNote($result['note'] ?? null) + ->setModifiedDate(new \DateTime()) ->setAskedHumainsSupport(false) ->setLastContactAttemptDate(null) - ->setStats(null); + ->setStats(null) + ->setNote($result['tags'] && isset($result['tags']['note']) ? isset($result['tags']['note']) : null) + ->setHasOpeningHours($result['tags'] && isset($result['tags']['opening_hours']) ? isset($result['tags']['opening_hours']) : null) + ->setHasAddress(($result['tags'] && isset($result['tags']['address']) || $result['tags'] && isset($result['tags']['contact:address'])) ? isset($result['tags']['address']) : null) + ->setHasWebsite($result['tags'] && isset($result['tags']['website']) ? $result['tags']['website'] : null) + ->setHasWheelchair($result['tags'] && isset($result['tags']['wheelchair']) ? $result['tags']['wheelchair'] : null) + ->setHasNote($result['tags'] && isset($result['tags']['note']) ? $result['tags']['note'] : null) + ; $this->entityManager->persist($commerce); } @@ -77,7 +187,99 @@ final class AdminController extends AbstractController return $this->render('admin/labourage_results.html.twig', [ 'results' => $results, + 'commerces' => $commerces, 'zone' => $zip_code, ]); } + + #[Route('/admin/delete/{id}', name: 'app_admin_delete')] + public function delete(int $id): Response + { + $commerce = $this->entityManager->getRepository(Place::class)->find($id); + $name = $commerce->getName(); + $this->entityManager->remove($commerce); + $this->entityManager->flush(); + + $this->addFlash('success', 'Le lieu '.$name.' a été supprimé avec succès de OSM Mes commerces, mais pas dans OpenStreetMap.'); + + return $this->redirectToRoute('app_admin_dashboard'); + } + + #[Route('/admin/delete_by_zone/{zip_code}', name: 'app_admin_delete_by_zone')] + public function delete_by_zone(string $zip_code): Response + { + $commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $zip_code]); + + foreach ($commerces as $commerce) { + $this->entityManager->remove($commerce); + } + $this->entityManager->flush(); + + $this->addFlash('success', 'Tous les commerces de la zone '.$zip_code.' ont été supprimés avec succès de OSM Mes commerces, mais pas dans OpenStreetMap.'); + + return $this->redirectToRoute('app_admin_dashboard'); + } + + + #[Route('/admin/export', name: 'app_admin_export')] + public function export(): Response + { + $places = $this->entityManager->getRepository(Place::class)->findAll(); + + $csvData = []; + $csvData[] = [ + 'Nom', + 'Email', + 'Code postal', + 'ID OSM', + 'Type OSM', + 'Date de modification', + 'Date dernier contact', + 'Note', + 'Désabonné', + 'Inactif', + 'Support humain demandé', + 'A des horaires', + 'A une adresse', + 'A un site web', + 'A accessibilité', + 'A une note' + ]; + + foreach ($places as $place) { + $csvData[] = [ + $place->getName(), + $place->getEmail(), + $place->getZipCode(), + $place->getOsmId(), + $place->getOsmKind(), + $place->getModifiedDate() ? $place->getModifiedDate()->format('Y-m-d H:i:s') : '', + $place->getLastContactAttemptDate() ? $place->getLastContactAttemptDate()->format('Y-m-d H:i:s') : '', + $place->getNote(), + $place->isOptedOut() ? 'Oui' : 'Non', + $place->isDead() ? 'Oui' : 'Non', + $place->isAskedHumainsSupport() ? 'Oui' : 'Non', + $place->hasOpeningHours() ? 'Oui' : 'Non', + $place->hasAddress() ? 'Oui' : 'Non', + $place->hasWebsite() ? 'Oui' : 'Non', + $place->hasWheelchair() ? 'Oui' : 'Non', + $place->hasNote() ? 'Oui' : 'Non' + ]; + } + + $response = new Response(); + $response->headers->set('Content-Type', 'text/csv'); + $response->headers->set('Content-Disposition', 'attachment; filename="export_places.csv"'); + + $handle = fopen('php://temp', 'r+'); + foreach ($csvData as $row) { + fputcsv($handle, $row, ';'); + } + rewind($handle); + $response->setContent(stream_get_contents($handle)); + fclose($handle); + + return $response; + } + } diff --git a/src/Controller/PublicController.php b/src/Controller/PublicController.php index d6a7462..ec7cd49 100644 --- a/src/Controller/PublicController.php +++ b/src/Controller/PublicController.php @@ -40,12 +40,31 @@ class PublicController extends AbstractController { $place = $this->entityManager->getRepository(Place::class)->findOneBy(['uuid_for_url' => $uuid]); if (!$place) { + $this->addFlash('warning', 'Ce lien de modification n\'existe pas.'); return $this->redirectToRoute('app_public_index'); } - $commerce = $this->motocultrice->get_osm_object_data($place->getOsmKind(), $place->getOsmId()); + + if ($place->getOsmKind() === 'relation') { + $this->addFlash('warning', 'Les objets OSM de type "relation" ne sont pas gérés dans cet outil.'); + return $this->redirectToRoute('app_public_index'); + } + + // récupérer les tags de base + $base_tags = $this->motocultrice->base_tags; + $base_tags = array_fill_keys($base_tags, ''); + + $commerce_overpass = $this->motocultrice->get_osm_object_data($place->getOsmKind(), $place->getOsmId()); + // Fusionner les tags de base avec les tags existants + + $commerce_overpass['tags_converted'] = array_merge($base_tags, $commerce_overpass['tags_converted']); + + // Trier les tags par ordre alphabétique des clés + ksort($commerce_overpass['tags_converted']); + return $this->render('public/edit.html.twig', [ - 'commerce' => $commerce, + 'commerce_overpass' => $commerce_overpass, 'name' => $name, + 'commerce' => $place, 'osm_kind' => $place->getOsmKind(), "mapbox_token" => $_ENV['MAPBOX_TOKEN'], "maptiler_token" => $_ENV['MAPTILER_TOKEN'], @@ -57,7 +76,8 @@ class PublicController extends AbstractController { // get stats $stats = $this->entityManager->getRepository(Stats::class)->findAll(); - $places = $this->entityManager->getRepository(Place::class)->findAll(); + + $places = $this->entityManager->getRepository(Place::class)->findBy([], ['zip_code' => 'ASC', 'name' => 'ASC']); return $this->render('public/dashboard.html.twig', [ 'controller_name' => 'PublicController', 'stats' => $stats, @@ -108,6 +128,9 @@ class PublicController extends AbstractController // Récupérer le token OSM depuis les variables d'environnement $osm_api_token = $_ENV['APP_OSM_BEARER']; + + $exception = false; + $exception_message = ""; try { $client = new Client(); @@ -186,6 +209,8 @@ class PublicController extends AbstractController } } catch (\Exception $e) { $status = "Erreur lors de la communication avec l'API OSM: " . $e->getMessage(); + $exception = true; + $exception_message = $e->getMessage(); // Debug de la réponse en cas d'erreur if (method_exists($e, 'getResponse')) { var_dump($e->getResponse()->getBody()->getContents()); @@ -199,6 +224,8 @@ class PublicController extends AbstractController 'controller_name' => 'PublicController', 'commerce' => $commerce, 'status' => $status, + 'exception' => $exception, + 'exception_message' => $exception_message, 'mapbox_token' => $_ENV['MAPBOX_TOKEN'], 'maptiler_token' => $_ENV['MAPTILER_TOKEN'], ]); diff --git a/src/DataFixtures/HistoryFixtures.php b/src/DataFixtures/HistoryFixtures.php index db193fe..7819eb0 100644 --- a/src/DataFixtures/HistoryFixtures.php +++ b/src/DataFixtures/HistoryFixtures.php @@ -21,7 +21,7 @@ class HistoryFixtures extends Fixture // Créer quelques places de test for ($i = 0; $i < 15; $i++) { $place = new Place(); - $place->setName($faker->company) + $place->setName($faker->company . ' (mock)') ->setUuidForUrl($faker->uuid) ->setOsmId((string)$faker->numberBetween(1000000, 9999999)) ->setOsmKind($faker->randomElement(['node', 'way', 'relation'])) @@ -48,7 +48,7 @@ class HistoryFixtures extends Fixture // Créer des statistiques de test for ($i = 0; $i < 3; $i++) { $stat = new Stats(); - $stat->setZone($faker->city) + $stat->setZone($faker->city . ' (mock)') ->setCompletionPercent($faker->numberBetween(0, 100)) ->addPlace( $faker->randomElement($places_list)) ->addPlace( $faker->randomElement($places_list)) diff --git a/src/Entity/Place.php b/src/Entity/Place.php index 5a58570..d37c468 100644 --- a/src/Entity/Place.php +++ b/src/Entity/Place.php @@ -61,6 +61,21 @@ class Place #[ORM\Column(length: 255, nullable: true)] private ?string $name = null; + #[ORM\Column(nullable: true)] + private ?bool $has_opening_hours = null; + + #[ORM\Column(nullable: true)] + private ?bool $has_address = null; + + #[ORM\Column(nullable: true)] + private ?bool $has_website = null; + + #[ORM\Column(nullable: true)] + private ?bool $has_wheelchair = null; + + #[ORM\Column(nullable: true)] + private ?bool $has_note = null; + public function __construct() { $this->histories = new ArrayCollection(); @@ -256,4 +271,77 @@ class Place return $this; } + + public function hasOpeningHours(): ?bool + { + return $this->has_opening_hours; + } + + public function setHasOpeningHours(?bool $has_opening_hours): static + { + $this->has_opening_hours = $has_opening_hours; + + return $this; + } + + public function hasAddress(): ?bool + { + return $this->has_address; + } + + public function setHasAddress(?bool $has_address): static + { + $this->has_address = $has_address; + + return $this; + } + + public function hasWebsite(): ?bool + { + return $this->has_website; + } + + public function setHasWebsite(?bool $has_website): static + { + $this->has_website = $has_website; + + return $this; + } + + public function hasWheelchair(): ?bool + { + return $this->has_wheelchair; + } + + public function setHasWheelchair(?bool $has_wheelchair): static + { + $this->has_wheelchair = $has_wheelchair; + + return $this; + } + + public function hasNote(): ?bool + { + return $this->has_note; + } + + public function setHasNote(?bool $has_note): static + { + $this->has_note = $has_note; + + return $this; + } + + + public function getPlaceCount(): ?int + { + return $this->place_count; + } + + public function setPlaceCount(int $place_count): static + { + $this->place_count = $place_count; + + return $this; + } } diff --git a/src/Entity/Stats.php b/src/Entity/Stats.php index c76f3ef..c802fef 100644 --- a/src/Entity/Stats.php +++ b/src/Entity/Stats.php @@ -28,9 +28,54 @@ class Stats #[ORM\OneToMany(targetEntity: Place::class, mappedBy: 'stats')] private Collection $places; - #[ORM\Column(type: Types::SMALLINT)] + // nombre de commerces dans la zone + #[ORM\Column(type: Types::SMALLINT, nullable: true)] private ?int $places_count = null; + // nombre de commerces avec horaires + #[ORM\Column(type: Types::SMALLINT, nullable: true)] + private ?int $avec_horaires = null; + + // nombre de commerces avec adresse + #[ORM\Column(type: Types::SMALLINT, nullable: true)] + private ?int $avec_adresse = null; + + // nombre de commerces avec site + #[ORM\Column(type: Types::SMALLINT, nullable: true)] + private ?int $avec_site = null; + + // nombre de commerces avec accessibilité + #[ORM\Column(type: Types::SMALLINT, nullable: true)] + private ?int $avec_accessibilite = null; + + // nombre de commerces avec note + #[ORM\Column(type: Types::SMALLINT, nullable: true)] + private ?int $avec_note = null; + + // calcule le pourcentage de complétion de la zone + public function computeCompletionPercent(): ?int + { + // Si aucun commerce, on retourne 0 + if ($this->places_count === 0 || $this->places_count === null) { + $this->setCompletionPercent(0); + return 0; + } + + // On prend le maximum entre les différents critères + $max = max( + $this->avec_horaires ?? 0, + $this->avec_adresse ?? 0, + $this->avec_site ?? 0, + $this->avec_accessibilite ?? 0, + $this->avec_note ?? 0 + ); + $computed = round(($max) / $this->places_count * 100); + $this->setCompletionPercent($computed); + return $this->completion_percent; + } + + + public function __construct() { $this->places = new ArrayCollection(); @@ -106,4 +151,66 @@ class Stats return $this; } -} + + public function getAvecHoraires(): ?int + { + return $this->avec_horaires; + } + + public function setAvecHoraires(int $avec_horaires): static + { + $this->avec_horaires = $avec_horaires; + + return $this; + } + + public function getAvecAdresse(): ?int + { + return $this->avec_adresse; + } + + public function setAvecAdresse(int $avec_adresse): static + { + $this->avec_adresse = $avec_adresse; + + return $this; + } + + public function getAvecSite(): ?int + { + return $this->avec_site; + } + + public function setAvecSite(int $avec_site): static + { + $this->avec_site = $avec_site; + + return $this; + } + + public function getAvecAccessibilite(): ?int + { + return $this->avec_accessibilite; + } + + public function setAvecAccessibilite(int $avec_accessibilite): static + { + $this->avec_accessibilite = $avec_accessibilite; + + return $this; + } + + public function getAvecNote(): ?int + { + return $this->avec_note; + } + + public function setAvecNote(int $avec_note): static + { + $this->avec_note = $avec_note; + + return $this; + } + + +} diff --git a/src/Service/Motocultrice.php b/src/Service/Motocultrice.php index 1124218..617c0f4 100644 --- a/src/Service/Motocultrice.php +++ b/src/Service/Motocultrice.php @@ -10,12 +10,26 @@ class Motocultrice private $overpassApiUrl = 'https://overpass-api.de/api/interpreter'; private $osmApiUrl = 'https://www.openstreetmap.org/api/0.6'; + public $base_tags = [ + 'name', + 'opening_hours', + 'contact:email', + 'contact:phone', + 'wheelchair', + 'addr:housenumber', + 'addr:street', + 'contact:website', + 'contact:mastodon', + // 'EEEEEEEEEEEEEEEEEEE' + ]; + private $more_tags = ['image', 'ref:FR:SIRET']; public function __construct( private HttpClientInterface $client, private EntityManagerInterface $entityManager ) { } + public function labourer(string $zone): array { if (!$zone) { @@ -58,6 +72,9 @@ QUERY; if (isset($data['elements'])) { foreach ($data['elements'] as $element) { if (isset($element['tags'])) { + + + $email = $element['tags']['contact:email'] ?? $element['tags']['email'] ?? null; // On passe si pas d'email if (!$email) { @@ -86,6 +103,8 @@ QUERY; public function get_osm_object_data($osm_kind = 'node', $osm_object_id = 12855459190) { $object_id = "https://www.openstreetmap.org/api/0.6/".$osm_kind."/".$osm_object_id; + // dump($object_id); + // die(); try { $response = $this->client->request('GET', $object_id); @@ -96,8 +115,8 @@ QUERY; throw new \Exception("Impossible de récupérer les données OSM : " . $e->getMessage()); } - // convertir les tags en clés et valeurs - $osm_object_data['tags_converted'] = []; + // convertir les tags en clés et valeurs, remplir avec les tags de base + $osm_object_data['tags_converted'] = $this->base_tags; // Initialiser le tableau des tags convertis if (isset($osm_object_data['node'])) { $osm_object_data['node']['tags_converted'] = []; @@ -236,4 +255,48 @@ QUERY; throw new \Exception("Erreur lors de la communication avec l'API OSM : " . $e->getMessage()); } } + + public function calculateStats(array $places): array + { + $counters = [ + 'avec_horaires' => 0, + 'avec_adresse' => 0, + 'avec_site' => 0, + 'avec_accessibilite' => 0, + 'avec_note' => 0 + ]; + + foreach ($places as $place) { + if ($place->hasOpeningHours()) { + $counters['avec_horaires']++; + } + if ($place->hasAddress()) { + $counters['avec_adresse']++; + } + if ($place->hasWebsite()) { + $counters['avec_site']++; + } + if ($place->hasWheelchair()) { + $counters['avec_accessibilite']++; + } + if ($place->hasNote()) { + $counters['avec_note']++; + } + } + + $totalPlaces = count($places); + $completionPercent = 0; + + if ($totalPlaces > 0) { + $totalCriteria = 5; // nombre total de critères + $totalCompleted = array_sum($counters); + $completionPercent = round(($totalCompleted / ($totalPlaces * $totalCriteria)) * 100); + } + + return [ + 'places_count' => $totalPlaces, + 'completion_percent' => $completionPercent, + 'counters' => $counters + ]; + } } \ No newline at end of file diff --git a/templates/admin/labourage_results.html.twig b/templates/admin/labourage_results.html.twig index cffa05c..2c91677 100644 --- a/templates/admin/labourage_results.html.twig +++ b/templates/admin/labourage_results.html.twig @@ -11,15 +11,37 @@

Labourage fait sur la zone "{{ zone }}" ✅

+

+ lieux trouvés en plus: {{ results|length }} +

+ {# {{ dump(results) }} #} - {% for commerce in results %} -

- {{ commerce.name }} -

-
-            {{ dump(commerce) }}
-        
+
+ +

+commerces existants disposant d'un moyen de contact mail: {{ commerces|length }} +

+{# {{ dump(commerces[0]) }} #} +
{% endblock %} diff --git a/templates/admin/stats.html.twig b/templates/admin/stats.html.twig new file mode 100644 index 0000000..df1c766 --- /dev/null +++ b/templates/admin/stats.html.twig @@ -0,0 +1,192 @@ +{% extends 'base.html.twig' %} + +{% block title %}{{ 'display.stats'|trans }}{% endblock %} + +{% block body %} +
+
+

{{ 'display.stats'|trans }}

+

+ {{ stats.zone }} +

+

+ {# {{ dump(stats) }} #} +

+ {{ stats.getCompletionPercent() }} % complété sur les critères donnés. +
+ {{ stats.getPlacesCount() }} commerces dans la zone. +
+ {{ stats.getAvecHoraires() }} commerces avec horaires. +
+ {{ stats.getAvecAdresse() }} commerces avec adresse. +
+ {{ stats.getAvecSite() }} commerces avec site web renseigné. +
+ {{ stats.getAvecAccessibilite() }} commerces avec accessibilité renseignée. +
+ {{ stats.getAvecNote() }} commerces avec note renseignée. +
+
+ +
+

Tableau des commerces

+ + + + + + + + + + + + {% for commerce in stats.places %} + + + + + + + + {% endfor %} + +
Nom ({{ stats.getPlacesCount() }})Adresse ({{ stats.getAvecAdresse() }} / {{ stats.getPlacesCount() }})Site web ({{ stats.getAvecSite() }} / {{ stats.getPlacesCount() }})Accessibilité ({{ stats.getAvecAccessibilite() }} / {{ stats.getPlacesCount() }})Note ({{ stats.getAvecNote() }} / {{ stats.getPlacesCount() }})
{{ commerce.name }}{{ commerce.address }}{{ commerce.website }}{{ commerce.wheelchair }}{{ commerce.note }}
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/base.html.twig b/templates/base.html.twig index b102a8f..6a0113c 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -10,6 +10,7 @@ {# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #} {% block stylesheets %} + {{ encore_entry_link_tags('app') }} {% endblock %} @@ -17,6 +18,7 @@
+ + {% for label, messages in app.flashes %} + {% for message in messages %} + + {% endfor %} + {% endfor %} +
@@ -34,7 +68,9 @@

OpenStreetMap Mon Commerce

-

Licence AGPLv3+, fait par CipherBliss, membre de la fédération des professionels d'OpenStreetMap, Sources des données : OpenStreetMap. +

Licence AGPLv3+, + fait par Tykayn de + CipherBliss EI, membre de la fédération des professionels d'OpenStreetMap, Sources des données : OpenStreetMap.
OpenStreetMap France

diff --git a/templates/public/edit.html.twig b/templates/public/edit.html.twig index a2fa219..4c8b65f 100644 --- a/templates/public/edit.html.twig +++ b/templates/public/edit.html.twig @@ -18,30 +18,7 @@ {% block body %}
- +
@@ -50,22 +27,18 @@

{{ 'display.welcome'|trans }}

- {% if commerce is not empty %} -
-
- -
-
+ {% if commerce_overpass is not empty %} + -
+
{% include 'public/edit/ask_angela.html.twig' %} - {% include 'public/edit/wheelchair.html.twig' %} - {% include 'public/edit/opening_hours.html.twig' %} - {% include 'public/edit/address.html.twig' %} - {% include 'public/edit/tags.html.twig' %} + {# {# {% include 'public/edit/wheelchair.html.twig' %} #} + {# {% include 'public/edit/opening_hours.html.twig' %} + {% include 'public/edit/address.html.twig' %} #} #} + {% include 'public/edit/tags.html.twig' %} @@ -124,15 +97,15 @@
- {{ 'display.last_modification'|trans }}: {{ commerce['@attributes'].timestamp }}, - {{ 'display.days_ago'|trans({'%days%': date(commerce['@attributes'].timestamp).diff(date()).days}) }} + {{ 'display.last_modification'|trans }}: {{ commerce_overpass['@attributes'].timestamp }}, + {{ 'display.days_ago'|trans({'%days%': date(commerce_overpass['@attributes'].timestamp).diff(date()).days}) }} {{ 'display.by'|trans }} - {{ commerce['@attributes'].user }} + {{ commerce_overpass['@attributes'].user }} - {{ dump(commerce) }} + {{ dump(commerce_overpass) }}
@@ -162,25 +135,31 @@ {% block javascripts %} {{ parent() }} - {# #} + {% endblock %} {% endblock %} diff --git a/templates/public/edit/opening_hours.html.twig b/templates/public/edit/opening_hours.html.twig index b93fa6c..57b91e8 100644 --- a/templates/public/edit/opening_hours.html.twig +++ b/templates/public/edit/opening_hours.html.twig @@ -2,7 +2,16 @@

{{ 'display.opening_hours'|trans }}

{{ 'display.opening_hours_description'|trans }}

- + {% if commerce_overpass.tags_converted.opening_hours is defined %} + {{ dump(commerce_overpass.tags_converted.opening_hours) }} + {% else %} + +
ajoutez les horaires au format OSM + + + {% endif %} + + {# Lundi de @@ -16,7 +25,8 @@ à . - + #}
- + +
\ No newline at end of file diff --git a/templates/public/edit/tags.html.twig b/templates/public/edit/tags.html.twig index 23b39ca..2dc79bd 100644 --- a/templates/public/edit/tags.html.twig +++ b/templates/public/edit/tags.html.twig @@ -1,21 +1,16 @@ {% block tags %}
- {% for attributes in commerce.tag %} - {% for kv in attributes %} - {# {% if kv.k == 'opening_hours' %} - {{ 'display.keys.opening_hours'|trans }} - {% else %} #} + {% for k, v in commerce_overpass.tags_converted %}
- - {{ ('display.keys.' ~ kv.k)|trans }} + + {{ ('display.keys.' ~ k)|trans }}
- + +
- {# {% endif %} #} - {% endfor %} {% endfor %}
diff --git a/templates/public/index.html.twig b/templates/public/index.html.twig index f657702..46f4621 100644 --- a/templates/public/index.html.twig +++ b/templates/public/index.html.twig @@ -14,40 +14,12 @@ {% block body %}
- +
-
-
-

{{ 'display.welcome'|trans }}

-
- - -
+
+ Ce site permet aux commerçants et aux lieux référencés sur OpenStreetMap de modifier leurs informations facilement pour gagner en visibilité sur des milliers de sites web à la fois.