diff --git a/assets/app.js b/assets/app.js index 5e941e38..351dbe6e 100644 --- a/assets/app.js +++ b/assets/app.js @@ -36,9 +36,8 @@ import { toggleCompletionInfo, updateMapHeightForLargeScreens } from './utils.js'; -// import Tablesort from 'tablesort'; -import TableSort from 'table-sort-js/table-sort.js'; -console.log('TableSort', TableSort) +import tableSortJs from 'table-sort-js/table-sort.js'; +console.log('TableSort', tableSortJs) // Charger table-sortable (version non minifiée locale) // import '../assets/js/table-sortable.js'; @@ -203,11 +202,10 @@ document.addEventListener('DOMContentLoaded', () => { adjustListGroupFontSize('.list-group-item'); // Activer le tri naturel sur tous les tableaux avec la classe table-sort - if(TableSort){ - - document.querySelectorAll('table.table-sort')?.forEach(table => { - new TableSort(table); - }); + if (tableSortJs) { + tableSortJs(); + }else{ + console.log('pas de tablesort') } // Initialisation du tri et filtrage sur les tableaux du dashboard et de la page stats diff --git a/assets/styles/app.css b/assets/styles/app.css index d4ac0bc5..f37a0bd3 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -31,6 +31,11 @@ body { color: #df5a0d; } +table { + max-height: 100vh; + max-width: 100vw; +} + table.js-sort-table th { cursor: pointer; } @@ -141,7 +146,7 @@ img { } -#completionHistoryChart{ +#completionHistoryChart { min-height: 500px; } diff --git a/migrations/Version20250626204942.php b/migrations/Version20250626204942.php new file mode 100644 index 00000000..5344d855 --- /dev/null +++ b/migrations/Version20250626204942.php @@ -0,0 +1,35 @@ +addSql(<<<'SQL' + CREATE TABLE action_log (id INT AUTO_INCREMENT NOT NULL, kind VARCHAR(255) NOT NULL, from_url VARCHAR(500) NOT NULL, who VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 + SQL); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql(<<<'SQL' + DROP TABLE action_log + SQL); + } +} diff --git a/migrations/Version20250626205820.php b/migrations/Version20250626205820.php new file mode 100644 index 00000000..512b9ea6 --- /dev/null +++ b/migrations/Version20250626205820.php @@ -0,0 +1,35 @@ +addSql(<<<'SQL' + ALTER TABLE action_log ADD from_url VARCHAR(500) 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 action_log DROP from_url + SQL); + } +} diff --git a/migrations/Version20250626210012.php b/migrations/Version20250626210012.php new file mode 100644 index 00000000..172cebb1 --- /dev/null +++ b/migrations/Version20250626210012.php @@ -0,0 +1,35 @@ +addSql(<<<'SQL' + ALTER TABLE action_log ADD who VARCHAR(255) 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 action_log DROP who + SQL); + } +} diff --git a/package-lock.json b/package-lock.json index 953c42e8..e605a0cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,8 @@ "": { "license": "UNLICENSED", "dependencies": { - "jquery": "^3.7.1" + "jquery": "^3.7.1", + "table-sort": "^1.0.16" }, "devDependencies": { "@babel/core": "^7.17.0", @@ -5501,6 +5502,12 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/table-sort": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/table-sort/-/table-sort-1.0.16.tgz", + "integrity": "sha512-w7TDMfszdFY36aWQKRiAg0qQjOmvIy1IQKplmgpOCimOZ69BP4y5Ne4+jBQeYn990Rn40/wCALR0eAcLqxECWA==", + "license": "ISC" + }, "node_modules/table-sort-js": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/table-sort-js/-/table-sort-js-1.22.2.tgz", diff --git a/package.json b/package.json index 8e5215b2..0d62501b 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "build": "encore production --progress" }, "dependencies": { - "jquery": "^3.7.1" + "jquery": "^3.7.1", + "table-sort": "^1.0.16" } } diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php index 4015981d..ce4152df 100644 --- a/src/Controller/AdminController.php +++ b/src/Controller/AdminController.php @@ -17,6 +17,7 @@ use function uuid_create; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\JsonResponse; use Twig\Environment; +use App\Service\ActionLogger; final class AdminController extends AbstractController { @@ -25,7 +26,8 @@ final class AdminController extends AbstractController private EntityManagerInterface $entityManager, private Motocultrice $motocultrice, private BudgetService $budgetService, - private Environment $twig + private Environment $twig, + private ActionLogger $actionLogger ) { } @@ -145,6 +147,7 @@ final class AdminController extends AbstractController $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 @@ -730,15 +733,9 @@ final class AdminController extends AbstractController // Afficher le log des objets non trouvés à la fin if (!empty($notFoundOsmKeys)) { - return $this->render('admin/labourage_results.html.twig', [ - 'stats' => $stats, - 'zone' => $insee_code, - 'new_places_counter' => $processedCount, - 'commerces' => $commerces, - 'not_found_osm_keys' => $notFoundOsmKeys - ]); + $this->addFlash('info', count($notFoundOsmKeys).' objets OSM non trouvés lors du labourage.'); } - // Sinon, rediriger comme avant + // Rediriger dans tous les cas vers la page de stats de la ville return $this->redirectToRoute('app_admin_stats', ['insee_code' => $insee_code]); } catch (\Exception $e) { $this->addFlash('error', 'Erreur lors du labourage : ' . $e->getMessage()); diff --git a/src/Controller/PublicController.php b/src/Controller/PublicController.php index 0d7474d3..5c887ea0 100644 --- a/src/Controller/PublicController.php +++ b/src/Controller/PublicController.php @@ -13,6 +13,7 @@ use GuzzleHttp\Client; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Mime\Email; use Symfony\Component\Mailer\MailerInterface; +use App\Service\ActionLogger; class PublicController extends AbstractController { @@ -22,19 +23,19 @@ class PublicController extends AbstractController public function __construct( private EntityManagerInterface $entityManager, private Motocultrice $motocultrice, - private MailerInterface $mailer - ) { - } + private MailerInterface $mailer, + private ActionLogger $actionLogger + ) {} #[Route('/propose-email/{email}/{type}/{id}', name: 'app_public_propose_email')] public function proposeEmail(string $email, string $type, int $id): Response { - $data = $this->motocultrice->get_osm_object_data($type, $id); - // Récupérer le code postal depuis les tags, sinon mettre -1 - $zipCode = isset($data['tags_converted']['addr:postcode']) ? (int)$data['tags_converted']['addr:postcode'] : -1; - $place_name = $data['tags_converted']['name']; + $data = $this->motocultrice->get_osm_object_data($type, $id); + // Récupérer le code postal depuis les tags, sinon mettre -1 + $zipCode = isset($data['tags_converted']['addr:postcode']) ? (int)$data['tags_converted']['addr:postcode'] : -1; + $place_name = $data['tags_converted']['name']; // Vérifier si une Place existe déjà avec le même osm_kind et osmId $existingPlace = $this->entityManager->getRepository(Place::class)->findOneBy([ @@ -45,10 +46,10 @@ class PublicController extends AbstractController if ($existingPlace) { // Mettre à jour l'email de la Place existante $existingPlace->setEmail($email)->setLastContactAttemptDate(new \DateTime()); - if($zipCode != -1) { + if ($zipCode != -1) { $existingPlace->setZipCode($zipCode); } - $this->entityManager->persist($existingPlace); + $this->entityManager->persist($existingPlace); $this->entityManager->flush(); $debug = ''; @@ -59,28 +60,28 @@ class PublicController extends AbstractController 'uuid' => $existingPlace->getUuidForUrl() ], true); } - - $this->addFlash('success', 'L\'email a été mis à jour. Un email vous sera envoyé avec le lien de modification. '.$debug); + + $this->addFlash('success', 'L\'email a été mis à jour. Un email vous sera envoyé avec le lien de modification. ' . $debug); } else { // Créer une nouvelle entité Place $place = new Place(); $place->setEmail($email) - ->setOsmId($id) - ->setOsmKind($type) - ->setAskedHumainsSupport(false) - ->setOptedOut(false) - ->setDead(false) - ->setNote('') - ->setModifiedDate(new \DateTime()) - ->setZipCode($zipCode) - ->setPlaceCount(0) - ->setMainTag($this->motocultrice->find_main_tag($data['tags_converted']) ?? '') - ->setStreet($this->motocultrice->find_street($data['tags_converted']) ?? '') - ->setHousenumber($this->motocultrice->find_housenumber($data['tags_converted']) ?? '') - ->setLastContactAttemptDate(new \DateTime()) - ->setUuidForUrl(uniqid()); + ->setOsmId($id) + ->setOsmKind($type) + ->setAskedHumainsSupport(false) + ->setOptedOut(false) + ->setDead(false) + ->setNote('') + ->setModifiedDate(new \DateTime()) + ->setZipCode($zipCode) + ->setPlaceCount(0) + ->setMainTag($this->motocultrice->find_main_tag($data['tags_converted']) ?? '') + ->setStreet($this->motocultrice->find_street($data['tags_converted']) ?? '') + ->setHousenumber($this->motocultrice->find_housenumber($data['tags_converted']) ?? '') + ->setLastContactAttemptDate(new \DateTime()) + ->setUuidForUrl(uniqid()); $this->entityManager->persist($place); $this->entityManager->flush(); @@ -93,7 +94,7 @@ class PublicController extends AbstractController 'uuid' => $place->getUuidForUrl() ], true); } - $this->addFlash('success', 'Un email vous sera envoyé avec le lien de modification. '.$debug); + $this->addFlash('success', 'Un email vous sera envoyé avec le lien de modification. ' . $debug); } // Envoyer l'email @@ -118,7 +119,7 @@ class PublicController extends AbstractController public function index(): Response { $stats = $this->entityManager->getRepository(Stats::class)->findAll(); - + return $this->render('public/home.html.twig', [ 'controller_name' => 'PublicController', 'stats' => $stats @@ -128,9 +129,15 @@ class PublicController extends AbstractController #[Route('/edit/{zipcode}/{name}/{uuid}', name: 'app_public_edit')] public function edit_with_uuid($zipcode, $name, $uuid): Response { + $this->actionLogger->log('dashboard', [ + 'zipcode' => $zipcode, + + + ]); + $place = $this->entityManager->getRepository(Place::class)->findOneBy(['uuid_for_url' => $uuid]); if (!$place) { - $this->addFlash('warning', 'Ce lien de modification n\'existe pas.'.$uuid); + $this->addFlash('warning', 'Ce lien de modification n\'existe pas.' . $uuid); return $this->redirectToRoute('app_public_index'); } @@ -142,19 +149,19 @@ class PublicController extends AbstractController // 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']); - + + $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']); $place->setDisplayedDate(new \DateTime()); $this->entityManager->persist($place); $this->entityManager->flush(); - + return $this->render('public/edit.html.twig', [ 'commerce_overpass' => $commerce_overpass, 'name' => $name, @@ -172,7 +179,9 @@ class PublicController extends AbstractController #[Route('/dashboard', name: 'app_public_dashboard')] public function dashboard(): Response { - + + $this->actionLogger->log('dashboard', []); + $stats_repo = $this->entityManager->getRepository(Stats::class)->findAll(); $stats_for_chart = []; @@ -204,6 +213,12 @@ class PublicController extends AbstractController #[Route('/modify/{osm_object_id}/{version}/{changesetID}', name: 'app_public_submit')] public function submit($osm_object_id, $version, $changesetID): Response { + + $this->actionLogger->log('submit_object', [ + 'osm_id' => $osm_object_id, + 'version' => $version, + 'changesetID' => $changesetID + ]); $place = $this->entityManager->getRepository(Place::class)->findOneBy(['osmId' => $osm_object_id]); if (!$place) { $this->addFlash('warning', 'Ce commerce n\'existe pas.'); @@ -212,19 +227,19 @@ class PublicController extends AbstractController // Récupérer les données POST $request = Request::createFromGlobals(); - + // Vérifier si des données ont été soumises if ($request->isMethod('POST')) { $status = "non modifié"; - + // Récupérer le type d'objet (node ou way) $osm_kind = $request->request->get('osm_kind', 'node'); - + // Récupérer tous les tags du formulaire $tags = []; $request_post = $request->request->all(); - + $request_post = $this->motocultrice->map_post_values($request_post); foreach ($request_post as $key => $value) { @@ -258,7 +273,7 @@ class PublicController extends AbstractController $tag = $changeset->addChild('tag'); $tag->addAttribute('k', 'created_by'); $tag->addAttribute('v', 'OSM Mon Commerce Web Editor'); - + $tag = $changeset->addChild('tag'); $tag->addAttribute('k', 'comment'); $tag->addAttribute('v', 'Modification dans #MonCommerceOSM'); @@ -275,14 +290,14 @@ class PublicController extends AbstractController // Récupérer les données actuelles de l'objet $currentObjectData = $this->motocultrice->get_osm_object_data($osm_kind, $osm_object_id); - + // 2. Modifier l'objet avec le nouveau changeset $xml = new \SimpleXMLElement(''); $object = $xml->addChild($osm_kind); $object->addAttribute('id', $osm_object_id); $object->addAttribute('version', $version); $object->addAttribute('changeset', $newChangesetId); - + // Ajouter les coordonnées pour les nodes if ($osm_kind === 'node') { if (!isset($currentObjectData['@attributes']['lat']) || !isset($currentObjectData['@attributes']['lon'])) { @@ -291,7 +306,7 @@ class PublicController extends AbstractController $object->addAttribute('lat', $currentObjectData['@attributes']['lat']); $object->addAttribute('lon', $currentObjectData['@attributes']['lon']); } - + // Ajouter les tags foreach ($tags as $key => $value) { if (!empty($key) && !empty($value)) { @@ -323,18 +338,26 @@ class PublicController extends AbstractController $status = "Les tags ont été mis à jour avec succès"; } else { $status = "Erreur lors de la mise à jour des tags"; + $this->actionLogger->log('ERROR_submit_object', [ + 'osm_id' => $osm_object_id, + 'version' => $version, + 'changesetID' => $changesetID, + 'body_sent' => $xmlString, + 'response' => $response->getBody()->getContents(), + ]); } } 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()); } } } - + // après envoi on récupère les données $commerce_overpass = $this->motocultrice->get_osm_object_data($osm_kind, $osm_object_id); @@ -344,14 +367,14 @@ class PublicController extends AbstractController $this->entityManager->clear(); $stats = $place->getStats(); - if(!$stats) { + if (!$stats) { $stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zip_code' => $place->getZipCode()]); } - if(!$stats) { + if (!$stats) { $stats = new Stats(); $stats->setZipCode($place->getZipCode()); } - + $stats->addPlace($place); $place->setStats($stats); $place->setModifiedDate(new \DateTime()); @@ -379,23 +402,31 @@ class PublicController extends AbstractController #[Route('/request_email_to_modify/{osm_object_id}', name: 'app_public_request_email')] public function request_email($osm_object_id): Response { + $this->actionLogger->log('request_email_to_modify', [ + 'osm_id' => $osm_object_id, + ]); + if ($this->getRequest()->isMethod('POST')) { $email = $this->getRequest()->request->get('email'); - + try { // TODO: Implémenter l'envoi réel du mail - + $this->addFlash( 'success', 'Un email vous a été envoyé avec les instructions pour modifier ce lieu.' ); } catch (\Exception $e) { + $this->actionLogger->log('ERROR_request_email_to_modify', [ + 'osm_id' => $osm_object_id, + 'exception_message' => $e->getMessage(), + ]); $this->addFlash( - 'error', + 'error', 'Une erreur est survenue lors de l\'envoi de l\'email. Veuillez réessayer plus tard.' ); } - + return $this->redirectToRoute('app_public_index'); } // TODO envoyer un email @@ -409,6 +440,9 @@ class PublicController extends AbstractController #[Route('/closed_commerce/{osm_object_id}', name: 'app_public_closed_commerce')] public function closed_commerce($osm_object_id): Response { + $this->actionLogger->log('closed_commerce', [ + 'osm_id' => $osm_object_id, + ]); $place = $this->entityManager->getRepository(Place::class)->findOneBy(['osm_id' => $osm_object_id]); if (!$place) { $this->addFlash('warning', 'Ce commerce n\'existe pas.'); @@ -417,7 +451,7 @@ class PublicController extends AbstractController $place->setClosed(true); $this->entityManager->flush(); - + return $this->render('public/closed_commerce.html.twig', [ 'controller_name' => 'PublicController', ]); @@ -474,12 +508,33 @@ class PublicController extends AbstractController } #[Route('/set_opted_out_place/{uuid}', name: 'app_public_set_opted_out_place')] - public function set_opted_out_place($uuid): Response + public function set_opted_out_place($uuid) { $place = $this->entityManager->getRepository(Place::class)->findOneBy(['uuid_for_url' => $uuid]); + $this->actionLogger->log('set_place_opted_out', [ + 'uuid' => $uuid, + ]); + if (!$place) { $this->addFlash('warning', 'Ce commerce n\'existe pas.'); return $this->redirectToRoute('app_public_index'); } } -} \ No newline at end of file + + #[Route('/ask-for-help', name: 'app_public_ask_for_help')] + public function askForHelp(Request $request): Response + { + + $this->actionLogger->log('ask_for_help', []); + return $this->redirect('https://www.openstreetmap.fr/contact'); + } + + #[Route('/logs/actions', name: 'app_public_action_logs')] + public function listActionLogs(): Response + { + $logs = $this->actionLogger->getLastLogs(100); + return $this->render('public/action_logs.html.twig', [ + 'logs' => $logs + ]); + } +} diff --git a/src/Entity/ActionLog.php b/src/Entity/ActionLog.php new file mode 100644 index 00000000..10fd580c --- /dev/null +++ b/src/Entity/ActionLog.php @@ -0,0 +1,91 @@ +id; + } + + public function getType(): ?string + { + return $this->type; + } + + public function setType(string $type): self + { + $this->type = $type; + return $this; + } + + public function getData(): ?array + { + return $this->data; + } + + public function setData(?array $data): self + { + $this->data = $data; + return $this; + } + + public function getCreatedAt(): ?\DateTimeInterface + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeInterface $createdAt): self + { + $this->createdAt = $createdAt; + return $this; + } + + public function getFromUrl(): ?string + { + return $this->from_url; + } + + public function setFromUrl(?string $from_url): static + { + $this->from_url = $from_url; + + return $this; + } + + public function getWho(): ?string + { + return $this->who; + } + + public function setWho(?string $who): static + { + $this->who = $who; + + return $this; + } +} diff --git a/src/Entity/Stats.php b/src/Entity/Stats.php index a9c09e2d..d52cede3 100644 --- a/src/Entity/Stats.php +++ b/src/Entity/Stats.php @@ -196,7 +196,9 @@ class Stats $this->avec_site = 0; $this->avec_accessibilite = 0; $this->avec_note = 0; - + $this->avec_siret = 0; + $this->avec_name = 0; + $somme_completions = 0; @@ -219,10 +221,13 @@ class Stats $this->avec_horaires++; $place_completions++; } - if($place->hasNote()) { - $this->avec_note++; + if($place->getSiret()) { + $this->avec_siret++; } - $somme_completions += $place_completions / 5; + if($place->getName()) { + $this->avec_name++; + } + $somme_completions += $place_completions / 6; } $this->setPlacesCount($places_count); diff --git a/src/Repository/ActionLogRepository.php b/src/Repository/ActionLogRepository.php new file mode 100644 index 00000000..b3c48da6 --- /dev/null +++ b/src/Repository/ActionLogRepository.php @@ -0,0 +1,43 @@ + + */ +class ActionLogRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, ActionLog::class); + } + + // /** + // * @return ActionLog[] Returns an array of ActionLog objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('a') + // ->andWhere('a.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('a.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?ActionLog + // { + // return $this->createQueryBuilder('a') + // ->andWhere('a.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Service/ActionLogger.php b/src/Service/ActionLogger.php new file mode 100644 index 00000000..cd5a4961 --- /dev/null +++ b/src/Service/ActionLogger.php @@ -0,0 +1,48 @@ +entityManager = $entityManager; + } + + public function log(string $type, array $data = []): void + { + $log = new ActionLog(); + $log->setType($type); + $log->setData($data); + $log->setCreatedAt(new \DateTime()); + + if (isset($data['from_url'])) { + $log->setFromUrl($data['from_url']); + } else if (isset($_SERVER['HTTP_REFERER'])) { + $log->setFromUrl($_SERVER['HTTP_REFERER']); + } + if (isset($data['who'])) { + $log->setWho($data['who']); + } + $this->entityManager->persist($log); + $this->entityManager->flush(); + } + + /** + * @return ActionLog[] + */ + public function getLastLogs(int $limit = 100): array + { + return $this->entityManager->getRepository(ActionLog::class) + ->createQueryBuilder('a') + ->orderBy('a.createdAt', 'DESC') + ->setMaxResults($limit) + ->getQuery() + ->getResult(); + } +} diff --git a/templates/admin/fraicheur_histogramme.html.twig b/templates/admin/fraicheur_histogramme.html.twig index 069b8633..924de5b8 100644 --- a/templates/admin/fraicheur_histogramme.html.twig +++ b/templates/admin/fraicheur_histogramme.html.twig @@ -80,7 +80,7 @@ document.addEventListener('DOMContentLoaded', function() { }, options: { responsive: true, - maintainAspectRatio: true, + plugins: { legend: { display: false }, title: { display: true, text: 'Fraîcheur des données OSM (par mois)' } @@ -126,7 +126,7 @@ document.addEventListener('DOMContentLoaded', function() { }, options: { responsive: true, - maintainAspectRatio: true, + plugins: { legend: { display: false }, title: { display: true, text: 'Fraîcheur des données OSM (par trimestre)' } @@ -166,7 +166,7 @@ document.addEventListener('DOMContentLoaded', function() { }, options: { responsive: true, - maintainAspectRatio: true, + plugins: { legend: { display: false }, title: { display: true, text: 'Fraîcheur des données OSM (par année)' } @@ -213,7 +213,7 @@ document.addEventListener('DOMContentLoaded', function() { }, options: { responsive: true, - maintainAspectRatio: true, + plugins: { legend: { display: false }, title: { display: true, text: "Distribution des villes par habitants/lieu (par pas de 10)" }, @@ -258,7 +258,7 @@ document.addEventListener('DOMContentLoaded', function() { }, options: { responsive: true, - maintainAspectRatio: true, + plugins: { legend: { display: false }, title: { display: true, text: "Distribution des villes par lieux/habitant (par pas de 0,01)" }, @@ -319,7 +319,7 @@ document.addEventListener('DOMContentLoaded', function() { }, options: { responsive: true, - maintainAspectRatio: true, + plugins: { legend: { display: false }, title: { display: true, text: "Distribution des villes par budget par habitant (par pas de 100€)" }, @@ -365,7 +365,7 @@ document.addEventListener('DOMContentLoaded', function() { }, options: { responsive: true, - maintainAspectRatio: true, + plugins: { legend: { display: false }, title: { display: true, text: "Distribution des villes par écart à la moyenne du budget par habitant (par pas de 10%)" }, @@ -411,7 +411,7 @@ document.addEventListener('DOMContentLoaded', function() { }, options: { responsive: true, - maintainAspectRatio: true, + plugins: { legend: { display: false }, title: { display: true, text: "Distribution des villes par budget par lieu (par pas de 5000€)" }, diff --git a/templates/admin/labourage_results.html.twig b/templates/admin/labourage_results.html.twig index a3bc8650..c8e7c5e9 100644 --- a/templates/admin/labourage_results.html.twig +++ b/templates/admin/labourage_results.html.twig @@ -34,5 +34,14 @@ commerces existants déjà en base: {{ commerces|length }} {% endfor %} + +{% if redirect_to_stats %} + +
Vous allez être redirigé automatiquement vers la page de statistiques de la ville dans 5 secondes.
+{% endif %} {% endblock %} diff --git a/templates/admin/podium_contributeurs_osm.html.twig b/templates/admin/podium_contributeurs_osm.html.twig index b0174bde..bcfd6645 100644 --- a/templates/admin/podium_contributeurs_osm.html.twig +++ b/templates/admin/podium_contributeurs_osm.html.twig @@ -43,6 +43,7 @@ {% endif %} + {% if row.completion_pondere_normalisee is not null %} {{ row.completion_pondere_normalisee }} {% else %} diff --git a/templates/admin/stats.html.twig b/templates/admin/stats.html.twig index c67866fb..29c21d20 100644 --- a/templates/admin/stats.html.twig +++ b/templates/admin/stats.html.twig @@ -101,72 +101,7 @@ {% endif %} {% 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 %} #} +
@@ -258,9 +193,8 @@
- {% include 'admin/stats_history.html.twig' %} -
-
+ {% include 'admin/stats_history.html.twig' with {stat: stats} %} +
@@ -603,8 +537,7 @@ }] }, options: { - responsive: true, - maintainAspectRatio: true, + responsive: true, plugins: { legend: { position: 'right', @@ -644,7 +577,7 @@ if(dc ){ return ; } new Chart(completionCtx, { - type: 'bar', + type: 'line', data: { labels: completionLabels, datasets: [{ @@ -661,59 +594,13 @@ if(dc ){ beginAtZero: true } }, - responsive: true, - maintainAspectRatio: false + responsive: true, } }); }else{ console.log('pas de distribution_completion') } - -// === Distribution du budget par habitant (écart à la moyenne, pas de 3%) === -const budgetEcartData = []; -{% for commerce in stats.places %} - {% if commerce.getBudgetParHabitantEcartPourcent is defined %} - budgetEcartData.push({{ commerce.getBudgetParHabitantEcartPourcent()|default(0) }}); - {% endif %} -{% endfor %} -const budgetEcartDistribution = {}; -budgetEcartData.forEach(ecart => { - const range = Math.floor(ecart / 3) * 3; - const key = `${range}% à ${range + 3}%`; - budgetEcartDistribution[key] = (budgetEcartDistribution[key] || 0) + 1; -}); -const budgetEcartLabels = Object.keys(budgetEcartDistribution).sort((a, b) => parseInt(a) - parseInt(b)); -const budgetEcartValues = budgetEcartLabels.map(label => budgetEcartDistribution[label]); -const dbh = document.getElementById('distribution_budget_habitant'); -if(dbh){ - const budgetEcartCtx = dbh.getContext ? dbh.getContext('2d') : null; - if(!budgetEcartCtx){ - console.log('pas de budgetEcartCtx'); - } else { - new Chart(budgetEcartCtx, { - type: 'bar', - data: { - labels: budgetEcartLabels, - datasets: [{ - label: 'Distribution des villes selon l\'écart à la moyenne du budget par habitant (pas 3%)', - data: budgetEcartValues, - backgroundColor: 'rgba(255, 206, 86, 0.5)', - borderColor: 'rgba(255, 206, 86, 1)', - borderWidth: 1 - }] - }, - options: { - scales: { - y: { - beginAtZero: true - } - }, - responsive: true, - maintainAspectRatio: false - } - }); - } -} + }); #} - +