up command and labourage

This commit is contained in:
Tykayn 2025-08-08 18:51:44 +02:00 committed by tykayn
parent 8cfea30fdf
commit a81112a018
5 changed files with 279 additions and 86 deletions

View file

@ -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

View file

@ -0,0 +1,67 @@
<?php
namespace App\Command;
use App\Entity\Stats;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:update-empty-stats-kind',
description: 'Change les Stats créés avant aujourd\'hui avec une kind vide pour leur mettre "user" en kind'
)]
class UpdateEmptyStatsKindCommand extends Command
{
public function __construct(
private EntityManagerInterface $entityManager
)
{
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->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;
}
}

View file

@ -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();

View file

@ -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,
];
}
}

View file

@ -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;
}
</style>
{% endblock %}
@ -37,17 +74,28 @@
<div class="map-container">
<div id="citiesMap"></div>
<div class="map-legend">
<div class="legend-item">
<div class="legend-color" style="background-color: #28a745;"></div>
<span>Complétion > 80%</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #17a2b8;"></div>
<span>Complétion 50-80%</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #ffc107;"></div>
<span>Complétion < 50%</span>
<div class="legend-title">Pourcentage de complétion</div>
<div class="legend-gradient">
<div class="legend-item">
<div class="legend-color" style="background-color: #808080;"></div>
<span>0%</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #6a9a40;"></div>
<span>25%</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #55b400;"></div>
<span>50%</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #40ce00;"></div>
<span>75%</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #00aa00;"></div>
<span>100%</span>
</div>
</div>
</div>
</div>
@ -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})