From a81112a01811b012abf5e0bec08f464bb3481847 Mon Sep 17 00:00:00 2001 From: Tykayn Date: Fri, 8 Aug 2025 18:51:44 +0200 Subject: [PATCH] up command and labourage --- README.md | 6 + src/Command/UpdateEmptyStatsKindCommand.php | 67 +++++++++ src/Controller/AdminController.php | 146 ++++++++++++-------- src/Controller/PublicController.php | 67 +++++++-- templates/public/cities.html.twig | 79 ++++++++--- 5 files changed, 279 insertions(+), 86 deletions(-) create mode 100644 src/Command/UpdateEmptyStatsKindCommand.php diff --git a/README.md b/README.md index f0bf91d..298b4fb 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,12 @@ Récupère et stocke les coordonnées lat/lon pour toutes les villes dans la bas php bin/console app:update-city-coordinates ``` +### Mise à jour des Stats avec kind vide +Change les Stats qui ont un kind vide (NULL) pour leur mettre "user" en kind et les enregistre : +```shell +php bin/console app:update-empty-stats-kind +``` + ### Test du budget Teste le calcul du budget pour une ville donnée : ```shell diff --git a/src/Command/UpdateEmptyStatsKindCommand.php b/src/Command/UpdateEmptyStatsKindCommand.php new file mode 100644 index 0000000..4ddc5b1 --- /dev/null +++ b/src/Command/UpdateEmptyStatsKindCommand.php @@ -0,0 +1,67 @@ +title('Mise à jour des Stats avec kind vide créés avant aujourd\'hui'); + + // Date d'aujourd'hui à minuit + $today = new \DateTime(); + $today->setTime(0, 0, 0); + + // Trouver toutes les Stats avec kind NULL créés avant aujourd'hui + $stats = $this->entityManager->getRepository(Stats::class) + ->createQueryBuilder('s') +// ->where('s.kind IS NULL') + ->where('s.date_created < :today') + ->setParameter('today', $today) + ->getQuery() + ->getResult(); + + if (empty($stats)) { + $io->success('Aucune Stats avec kind vide créée avant aujourd\'hui trouvée.'); + return Command::SUCCESS; + } + + $count = count($stats); + $io->info(sprintf('Trouvé %d Stats avec kind vide créées avant aujourd\'hui.', $count)); + + // Mettre à jour chaque Stats avec kind = "user" + $io->progressStart($count); + foreach ($stats as $stat) { + $stat->setKind('user'); + $this->entityManager->persist($stat); + $io->progressAdvance(); + } + $io->progressFinish(); + + // Enregistrer les changements + $this->entityManager->flush(); + + $io->success(sprintf('%d Stats créées avant aujourd\'hui ont été mises à jour avec kind = "user".', $count)); + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php index dad248e..1d93cc9 100644 --- a/src/Controller/AdminController.php +++ b/src/Controller/AdminController.php @@ -3,24 +3,22 @@ namespace App\Controller; -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\Entity\StatsHistory; -use App\Service\Motocultrice; +use App\Service\ActionLogger; use App\Service\BudgetService; +use App\Service\FollowUpService; +use App\Service\Motocultrice; use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\HttpFoundation\Request; -use function uuid_create; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; use Twig\Environment; -use App\Service\ActionLogger; -use DateTime; -use App\Service\FollowUpService; -use phpDocumentor\Reflection\DocBlock\Tags\Var_; +use function uuid_create; final class AdminController extends AbstractController { @@ -31,12 +29,13 @@ final class AdminController extends AbstractController public function __construct( private EntityManagerInterface $entityManager, - private Motocultrice $motocultrice, - private BudgetService $budgetService, - private Environment $twig, - private ActionLogger $actionLogger, - FollowUpService $followUpService - ) { + private Motocultrice $motocultrice, + private BudgetService $budgetService, + private Environment $twig, + private ActionLogger $actionLogger, + FollowUpService $followUpService + ) + { $this->followUpService = $followUpService; } @@ -46,7 +45,6 @@ final class AdminController extends AbstractController { - $this->actionLogger->log('labourer_toutes_les_zones', []); $updateExisting = true; @@ -103,8 +101,7 @@ final class AdminController extends AbstractController ->setSiret($this->motocultrice->find_siret($placeData['tags']) ?? '') ->setAskedHumainsSupport(false) ->setLastContactAttemptDate(null) - ->setPlaceCount(0) - // ->setOsmData($placeData['modified'] ?? null) + ->setPlaceCount(0)// ->setOsmData($placeData['modified'] ?? null) ; // Mettre à jour les données depuis Overpass @@ -324,7 +321,44 @@ final class AdminController extends AbstractController $this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager); $followups = $stats->getCityFollowUps(); } - $commerces = $stats->getPlaces(); + $commerces = $stats->getPlacesCount(); + if (!$commerces) { +// labourer + $places_found = $this->motocultrice->labourer($insee_code); + if (count($places_found)) { + var_dump(count($places_found)); + + foreach ($places_found as $placeData) { + $newPlace = new Place(); + $newPlace->setOsmId($placeData['id']) + ->setOsmKind($placeData['type']) + ->setZipCode($insee_code) + ->setUuidForUrl($this->motocultrice->uuid_create()) + ->setModifiedDate(new \DateTime()) + ->setStats($stats) + ->setDead(false) + ->setOptedOut(false) + ->setMainTag($this->motocultrice->find_main_tag($placeData['tags']) ?? '') + ->setStreet($this->motocultrice->find_street($placeData['tags']) ?? '') + ->setHousenumber($this->motocultrice->find_housenumber($placeData['tags']) ?? '') + ->setSiret($this->motocultrice->find_siret($placeData['tags']) ?? '') + ->setAskedHumainsSupport(false) + ->setLastContactAttemptDate(null) + ->setPlaceCount(0)// ->setOsmData($placeData['modified'] ?? null) + ; + + // Mettre à jour les données depuis Overpass + $newPlace->update_place_from_overpass_data($placeData); + + $stats->addPlace($newPlace); + $newPlace->setStats($stats); + $this->entityManager->persist($newPlace); + } + } + } + $this->entityManager->persist($newPlace); + $this->entityManager->flush(); + $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]); @@ -347,9 +381,12 @@ final class AdminController extends AbstractController // Données pour le graphique des modifications par trimestre $modificationsByQuarter = []; - foreach ($commerces as $commerce) { - if ($commerce->getOsmDataDate()) { - $date = $commerce->getOsmDataDate(); + if (isset($commerces) && count($commerces) > 0) { + + foreach ($commerces as $commerce) { + if ($commerce->getOsmDataDate()) { + $date = $commerce->getOsmDataDate(); + } $year = $date->format('Y'); $quarter = ceil($date->format('n') / 3); $key = $year . '-Q' . $quarter; @@ -393,10 +430,10 @@ final class AdminController extends AbstractController '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' + . ' (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 != ''") @@ -503,7 +540,7 @@ final class AdminController extends AbstractController } // Tri par date dans chaque série foreach ($ctc_completion_series as &$points) { - usort($points, function($a, $b) { + usort($points, function ($a, $b) { return strcmp($a['date'], $b['date']); }); } @@ -523,7 +560,7 @@ final class AdminController extends AbstractController 'latestFollowups' => $latestFollowups, 'followup_labels' => \App\Service\FollowUpService::getFollowUpThemes(), 'followup_icons' => \App\Service\FollowUpService::getFollowUpIcons(), - 'progression7Days' => $progression7Days, + 'progression7Days' => $progression7Days, 'all_types' => \App\Service\FollowUpService::getFollowUpThemes(), 'getTagEmoji' => [self::class, 'getTagEmoji'], 'completion_tags' => \App\Service\FollowUpService::getFollowUpCompletionTags(), @@ -607,15 +644,15 @@ final class AdminController extends AbstractController // Cas particuliers multi-valeurs (ex: healthcare) if ($theme === 'healthcare') { if ($main_tag && ( - str_starts_with($main_tag, 'healthcare=') || - in_array($main_tag, [ - 'amenity=doctors', - 'amenity=pharmacy', - 'amenity=hospital', - 'amenity=clinic', - 'amenity=social_facility' - ]) - )) { + str_starts_with($main_tag, 'healthcare=') || + in_array($main_tag, [ + 'amenity=doctors', + 'amenity=pharmacy', + 'amenity=hospital', + 'amenity=clinic', + 'amenity=social_facility' + ]) + )) { $match = true; } } else { @@ -714,7 +751,6 @@ final class AdminController extends AbstractController } - /** * rediriger vers l'url unique quand on est admin */ @@ -793,7 +829,8 @@ final class AdminController extends AbstractController $stats->setPopulation((int)$data['population']); } } - } catch (\Exception $e) {} + } catch (\Exception $e) { + } } // Compléter le budget si manquant if (!$stats->getBudgetAnnuel()) { @@ -815,7 +852,8 @@ final class AdminController extends AbstractController $stats->setLat((string)$data['centre']['coordinates'][1]); } } - } catch (\Exception $e) {} + } catch (\Exception $e) { + } } // Mettre à jour la date de requête de labourage $stats->setDateLabourageRequested(new \DateTime()); @@ -846,7 +884,7 @@ final class AdminController extends AbstractController // Toujours générer les CityFollowUp (mais ne jamais les supprimer) // $themes = \App\Service\FollowUpService::getFollowUpThemes(); // foreach (array_keys($themes) as $theme) { - $this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager, true); + $this->followUpService->generateCityFollowUps($stats, $this->motocultrice, $this->entityManager, true); // } $this->entityManager->flush(); return $this->redirectToRoute('app_admin_stats', ['insee_code' => $insee_code]); @@ -966,9 +1004,9 @@ final class AdminController extends AbstractController fclose($handle); - return $response; } + #[Route('/admin/export_csv/{insee_code}', name: 'app_admin_export_csv')] public function export_csv(string $insee_code): Response { @@ -1730,15 +1768,15 @@ final class AdminController extends AbstractController // Cas particuliers multi-valeurs (ex: healthcare) if ($theme === 'healthcare') { if ($main_tag && ( - str_starts_with($main_tag, 'healthcare=') || - in_array($main_tag, [ - 'amenity=doctors', - 'amenity=pharmacy', - 'amenity=hospital', - 'amenity=clinic', - 'amenity=social_facility' - ]) - )) { + str_starts_with($main_tag, 'healthcare=') || + in_array($main_tag, [ + 'amenity=doctors', + 'amenity=pharmacy', + 'amenity=hospital', + 'amenity=clinic', + 'amenity=social_facility' + ]) + )) { $match = true; } } else { @@ -1830,7 +1868,7 @@ final class AdminController extends AbstractController } } foreach ($ctc_completion_series as &$points) { - usort($points, function($a, $b) { + usort($points, function ($a, $b) { return strcmp($a['date'], $b['date']); }); } @@ -1870,7 +1908,7 @@ final class AdminController extends AbstractController foreach ($places as $place) { $rue = $place->getStreet() ?: '(sans nom)'; if (!isset($rues[$rue])) { - $rues[$rue] = [ 'places' => [], 'completion_sum' => 0 ]; + $rues[$rue] = ['places' => [], 'completion_sum' => 0]; } $rues[$rue]['places'][] = $place; $rues[$rue]['completion_sum'] += $place->getCompletionPercentage(); diff --git a/src/Controller/PublicController.php b/src/Controller/PublicController.php index 005d2ca..2ce7fde 100644 --- a/src/Controller/PublicController.php +++ b/src/Controller/PublicController.php @@ -96,10 +96,10 @@ class PublicController extends AbstractController $debug = ''; if ($this->getParameter('kernel.environment') !== 'prod') { $debug = 'Bonjour, nous sommes des bénévoles d\'OpenStreetMap France et nous vous proposons de modifier les informations de votre commerce. Voici votre lien unique de modification: ' . $this->generateUrl('app_public_edit', [ - 'zipcode' => $zipCode, - 'name' => $place_name, - 'uuid' => $place->getUuidForUrl() - ], true); + 'zipcode' => $zipCode, + 'name' => $place_name, + 'uuid' => $place->getUuidForUrl() + ], true); } $this->addFlash('success', 'Un email vous sera envoyé avec le lien de modification. ' . $debug); } @@ -112,10 +112,10 @@ class PublicController extends AbstractController ->to($destinataire) ->subject('Votre lien de modification OpenStreetMap') ->text('Bonjour, nous sommes des bénévoles d\'OpenStreetMap France et nous vous proposons de modifier les informations de votre commerce. Voici votre lien unique de modification: ' . $this->generateUrl('app_public_edit', [ - 'zipcode' => $zipCode, - 'name' => $place_name, - 'uuid' => $existingPlace ? $existingPlace->getUuidForUrl() : $place->getUuidForUrl() - ], true)); + 'zipcode' => $zipCode, + 'name' => $place_name, + 'uuid' => $existingPlace ? $existingPlace->getUuidForUrl() : $place->getUuidForUrl() + ], true)); $this->mailer->send($message); @@ -272,8 +272,8 @@ class PublicController extends AbstractController 'name' => $cityName, 'zone' => $stat->getZone(), 'coordinates' => [ - 'lat' => (float) $stat->getLat(), - 'lon' => (float) $stat->getLon() + 'lat' => (float)$stat->getLat(), + 'lon' => (float)$stat->getLon() ], 'placesCount' => $stat->getPlacesCount(), 'completionPercent' => $stat->getCompletionPercent(), @@ -329,8 +329,8 @@ class PublicController extends AbstractController if (!empty($data) && isset($data[0]['lat']) && isset($data[0]['lon'])) { error_log("DEBUG: Coordonnées trouvées pour $cityName ($inseeCode): " . $data[0]['lat'] . ", " . $data[0]['lon']); return [ - 'lat' => (float) $data[0]['lat'], - 'lon' => (float) $data[0]['lon'] + 'lat' => (float)$data[0]['lat'], + 'lon' => (float)$data[0]['lon'] ]; } else { error_log("DEBUG: Aucune coordonnée trouvée pour $cityName ($inseeCode)"); @@ -1060,22 +1060,61 @@ class PublicController extends AbstractController } + /** + * Calculate marker color based on completion percentage + * Returns a gradient from intense green (high completion) to gray (low completion) with 10 intermediate shades + */ + private function calculateMarkerColor(float $completionPercent): string + { + // Define the colors for the gradient + $greenColor = [0, 170, 0]; // Intense green RGB + $grayColor = [128, 128, 128]; // Gray RGB + + // Ensure completion percent is between 0 and 100 + $completionPercent = max(0, min(100, $completionPercent)); + + // Calculate the position in the gradient (0 to 1) + $position = $completionPercent / 100; + + // Calculate the RGB values for the gradient + $r = intval($grayColor[0] + ($greenColor[0] - $grayColor[0]) * $position); + $g = intval($grayColor[1] + ($greenColor[1] - $grayColor[1]) * $position); + $b = intval($grayColor[2] + ($greenColor[2] - $grayColor[2]) * $position); + + // Convert RGB to hexadecimal + return sprintf('#%02x%02x%02x', $r, $g, $b); + } + #[Route('/cities', name: 'app_public_cities')] public function cities(): Response { - $stats = $this->entityManager->getRepository(Stats::class)->findAll(); + // Only select Stats that have an empty kind or 'user' kind + $stats = $this->entityManager->getRepository(Stats::class) + ->createQueryBuilder('s') + ->where('s.kind IS NULL OR s.kind = :user_kind') + ->setParameter('user_kind', 'user') + ->orderBy('s.name', 'ASC') + ->getQuery() + ->getResult(); // Prepare data for the map $citiesForMap = []; foreach ($stats as $stat) { if ($stat->getZone() !== 'undefined' && preg_match('/^\d+$/', $stat->getZone())) { + // Calculate marker color based on completion percentage + // Gradient from intense green (high completion) to gray (low completion) with 10 intermediate shades + $completionPercent = $stat->getCompletionPercent(); + // Ensure we have a float value even if getCompletionPercent returns null + $markerColor = $this->calculateMarkerColor($completionPercent ?? 0); + $citiesForMap[] = [ 'name' => $stat->getName(), 'zone' => $stat->getZone(), 'lat' => $stat->getLat(), 'lon' => $stat->getLon(), 'placesCount' => $stat->getPlacesCount(), - 'completionPercent' => $stat->getCompletionPercent(), + 'completionPercent' => $completionPercent, + 'markerColor' => $markerColor, ]; } } diff --git a/templates/public/cities.html.twig b/templates/public/cities.html.twig index 11eaa51..511e20f 100644 --- a/templates/public/cities.html.twig +++ b/templates/public/cities.html.twig @@ -11,6 +11,43 @@ width: 100%; margin-bottom: 1rem; } + + .map-legend { + background: white; + padding: 10px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0,0,0,0.1); + position: absolute; + bottom: 20px; + right: 20px; + z-index: 1; + } + + .legend-title { + font-weight: bold; + margin-bottom: 5px; + text-align: center; + } + + .legend-gradient { + display: flex; + justify-content: space-between; + align-items: center; + } + + .legend-item { + display: flex; + flex-direction: column; + align-items: center; + margin: 0 5px; + } + + .legend-color { + width: 20px; + height: 20px; + border-radius: 50%; + margin-bottom: 2px; + } {% endblock %} @@ -37,17 +74,28 @@
-
-
- Complétion > 80% -
-
-
- Complétion 50-80% -
-
-
- Complétion < 50% +
Pourcentage de complétion
+
+
+
+ 0% +
+
+
+ 25% +
+
+
+ 50% +
+
+
+ 75% +
+
+
+ 100% +
@@ -109,13 +157,8 @@ {% if citiesForMap is not empty %} {% for city in citiesForMap %} {% if city.lat and city.lon %} - // Determine marker color based on completion percentage - - {% if city.completionPercent > 80 %} - color = '#28a745'; // Green for high completion - {% elseif city.completionPercent > 50 %} - color = '#17a2b8'; // Blue for medium completion - {% endif %} + // Use the marker color calculated in the controller + color = '{{ city.markerColor }}'; // Create marker and popup new maplibregl.Marker({color: color})