mirror of
https://forge.chapril.org/tykayn/osm-commerces
synced 2025-06-20 01:44:42 +02:00
Compare commits
3 commits
918527e15e
...
31b2bd4689
Author | SHA1 | Date | |
---|---|---|---|
![]() |
31b2bd4689 | ||
![]() |
b61fa6a287 | ||
![]() |
7fb0c9c8c2 |
23 changed files with 1181 additions and 332 deletions
0
assets/js/map-utils.js
Normal file
0
assets/js/map-utils.js
Normal file
|
@ -20,7 +20,11 @@ body {
|
|||
}
|
||||
|
||||
.filled {
|
||||
background-color: #b0dfa0;
|
||||
background-color: #b0dfa0 !important;
|
||||
}
|
||||
|
||||
.filled:hover {
|
||||
background-color: #8abb7a !important;
|
||||
}
|
||||
|
||||
.no-name {
|
||||
|
@ -144,4 +148,9 @@ img {
|
|||
.mb-3 {
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
table tbody {
|
||||
max-height: 700px;
|
||||
overflow: auto;
|
||||
}
|
41
migrations/Version20250617154118.php
Normal file
41
migrations/Version20250617154118.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250617154118 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats ADD siren SMALLINT DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats ADD code_epci SMALLINT 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 stats DROP siren
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats DROP code_epci
|
||||
SQL);
|
||||
}
|
||||
}
|
35
migrations/Version20250617154309.php
Normal file
35
migrations/Version20250617154309.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250617154309 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats ADD codes_postaux 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 stats DROP codes_postaux
|
||||
SQL);
|
||||
}
|
||||
}
|
44
migrations/Version20250617160626.php
Normal file
44
migrations/Version20250617160626.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250617160626 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE stats_history (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, places_count INT DEFAULT NULL, emails_count INT DEFAULT NULL, completion_percent REAL DEFAULT NULL, emails_sent INT DEFAULT NULL, stats_id INT DEFAULT NULL, PRIMARY KEY(id))
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_BE00311670AA3482 ON stats_history (stats_id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats_history ADD CONSTRAINT FK_BE00311670AA3482 FOREIGN KEY (stats_id) REFERENCES stats (id) NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||
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_history DROP CONSTRAINT FK_BE00311670AA3482
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE stats_history
|
||||
SQL);
|
||||
}
|
||||
}
|
35
migrations/Version20250617161207.php
Normal file
35
migrations/Version20250617161207.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250617161207 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats ALTER zone TYPE BIGINT
|
||||
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 ALTER zone TYPE VARCHAR(255)
|
||||
SQL);
|
||||
}
|
||||
}
|
59
migrations/Version20250617165738.php
Normal file
59
migrations/Version20250617165738.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250617165738 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats_history ADD names_count INT DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats_history ADD opening_hours_count INT DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats_history ADD website_count INT DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats_history ADD address_count INT DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats_history ADD siret_count INT 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 stats_history DROP names_count
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats_history DROP opening_hours_count
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats_history DROP website_count
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats_history DROP address_count
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE stats_history DROP siret_count
|
||||
SQL);
|
||||
}
|
||||
}
|
13
public/assets/img/Panoramax.svg
Normal file
13
public/assets/img/Panoramax.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 19 KiB |
|
@ -5,7 +5,7 @@
|
|||
* @param {string} suggestionListId - ID de la liste des suggestions
|
||||
* @param {Function} onSelect - Callback appelé lors de la sélection d'une ville
|
||||
*/
|
||||
export function setupCitySearch(inputId, suggestionListId, onSelect) {
|
||||
function setupCitySearch(inputId, suggestionListId, onSelect) {
|
||||
const searchInput = document.getElementById(inputId);
|
||||
const suggestionList = document.getElementById(suggestionListId);
|
||||
|
||||
|
@ -75,12 +75,12 @@ export function setupCitySearch(inputId, suggestionListId, onSelect) {
|
|||
* @param {string} zipCode - Le code postal
|
||||
* @returns {string} L'URL de labourage
|
||||
*/
|
||||
export function getLabourerUrl(zipCode) {
|
||||
function getLabourerUrl(zipCode) {
|
||||
return `/admin/labourer/${zipCode}`;
|
||||
}
|
||||
|
||||
// Fonction pour gérer la soumission du formulaire d'ajout de ville
|
||||
export function handleAddCityFormSubmit(event) {
|
||||
function handleAddCityFormSubmit(event) {
|
||||
event.preventDefault();
|
||||
const form = event.target;
|
||||
const submitButton = form.querySelector('button[type="submit"]');
|
||||
|
@ -101,7 +101,7 @@ export function handleAddCityFormSubmit(event) {
|
|||
* @param {string} selector - Le sélecteur CSS pour cibler les cellules à colorer
|
||||
* @param {string} color - La couleur de base en format RGB (ex: '154, 205, 50')
|
||||
*/
|
||||
export function colorizePercentageCells(selector, color = '154, 205, 50') {
|
||||
function colorizePercentageCells(selector, color = '154, 205, 50') {
|
||||
document.querySelectorAll(selector).forEach(cell => {
|
||||
const percentage = parseInt(cell.textContent);
|
||||
if (!isNaN(percentage)) {
|
||||
|
@ -116,7 +116,7 @@ export function colorizePercentageCells(selector, color = '154, 205, 50') {
|
|||
* @param {string} selector - Le sélecteur CSS pour cibler les cellules à colorer
|
||||
* @param {string} color - La couleur de base en format RGB (ex: '154, 205, 50')
|
||||
*/
|
||||
export function colorizePercentageCellsRelative(selector, color = '154, 205, 50') {
|
||||
function colorizePercentageCellsRelative(selector, color = '154, 205, 50') {
|
||||
// Récupérer toutes les cellules
|
||||
const cells = document.querySelectorAll(selector);
|
||||
|
||||
|
@ -145,7 +145,7 @@ export function colorizePercentageCellsRelative(selector, color = '154, 205, 50'
|
|||
* @param {number} [minFont=0.8] - Taille de police minimale en rem
|
||||
* @param {number} [maxFont=1.2] - Taille de police maximale en rem
|
||||
*/
|
||||
export function adjustListGroupFontSize(selector, minFont = 0.8, maxFont = 1.2) {
|
||||
function adjustListGroupFontSize(selector, minFont = 0.8, maxFont = 1.2) {
|
||||
const items = document.querySelectorAll(selector);
|
||||
const count = items.length;
|
||||
let fontSize = maxFont;
|
||||
|
|
|
@ -8,6 +8,7 @@ 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 Doctrine\ORM\EntityManagerInterface;
|
||||
use function uuid_create;
|
||||
|
@ -30,18 +31,29 @@ final class AdminController extends AbstractController
|
|||
]);
|
||||
}
|
||||
|
||||
#[Route('/admin/stats/{zip_code}', name: 'app_admin_stats')]
|
||||
public function calculer_stats(string $zip_code): Response
|
||||
#[Route('/admin/stats/{insee_code}', name: 'app_admin_stats')]
|
||||
public function calculer_stats(string $insee_code): Response
|
||||
{
|
||||
// Récupérer tous les commerces de la zone
|
||||
$commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $zip_code]);
|
||||
$commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]);
|
||||
|
||||
// Récupérer les stats existantes pour la zone
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $zip_code]);
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
||||
|
||||
|
||||
$statsHistory = $this->entityManager->getRepository(StatsHistory::class)
|
||||
->createQueryBuilder('sh')
|
||||
->where('sh.stats = :stats')
|
||||
->setParameter('stats', $stats)
|
||||
->orderBy('sh.id', 'DESC')
|
||||
->setMaxResults(365)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
|
||||
if(!$stats) {
|
||||
$stats = new Stats();
|
||||
$stats->setZone($zip_code);
|
||||
$stats->setZone($insee_code);
|
||||
}
|
||||
|
||||
// Calculer les statistiques
|
||||
|
@ -72,11 +84,12 @@ final class AdminController extends AbstractController
|
|||
|
||||
return $this->render('admin/stats.html.twig', [
|
||||
'stats' => $stats,
|
||||
'zip_code' => $zip_code,
|
||||
'query_places' => $this->motocultrice->get_query_places($zip_code),
|
||||
'insee_code' => $insee_code,
|
||||
'query_places' => $this->motocultrice->get_query_places($insee_code),
|
||||
'counters' => $calculatedStats['counters'],
|
||||
'maptiler_token' => $_ENV['MAPTILER_TOKEN'],
|
||||
'mapbox_token' => $_ENV['MAPBOX_TOKEN'],
|
||||
'statsHistory' => $statsHistory,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -122,19 +135,21 @@ final class AdminController extends AbstractController
|
|||
|
||||
|
||||
/**
|
||||
* récupérer les commerces de la zone, créer les nouveaux lieux, et mettre à jour les existants
|
||||
* récupérer les commerces de la zone selon le code INSEE, créer les nouveaux lieux, et mettre à jour les existants
|
||||
*/
|
||||
#[Route('/admin/labourer/{zip_code}', name: 'app_admin_labourer')]
|
||||
public function labourer(string $zip_code, bool $updateExisting = true): Response
|
||||
#[Route('/admin/labourer/{insee_code}', name: 'app_admin_labourer')]
|
||||
public function labourer(string $insee_code, bool $updateExisting = true): Response
|
||||
{
|
||||
try {
|
||||
// Récupérer ou créer les stats pour cette zone
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $zip_code]);
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
||||
|
||||
$city = $this->motocultrice->get_city_osm_from_zip_code($zip_code);
|
||||
$city = $this->motocultrice->get_city_osm_from_zip_code($insee_code);
|
||||
if (!$stats) {
|
||||
$stats = new Stats();
|
||||
$stats->setZone($zip_code)
|
||||
$stats->setDateCreated(new \DateTime());
|
||||
$stats->setDateModified(new \DateTime());
|
||||
$stats->setZone($insee_code)
|
||||
->setPlacesCount(0)
|
||||
->setAvecHoraires(0)
|
||||
->setAvecAdresse(0)
|
||||
|
@ -150,7 +165,7 @@ final class AdminController extends AbstractController
|
|||
// Récupérer la population via l'API
|
||||
$population = null;
|
||||
try {
|
||||
$apiUrl = 'https://geo.api.gouv.fr/communes/' . $zip_code . '?fields=population';
|
||||
$apiUrl = 'https://geo.api.gouv.fr/communes/' . $insee_code;
|
||||
$response = file_get_contents($apiUrl);
|
||||
if ($response !== false) {
|
||||
$data = json_decode($response, true);
|
||||
|
@ -158,13 +173,23 @@ final class AdminController extends AbstractController
|
|||
$population = (int)$data['population'];
|
||||
$stats->setPopulation($population);
|
||||
}
|
||||
if (isset($data['siren'])) {
|
||||
$stats->setSiren((int)$data['siren']);
|
||||
}
|
||||
if (isset($data['codeEpci'])) {
|
||||
$stats->setCodeEpci((int)$data['codeEpci']);
|
||||
}
|
||||
if (isset($data['codesPostaux'])) {
|
||||
$stats->setCodesPostaux(implode(';', $data['codesPostaux']));
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Ne rien faire si l'API échoue
|
||||
$this->addFlash('error', 'Erreur lors de la récupération des données de l\'API : ' . $e->getMessage());
|
||||
|
||||
}
|
||||
|
||||
// Récupérer toutes les données
|
||||
$places = $this->motocultrice->labourer($zip_code);
|
||||
$places = $this->motocultrice->labourer($insee_code);
|
||||
$processedCount = 0;
|
||||
$updatedCount = 0;
|
||||
|
||||
|
@ -177,7 +202,7 @@ final class AdminController extends AbstractController
|
|||
$place = new Place();
|
||||
$place->setOsmId($placeData['id'])
|
||||
->setOsmKind($placeData['type'])
|
||||
->setZipCode($zip_code)
|
||||
->setZipCode($insee_code)
|
||||
->setUuidForUrl($this->motocultrice->uuid_create())
|
||||
->setModifiedDate(new \DateTime())
|
||||
->setStats($stats)
|
||||
|
@ -212,7 +237,30 @@ final class AdminController extends AbstractController
|
|||
// Mettre à jour les statistiques finales
|
||||
$stats->computeCompletionPercent();
|
||||
|
||||
if($stats->getDateCreated() == null) {
|
||||
$stats->setDateCreated(new \DateTime());
|
||||
}
|
||||
|
||||
$stats->setDateModified(new \DateTime());
|
||||
|
||||
// Créer un historique des statistiques
|
||||
$statsHistory = new StatsHistory();
|
||||
$statsHistory->setDate(new \DateTime())
|
||||
->setStats($stats);
|
||||
|
||||
$statsHistory->setPlacesCount($stats->getPlaces()->count())
|
||||
->setOpeningHoursCount($stats->getAvecHoraires())
|
||||
->setAddressCount($stats->getAvecAdresse())
|
||||
->setWebsiteCount($stats->getAvecSite())
|
||||
->setSiretCount($stats->getAvecSiret())
|
||||
// ->setAccessibiliteCount($stats->getAvecAccessibilite())
|
||||
// ->setNoteCount($stats->getAvecNote())
|
||||
->setCompletionPercent($stats->getCompletionPercent())
|
||||
->setStats($stats);
|
||||
|
||||
$this->entityManager->persist($statsHistory);
|
||||
|
||||
|
||||
$this->entityManager->persist($stats);
|
||||
$this->entityManager->flush();
|
||||
|
||||
|
@ -223,9 +271,11 @@ final class AdminController extends AbstractController
|
|||
$this->addFlash('success', $message);
|
||||
} catch (\Exception $e) {
|
||||
$this->addFlash('error', 'Erreur lors du labourage : ' . $e->getMessage());
|
||||
die(var_dump($e));
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_admin_stats', ['zip_code' => $zip_code]);
|
||||
// return $this->redirectToRoute('app_public_dashboard');
|
||||
return $this->redirectToRoute('app_admin_stats', ['insee_code' => $insee_code]);
|
||||
}
|
||||
|
||||
#[Route('/admin/delete/{id}', name: 'app_admin_delete')]
|
||||
|
@ -244,11 +294,11 @@ final class AdminController extends AbstractController
|
|||
return $this->redirectToRoute('app_public_dashboard');
|
||||
}
|
||||
|
||||
#[Route('/admin/delete_by_zone/{zip_code}', name: 'app_admin_delete_by_zone')]
|
||||
public function delete_by_zone(string $zip_code): Response
|
||||
#[Route('/admin/delete_by_zone/{insee_code}', name: 'app_admin_delete_by_zone')]
|
||||
public function delete_by_zone(string $insee_code): Response
|
||||
{
|
||||
$commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $zip_code]);
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $zip_code]);
|
||||
$commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]);
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
||||
|
||||
foreach ($commerces as $commerce) {
|
||||
$this->entityManager->remove($commerce);
|
||||
|
@ -256,7 +306,7 @@ final class AdminController extends AbstractController
|
|||
$this->entityManager->remove($stats);
|
||||
$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.');
|
||||
$this->addFlash('success', 'Tous les commerces de la zone '.$insee_code.' ont été supprimés avec succès de OSM Mes commerces, mais pas dans OpenStreetMap.');
|
||||
|
||||
return $this->redirectToRoute('app_public_dashboard');
|
||||
}
|
||||
|
@ -322,16 +372,16 @@ final class AdminController extends AbstractController
|
|||
|
||||
return $response;
|
||||
}
|
||||
#[Route('/admin/export_csv/{zip_code}', name: 'app_admin_export_csv')]
|
||||
public function export_csv(string $zip_code): Response
|
||||
#[Route('/admin/export_csv/{insee_code}', name: 'app_admin_export_csv')]
|
||||
public function export_csv(string $insee_code): Response
|
||||
{
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $zip_code]);
|
||||
$response = new Response($this->motocultrice->export($zip_code));
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
||||
$response = new Response($this->motocultrice->export($insee_code));
|
||||
$response->headers->set('Content-Type', 'text/csv');
|
||||
|
||||
$slug_name = str_replace(' ', '-', $stats->getName());
|
||||
|
||||
$response->headers->set('Content-Disposition', 'attachment; filename="osm-commerces-export_' . $zip_code . '_' . $slug_name . '_' . date('Y-m-d_H-i-s') . '.csv"');
|
||||
$response->headers->set('Content-Disposition', 'attachment; filename="osm-commerces-export_' . $insee_code . '_' . $slug_name . '_' . date('Y-m-d_H-i-s') . '.csv"');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
|
|
@ -16,9 +16,6 @@ class History
|
|||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
|
||||
|
||||
|
||||
#[ORM\Column(type: Types::SMALLINT, nullable: true)]
|
||||
private ?int $completion_percent = null;
|
||||
|
||||
|
|
|
@ -16,10 +16,10 @@ class Stats
|
|||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[ORM\Column(length: 255, type: Types::STRING, nullable: true)]
|
||||
private ?string $zone = null; // code insee de la zone
|
||||
|
||||
#[ORM\Column(type: Types::SMALLINT)]
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: true)]
|
||||
private ?int $completion_percent = null;
|
||||
|
||||
/**
|
||||
|
@ -29,27 +29,27 @@ class Stats
|
|||
private Collection $places;
|
||||
|
||||
// nombre de commerces dans la zone
|
||||
#[ORM\Column(type: Types::SMALLINT, nullable: true)]
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: true)]
|
||||
private ?int $places_count = null;
|
||||
|
||||
// nombre de commerces avec horaires
|
||||
#[ORM\Column(type: Types::SMALLINT, nullable: true)]
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: true)]
|
||||
private ?int $avec_horaires = null;
|
||||
|
||||
// nombre de commerces avec adresse
|
||||
#[ORM\Column(type: Types::SMALLINT, nullable: true)]
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: true)]
|
||||
private ?int $avec_adresse = null;
|
||||
|
||||
// nombre de commerces avec site
|
||||
#[ORM\Column(type: Types::SMALLINT, nullable: true)]
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: true)]
|
||||
private ?int $avec_site = null;
|
||||
|
||||
// nombre de commerces avec accessibilité
|
||||
#[ORM\Column(type: Types::SMALLINT, nullable: true)]
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: true)]
|
||||
private ?int $avec_accessibilite = null;
|
||||
|
||||
// nombre de commerces avec note
|
||||
#[ORM\Column(type: Types::SMALLINT, nullable: true)]
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: true)]
|
||||
private ?int $avec_note = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
|
@ -59,6 +59,33 @@ class Stats
|
|||
#[ORM\Column(type: Types::INTEGER, nullable: true)]
|
||||
private ?int $population = null;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: true)]
|
||||
private ?int $siren = null;
|
||||
|
||||
#[ORM\Column(type: Types::INTEGER, nullable: true)]
|
||||
private ?int $codeEpci = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $codesPostaux = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, StatsHistory>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: StatsHistory::class, mappedBy: 'stats')]
|
||||
private Collection $statsHistories;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?\DateTime $date_created = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?\DateTime $date_modified = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $avec_siret = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $avec_name = null;
|
||||
|
||||
// calcule le pourcentage de complétion de la zone
|
||||
public function computeCompletionPercent(): ?int
|
||||
{
|
||||
|
@ -115,6 +142,7 @@ class Stats
|
|||
public function __construct()
|
||||
{
|
||||
$this->places = new ArrayCollection();
|
||||
$this->statsHistories = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
|
@ -270,6 +298,120 @@ class Stats
|
|||
$this->population = $population;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSiren(): ?int
|
||||
{
|
||||
return $this->siren;
|
||||
}
|
||||
|
||||
public function setSiren(?int $siren): static
|
||||
{
|
||||
$this->siren = $siren;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCodeEpci(): ?int
|
||||
{
|
||||
return $this->codeEpci;
|
||||
}
|
||||
|
||||
public function setCodeEpci(?int $codeEpci): static
|
||||
{
|
||||
$this->codeEpci = $codeEpci;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCodesPostaux(): ?string
|
||||
{
|
||||
return $this->codesPostaux;
|
||||
}
|
||||
|
||||
public function setCodesPostaux(?string $codesPostaux): static
|
||||
{
|
||||
$this->codesPostaux = $codesPostaux;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, StatsHistory>
|
||||
*/
|
||||
public function getStatsHistories(): Collection
|
||||
{
|
||||
return $this->statsHistories;
|
||||
}
|
||||
|
||||
public function addStatsHistory(StatsHistory $statsHistory): static
|
||||
{
|
||||
if (!$this->statsHistories->contains($statsHistory)) {
|
||||
$this->statsHistories->add($statsHistory);
|
||||
$statsHistory->setStats($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeStatsHistory(StatsHistory $statsHistory): static
|
||||
{
|
||||
if ($this->statsHistories->removeElement($statsHistory)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($statsHistory->getStats() === $this) {
|
||||
$statsHistory->setStats(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDateCreated(): ?\DateTime
|
||||
{
|
||||
return $this->date_created;
|
||||
}
|
||||
|
||||
public function setDateCreated(?\DateTime $date_created): static
|
||||
{
|
||||
$this->date_created = $date_created;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDateModified(): ?\DateTime
|
||||
{
|
||||
return $this->date_modified;
|
||||
}
|
||||
|
||||
public function setDateModified(?\DateTime $date_modified): static
|
||||
{
|
||||
$this->date_modified = $date_modified;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAvecSiret(): ?int
|
||||
{
|
||||
return $this->avec_siret;
|
||||
}
|
||||
|
||||
public function setAvecSiret(?int $avec_siret): static
|
||||
{
|
||||
$this->avec_siret = $avec_siret;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAvecName(): ?int
|
||||
{
|
||||
return $this->avec_name;
|
||||
}
|
||||
|
||||
public function setAvecName(?int $avec_name): static
|
||||
{
|
||||
$this->avec_name = $avec_name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
188
src/Entity/StatsHistory.php
Normal file
188
src/Entity/StatsHistory.php
Normal file
|
@ -0,0 +1,188 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\StatsHistoryRepository;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: StatsHistoryRepository::class)]
|
||||
class StatsHistory
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $places_count = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $emails_count = null;
|
||||
|
||||
#[ORM\Column(type: Types::SMALLFLOAT, nullable: true)]
|
||||
private ?float $completion_percent = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $emails_sent = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'statsHistories')]
|
||||
private ?Stats $stats = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTime $date = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $names_count = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $opening_hours_count = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $website_count = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $address_count = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $siret_count = null;
|
||||
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getPlacesCount(): ?int
|
||||
{
|
||||
return $this->places_count;
|
||||
}
|
||||
|
||||
public function setPlacesCount(?int $places_count): static
|
||||
{
|
||||
$this->places_count = $places_count;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEmailsCount(): ?int
|
||||
{
|
||||
return $this->emails_count;
|
||||
}
|
||||
|
||||
public function setEmailsCount(?int $emails_count): static
|
||||
{
|
||||
$this->emails_count = $emails_count;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCompletionPercent(): ?float
|
||||
{
|
||||
return $this->completion_percent;
|
||||
}
|
||||
|
||||
public function setCompletionPercent(?float $completion_percent): static
|
||||
{
|
||||
$this->completion_percent = $completion_percent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEmailsSent(): ?int
|
||||
{
|
||||
return $this->emails_sent;
|
||||
}
|
||||
|
||||
public function setEmailsSent(?int $emails_sent): static
|
||||
{
|
||||
$this->emails_sent = $emails_sent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStats(): ?Stats
|
||||
{
|
||||
return $this->stats;
|
||||
}
|
||||
|
||||
public function setStats(?Stats $stats): static
|
||||
{
|
||||
$this->stats = $stats;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDate(): ?\DateTime
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
public function setDate(\DateTime $date): static
|
||||
{
|
||||
$this->date = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNamesCount(): ?int
|
||||
{
|
||||
return $this->names_count;
|
||||
}
|
||||
|
||||
public function setNamesCount(?int $names_count): static
|
||||
{
|
||||
$this->names_count = $names_count;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOpeningHoursCount(): ?int
|
||||
{
|
||||
return $this->opening_hours_count;
|
||||
}
|
||||
|
||||
public function setOpeningHoursCount(?int $opening_hours_count): static
|
||||
{
|
||||
$this->opening_hours_count = $opening_hours_count;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWebsiteCount(): ?int
|
||||
{
|
||||
return $this->website_count;
|
||||
}
|
||||
|
||||
public function setWebsiteCount(?int $website_count): static
|
||||
{
|
||||
$this->website_count = $website_count;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAddressCount(): ?int
|
||||
{
|
||||
return $this->address_count;
|
||||
}
|
||||
|
||||
public function setAddressCount(?int $address_count): static
|
||||
{
|
||||
$this->address_count = $address_count;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSiretCount(): ?int
|
||||
{
|
||||
return $this->siret_count;
|
||||
}
|
||||
|
||||
public function setSiretCount(?int $siret_count): static
|
||||
{
|
||||
$this->siret_count = $siret_count;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
43
src/Repository/StatsHistoryRepository.php
Normal file
43
src/Repository/StatsHistoryRepository.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\StatsHistory;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<StatsHistory>
|
||||
*/
|
||||
class StatsHistoryRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, StatsHistory::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return StatsHistory[] Returns an array of StatsHistory objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('s')
|
||||
// ->andWhere('s.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('s.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?StatsHistory
|
||||
// {
|
||||
// return $this->createQueryBuilder('s')
|
||||
// ->andWhere('s.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
|
@ -10,8 +10,8 @@
|
|||
|
||||
<div class="example-wrapper">
|
||||
<h1>Labourage fait sur la zone "{{ stats.zone }} {{stats.name}}" ✅</h1>
|
||||
<a href="{{ path('app_admin_labourer', {'zip_code': stats.zone}) }}" class="btn btn-primary" id="labourer">Labourer les mises à jour</a>
|
||||
<a href="{{ path('app_admin_stats', {'zip_code': stats.zone}) }}" class="btn btn-primary" id="labourer">Voir les résultats</a>
|
||||
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone}) }}" class="btn btn-primary" id="labourer">Labourer les mises à jour</a>
|
||||
<a href="{{ path('app_admin_stats', {'insee_code': stats.zone}) }}" class="btn btn-primary" id="labourer">Voir les résultats</a>
|
||||
|
||||
<p>
|
||||
lieux trouvés en plus: {{ new_places_counter }}
|
||||
|
|
|
@ -22,6 +22,164 @@
|
|||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src='{{ asset('js/maplibre/maplibre-gl.js') }}'></script>
|
||||
<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>
|
||||
<script src="{{ asset('js/map-utils.js') }}"></script>
|
||||
<script>
|
||||
// Attendre que le DOM et tous les scripts soient chargés
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Vérifier que Chart.js est disponible
|
||||
if (typeof Chart === 'undefined') {
|
||||
console.error('Chart.js n\'est pas chargé');
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier que les fonctions sont disponibles
|
||||
if (typeof calculateCompletion === 'undefined') {
|
||||
console.error('La fonction calculateCompletion n\'est pas définie');
|
||||
return;
|
||||
}
|
||||
|
||||
let map;
|
||||
let dropMarkers = [];
|
||||
let currentMarkerType = 'drop';
|
||||
let completionChart;
|
||||
let contextMenu;
|
||||
let selectedFeature = null;
|
||||
|
||||
// Fonction pour calculer la distribution des taux de complétion
|
||||
function calculateCompletionDistribution(features) {
|
||||
const buckets = Array(11).fill(0); // 0-10%, 11-20%, ..., 91-100%
|
||||
|
||||
features.forEach(feature => {
|
||||
const completion = calculateCompletion(feature.properties);
|
||||
const bucketIndex = Math.min(Math.floor(completion.percentage / 10), 10);
|
||||
buckets[bucketIndex]++;
|
||||
});
|
||||
|
||||
return buckets;
|
||||
}
|
||||
|
||||
// Fonction pour créer le graphique de complétion
|
||||
function createCompletionChart(features) {
|
||||
const ctx = document.getElementById('completionChart').getContext('2d');
|
||||
const distribution = calculateCompletionDistribution(features);
|
||||
|
||||
if (completionChart) {
|
||||
completionChart.destroy();
|
||||
}
|
||||
|
||||
completionChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['0-10%', '11-20%', '21-30%', '31-40%', '41-50%', '51-60%', '61-70%', '71-80%', '81-90%', '91-100%'],
|
||||
datasets: [{
|
||||
label: 'Nombre de lieux',
|
||||
data: distribution,
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
stepSize: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fonction pour charger les lieux depuis l'API Overpass
|
||||
async function loadPlaces() {
|
||||
try {
|
||||
const response = await fetch('https://overpass-api.de/api/interpreter?data={{query_places|raw}}');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.features && data.features.length > 0) {
|
||||
// Mettre à jour les statistiques
|
||||
const totallieux = data.features.length;
|
||||
document.getElementById('totallieux').textContent = totallieux;
|
||||
|
||||
// Calculer et afficher la distribution des taux de complétion
|
||||
createCompletionChart(data.features);
|
||||
|
||||
// Mettre à jour les marqueurs sur la carte
|
||||
dropMarkers = updateMarkers(data.features, map, currentMarkerType, dropMarkers, data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des lieux:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialisation de la carte
|
||||
map = new maplibregl.Map({
|
||||
container: 'map',
|
||||
style: 'https://api.maptiler.com/maps/basic-v2/style.json?key={{ maptiler_token }}',
|
||||
center: [2.3522, 48.8566], // Paris
|
||||
zoom: 12
|
||||
});
|
||||
|
||||
// Ajouter les contrôles de navigation
|
||||
map.addControl(new maplibregl.NavigationControl());
|
||||
|
||||
// Gestionnaire d'événements pour le menu contextuel
|
||||
map.on('contextmenu', function(e) {
|
||||
e.preventDefault();
|
||||
const features = map.queryRenderedFeatures(e.point, {
|
||||
layers: ['markers', 'circles']
|
||||
});
|
||||
|
||||
if (features.length > 0) {
|
||||
selectedFeature = features[0];
|
||||
const popup = new maplibregl.Popup()
|
||||
.setLngLat(e.lngLat)
|
||||
.setHTML(`
|
||||
<div class="context-menu">
|
||||
<button onclick="window.location.href='{{ path('app_admin_labourer', {'insee_code': 'ZONE_CODE'}) }}'.replace('ZONE_CODE', '${selectedFeature.properties.insee_code}')">
|
||||
Labourer cette zone
|
||||
</button>
|
||||
</div>
|
||||
`)
|
||||
.addTo(map);
|
||||
}
|
||||
});
|
||||
|
||||
// Gestionnaire d'événements pour le clic sur la carte
|
||||
map.on('click', function(e) {
|
||||
const features = map.queryRenderedFeatures(e.point, {
|
||||
layers: ['markers', 'circles']
|
||||
});
|
||||
|
||||
if (features.length > 0) {
|
||||
const popup = new maplibregl.Popup()
|
||||
.setLngLat(e.lngLat)
|
||||
.setHTML(createPopupContent(features[0].properties))
|
||||
.addTo(map);
|
||||
}
|
||||
});
|
||||
|
||||
// Gestionnaire d'événements pour le bouton de changement de type de marqueur
|
||||
document.getElementById('toggleMarkerType').addEventListener('click', function() {
|
||||
currentMarkerType = currentMarkerType === 'drop' ? 'circle' : 'drop';
|
||||
this.textContent = currentMarkerType === 'drop' ? 'Afficher en cercles' : 'Afficher en points';
|
||||
loadPlaces();
|
||||
});
|
||||
|
||||
// Charger les lieux au démarrage
|
||||
loadPlaces();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
<div class="mt-4 p-4">
|
||||
|
@ -31,7 +189,7 @@
|
|||
{{ stats.name }} - {{ stats.completionPercent }}% complété</h1>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<a href="{{ path('app_admin_labourer', {'zip_code': stats.zone}) }}" class="btn btn-primary" id="labourer">Labourer les mises à jour</a>
|
||||
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone}) }}" class="btn btn-primary" id="labourer">Labourer les mises à jour</a>
|
||||
<button id="openInJOSM" class="btn btn-secondary ms-2">
|
||||
<i class="bi bi-map"></i> Ouvrir dans JOSM
|
||||
</button>
|
||||
|
@ -53,7 +211,7 @@
|
|||
</div>
|
||||
<div class="col-md-4 col-12">
|
||||
<span class="badge bg-success">
|
||||
<i class="bi bi-pencil-square"></i> {{ stats.getAvecNote() }} / {{ stats.places|length }} commerces avec note
|
||||
<i class="bi bi-pencil-square"></i> {{ stats.getAvecNote() }} / {{ stats.places|length }} lieux avec note
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -68,63 +226,42 @@
|
|||
<div class="col-md-3 col-12">
|
||||
<span class="badge bg-primary">
|
||||
<i class="bi bi-building"></i> {{ stats.places | length}}
|
||||
</span>commerces dans la zone.
|
||||
</span>lieux dans la zone.
|
||||
</div>
|
||||
<div class="col-md-3 col-12">
|
||||
<span class="badge bg-primary">
|
||||
<i class="bi bi-clock"></i> {{ stats.getAvecHoraires() }}
|
||||
</span>
|
||||
commerces avec horaires.
|
||||
lieux avec horaires.
|
||||
</div>
|
||||
<div class="col-md-3 col-12">
|
||||
<span class="badge bg-primary">
|
||||
<i class="bi bi-map"></i> {{ stats.getAvecAdresse() }}
|
||||
</span>
|
||||
commerces avec adresse.
|
||||
lieux avec adresse.
|
||||
</div>
|
||||
<div class="col-md-3 col-12">
|
||||
<span class="badge bg-primary">
|
||||
<i class="bi bi-globe"></i> {{ stats.getAvecSite() }}
|
||||
</span>
|
||||
commerces avec site web renseigné.
|
||||
lieux avec site web renseigné.
|
||||
</div>
|
||||
<div class="col-md-3 col-12">
|
||||
<span class="badge bg-primary">
|
||||
<i class="bi bi-arrow-up-right"></i>
|
||||
{{ stats.getAvecAccessibilite() }}
|
||||
</span>
|
||||
commerces avec accessibilité PMR renseignée.
|
||||
lieux avec accessibilité PMR renseignée.
|
||||
</div>
|
||||
<div class="col-md-3 col-12">
|
||||
<span class="badge bg-primary">
|
||||
<i class="bi bi-chat-dots"></i> {{ stats.getAvecNote() }}
|
||||
</span>
|
||||
commerces avec note renseignée.
|
||||
lieux avec note renseignée.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="completion-info mt-4">
|
||||
<div class="alert alert-info">
|
||||
<div class="d-flex align-items-center" style="cursor: pointer;" onclick="toggleCompletionInfo()">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
<h4 class="alert-heading mb-0">Comment est calculé le score de complétion ?</h4>
|
||||
<i class="bi bi-chevron-down ms-auto" id="completionInfoIcon"></i>
|
||||
</div>
|
||||
<div id="completionInfoContent" style="display: none;" class="mt-3">
|
||||
<p>Le score de complétion est calculé en fonction de plusieurs critères :</p>
|
||||
<ul>
|
||||
<li>Nom du commerce (obligatoire)</li>
|
||||
<li>Adresse complète (numéro, rue, code postal)</li>
|
||||
<li>Horaires d'ouverture</li>
|
||||
<li>Site web</li>
|
||||
<li>Numéro de téléphone</li>
|
||||
<li>Accessibilité PMR</li>
|
||||
<li>Note descriptive</li>
|
||||
</ul>
|
||||
<p>Chaque critère rempli augmente le score de complétion. Un commerce parfaitement renseigné aura un score de 100%.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="maploader">
|
||||
<div class="spinner-border" role="status">
|
||||
|
@ -148,15 +285,16 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="card mt-4">
|
||||
|
||||
{% include 'admin/stats_history.html.twig' %}
|
||||
<div id="distribution_completion" class="mt-4 mb-4"></div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-12">
|
||||
<h1 class="card-title p-4">Tableau des {{ stats.places |length }} lieux</h1>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<a class="btn btn-primary pull-right mt-4" href="{{ path('app_admin_export_csv', {'zip_code': stats.zone}) }}" class="btn btn-primary">
|
||||
<a class="btn btn-primary pull-right mt-4" href="{{ path('app_admin_export_csv', {'insee_code': stats.zone}) }}" class="btn btn-primary">
|
||||
<i class="bi bi-filetype-csv"></i>
|
||||
Exporter en CSV
|
||||
</a>
|
||||
|
@ -185,15 +323,70 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="history">
|
||||
<h2>Historique des {{ statsHistory|length }} stats</h2>
|
||||
<table class="table table-bordered table-striped table-hover table-responsive js-sort-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Places</th>
|
||||
<th>Complétion</th>
|
||||
<th>Emails count</th>
|
||||
<th>Emails sent</th>
|
||||
<th>Opening hours</th>
|
||||
<th>Address</th>
|
||||
<th>Website</th>
|
||||
<th>Siret</th>
|
||||
{# <th>Accessibilite</th> #}
|
||||
{# <th>Note</th> #}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for stat in statsHistory %}
|
||||
<tr>
|
||||
<td>{{ stat.date|date('d/m/Y') }}</td>
|
||||
<td>{{ stat.placesCount }}</td>
|
||||
<td>{{ stat.completionPercent }}%</td>
|
||||
<td>{{ stat.emailsCount }}</td>
|
||||
<td>{{ stat.emailsSent }}</td>
|
||||
<td>{{ stat.openingHoursCount }}</td>
|
||||
<td>{{ stat.addressCount }}</td>
|
||||
<td>{{ stat.websiteCount }}</td>
|
||||
<td>{{ stat.siretCount }}</td>
|
||||
{# <td>{{ stat.accessibiliteCount }}</td> #}
|
||||
{# <td>{{ stat.noteCount }}</td> #}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="completion-info mt-4">
|
||||
<div class="alert alert-info">
|
||||
<div class="d-flex align-items-center" style="cursor: pointer;" onclick="toggleCompletionInfo()">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
<p class="mb-0">Comment est calculé le score de complétion ?</p>
|
||||
<i class="bi bi-chevron-down ms-auto" id="completionInfoIcon"></i>
|
||||
</div>
|
||||
<div id="completionInfoContent" style="display: none;" class="mt-3">
|
||||
<p>Le score de complétion est calculé en fonction de plusieurs critères :</p>
|
||||
<ul>
|
||||
<li>Nom du commerce (obligatoire)</li>
|
||||
<li>Adresse complète (numéro, rue, code postal)</li>
|
||||
<li>Horaires d'ouverture</li>
|
||||
<li>Site web</li>
|
||||
<li>Numéro de téléphone</li>
|
||||
<li>Accessibilité PMR</li>
|
||||
<li>Note descriptive</li>
|
||||
</ul>
|
||||
<p>Chaque critère rempli augmente le score de complétion. Un commerce parfaitement renseigné aura un score de 100%.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bouton caché pour JOSM -->
|
||||
<a id="josmButton" style="display: none;"></a>
|
||||
|
||||
<script src='{{ asset('js/maplibre/maplibre-gl.js') }}'></script>
|
||||
<script src='{{ asset('js/turf/turf.min.js') }}'></script>
|
||||
<script src="{{ asset('js/chartjs/chart.umd.js') }}"></script>
|
||||
<script src="{{ asset('js/app.js') }}"></script>
|
||||
<script>
|
||||
const request = `{{query_places|raw}}`;
|
||||
const zip_code = `{{stats.zone}}`;
|
||||
|
@ -207,98 +400,169 @@
|
|||
let dropMarkers = []; // Tableau pour stocker les marqueurs en goutte
|
||||
let contextMenu = null; // Menu contextuel
|
||||
|
||||
function getCompletionColor(completion) {
|
||||
if (completion === undefined || completion === null) {
|
||||
return '#808080'; // Gris pour pas d'information
|
||||
}
|
||||
// Convertir le pourcentage en couleur verte (0% = blanc, 100% = vert foncé)
|
||||
const intensity = Math.floor((completion / 100) * 255);
|
||||
return `rgb(0, ${intensity}, 0)`;
|
||||
|
||||
function calculateCompletion(element) {
|
||||
let completionCount = 0;
|
||||
let totalFields = 0;
|
||||
let missingFields = [];
|
||||
|
||||
const fieldsToCheck = [
|
||||
{ name: 'name', label: 'Nom du commerce' },
|
||||
{ name: 'contact:street', label: 'Rue' },
|
||||
{ name: 'contact:housenumber', label: 'Numéro' },
|
||||
{ name: 'opening_hours', label: 'Horaires d\'ouverture' },
|
||||
{ name: 'contact:website', label: 'Site web' },
|
||||
{ name: 'contact:phone', label: 'Téléphone' },
|
||||
{ name: 'wheelchair', label: 'Accessibilité PMR' }
|
||||
];
|
||||
|
||||
fieldsToCheck.forEach(field => {
|
||||
totalFields++;
|
||||
if (element.tags && element.tags[field.name]) {
|
||||
completionCount++;
|
||||
} else {
|
||||
missingFields.push(field.label);
|
||||
}
|
||||
});
|
||||
|
||||
function calculateCompletion(element) {
|
||||
let completionCount = 0;
|
||||
let totalFields = 0;
|
||||
let missingFields = [];
|
||||
|
||||
const fieldsToCheck = [
|
||||
{name: 'name', label: 'Nom du commerce'},
|
||||
{name: 'contact:street', label: 'Rue'},
|
||||
{name: 'contact:housenumber', label: 'Numéro'},
|
||||
{name: 'opening_hours', label: 'Horaires d\'ouverture'},
|
||||
{name: 'contact:website', label: 'Site web'},
|
||||
{name: 'contact:phone', label: 'Téléphone'},
|
||||
{name: 'wheelchair', label: 'Accessibilité PMR'}
|
||||
];
|
||||
return {
|
||||
percentage: (completionCount / totalFields) * 100,
|
||||
missingFields: missingFields
|
||||
};
|
||||
}
|
||||
// Fonctions utilitaires pour la gestion des marqueurs et popups sur la carte
|
||||
|
||||
fieldsToCheck.forEach(field => {
|
||||
totalFields++;
|
||||
if (element.tags && element.tags[field.name]) {
|
||||
completionCount++;
|
||||
} else {
|
||||
missingFields.push(field.label);
|
||||
}
|
||||
});
|
||||
function getCompletionColor(completion) {
|
||||
if (completion === undefined || completion === null) {
|
||||
return '#808080'; // Gris pour pas d'information
|
||||
}
|
||||
// Convertir le pourcentage en couleur verte (0% = blanc, 100% = vert foncé)
|
||||
const intensity = Math.floor((completion / 100) * 255);
|
||||
return `rgb(0, ${intensity}, 0)`;
|
||||
}
|
||||
|
||||
return {
|
||||
percentage: (completionCount / totalFields) * 100,
|
||||
missingFields: missingFields
|
||||
};
|
||||
}
|
||||
|
||||
function showMissingFieldsPopup(element) {
|
||||
const completion = calculateCompletion(element);
|
||||
if (completion.percentage < 100) {
|
||||
const popup = new maplibregl.Popup()
|
||||
.setLngLat(element.geometry.coordinates)
|
||||
.setHTML(`
|
||||
<div class="p-2">
|
||||
<h5>Informations manquantes pour ${element.tags?.name || 'ce commerce'}</h5>
|
||||
<ul class="list-unstyled">
|
||||
${completion.missingFields.map(field => `<li><i class="bi bi-x-circle text-danger"></i> ${field}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`);
|
||||
popup.addTo(map);
|
||||
}
|
||||
}
|
||||
|
||||
function createPopupContent(element) {
|
||||
const completion = calculateCompletion(element);
|
||||
let content = `
|
||||
<div class="mb-2">
|
||||
<a class="btn btn-primary" href="/admin/placeType/${element.type}/${element.id}">
|
||||
<i class="bi bi-pencil"></i> ${element.tags?.name || 'Sans nom'}
|
||||
function createPopupContent(element) {
|
||||
const completion = calculateCompletion(element);
|
||||
let content = `
|
||||
<div class="mb-2">
|
||||
<h5>${element.tags?.name || 'Sans nom'}</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<a class="btn btn-primary btn-sm" href="/admin/placeType/${element.type}/${element.id}">
|
||||
<i class="bi bi-pencil"></i> Éditer
|
||||
</a>
|
||||
<a class="btn btn-secondary ms-2" href="https://openstreetmap.org/${element.type}/${element.id}">
|
||||
<a class="btn btn-secondary btn-sm" href="https://openstreetmap.org/${element.type}/${element.id}" target="_blank">
|
||||
<i class="bi bi-map"></i> OSM
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (completion.percentage < 100) {
|
||||
content += `
|
||||
<div class="alert alert-warning mt-2">
|
||||
<h6>Informations manquantes :</h6>
|
||||
<ul class="list-unstyled mb-0">
|
||||
${completion.missingFields.map(field => `<li><i class="bi bi-x-circle text-danger"></i> ${field}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
content += '<table class="table table-sm">';
|
||||
|
||||
// Ajouter tous les tags
|
||||
if (element.tags) {
|
||||
for (const tag in element.tags) {
|
||||
content += `<tr><td><strong>${tag}</strong></td><td>${element.tags[tag]}</td></tr>`;
|
||||
}
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
|
||||
content += '</table>';
|
||||
return content;
|
||||
if (completion.percentage < 100) {
|
||||
content += `
|
||||
<div class="alert alert-warning mt-2">
|
||||
<h6>Informations manquantes :</h6>
|
||||
<ul class="list-unstyled mb-0">
|
||||
${completion.missingFields.map(field => `<li><i class="bi bi-x-circle text-danger"></i> ${field}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
content += '<table class="table table-sm mt-2">';
|
||||
|
||||
// Ajouter tous les tags
|
||||
if (element.tags) {
|
||||
for (const tag in element.tags) {
|
||||
content += `<tr><td><strong>${tag}</strong></td><td>${element.tags[tag]}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
content += '</table>';
|
||||
return content;
|
||||
}
|
||||
|
||||
function updateMarkers(features, map, currentMarkerType, dropMarkers, overpassData) {
|
||||
// Supprimer tous les marqueurs existants
|
||||
dropMarkers.forEach(marker => marker.remove());
|
||||
dropMarkers = [];
|
||||
|
||||
features.forEach(feature => {
|
||||
if (currentMarkerType === 'drop') {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'marker';
|
||||
el.style.backgroundColor = getCompletionColor(feature.properties.completion);
|
||||
el.style.width = '15px';
|
||||
el.style.height = '15px';
|
||||
el.style.borderRadius = '50%';
|
||||
el.style.border = '2px solid white';
|
||||
el.style.cursor = 'pointer';
|
||||
|
||||
const marker = new maplibregl.Marker(el)
|
||||
.setLngLat(feature.geometry.coordinates)
|
||||
.addTo(map);
|
||||
|
||||
// Ajouter l'événement de clic
|
||||
el.addEventListener('click', () => {
|
||||
const element = overpassData[feature.properties.id];
|
||||
if (element) {
|
||||
const popup = new maplibregl.Popup()
|
||||
.setLngLat(feature.geometry.coordinates)
|
||||
.setHTML(createPopupContent(element));
|
||||
popup.addTo(map);
|
||||
}
|
||||
});
|
||||
|
||||
dropMarkers.push(marker);
|
||||
} else {
|
||||
// Créer un cercle pour chaque feature
|
||||
const circle = turf.circle(
|
||||
feature.geometry.coordinates,
|
||||
0.5, // rayon en kilomètres
|
||||
{ steps: 64, units: 'kilometers' }
|
||||
);
|
||||
|
||||
// Ajouter la source et la couche pour le cercle
|
||||
if (!map.getSource(feature.id)) {
|
||||
map.addSource(feature.id, {
|
||||
type: 'geojson',
|
||||
data: circle
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id: feature.id,
|
||||
type: 'fill',
|
||||
source: feature.id,
|
||||
paint: {
|
||||
'fill-color': getCompletionColor(feature.properties.completion),
|
||||
'fill-opacity': 0.6,
|
||||
'fill-outline-color': '#fff'
|
||||
}
|
||||
});
|
||||
|
||||
// Ajouter l'événement de clic sur le cercle
|
||||
map.on('click', feature.id, () => {
|
||||
const element = overpassData[feature.properties.id];
|
||||
if (element) {
|
||||
const popup = new maplibregl.Popup()
|
||||
.setLngLat(feature.geometry.coordinates)
|
||||
.setHTML(createPopupContent(element));
|
||||
popup.addTo(map);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return dropMarkers;
|
||||
}
|
||||
|
||||
// Exporter les fonctions
|
||||
window.getCompletionColor = getCompletionColor;
|
||||
window.calculateCompletion = calculateCompletion;
|
||||
window.createPopupContent = createPopupContent;
|
||||
window.updateMarkers = updateMarkers;
|
||||
|
||||
|
||||
function calculateCompletionDistribution(elements) {
|
||||
// Créer des buckets de 10% (0-10%, 10-20%, etc.)
|
||||
|
@ -323,7 +587,7 @@
|
|||
data: {
|
||||
labels: ['0-10%', '10-20%', '20-30%', '30-40%', '40-50%', '50-60%', '60-70%', '70-80%', '80-90%', '90-100%'],
|
||||
datasets: [{
|
||||
label: 'Nombre de commerces',
|
||||
label: 'Nombre de lieux',
|
||||
data: distribution,
|
||||
backgroundColor: 'rgba(0, 128, 0, 0.1)',
|
||||
borderColor: 'rgba(0, 128, 0, 1)',
|
||||
|
@ -345,7 +609,7 @@
|
|||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Nombre de commerces'
|
||||
text: 'Nombre de lieux'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
|
@ -358,7 +622,7 @@
|
|||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Distribution du taux de complétion des commerces'
|
||||
text: 'Distribution du taux de complétion des lieux'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -426,7 +690,7 @@
|
|||
});
|
||||
|
||||
// Afficher les marqueurs selon le type actuel
|
||||
updateMarkers();
|
||||
dropMarkers = updateMarkers(features, map, currentMarkerType, dropMarkers, overpassData);
|
||||
|
||||
// Ajuster la vue pour inclure tous les points
|
||||
const points = features.map(f => f.properties.center);
|
||||
|
@ -454,109 +718,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
function updateMarkers() {
|
||||
// Supprimer tous les marqueurs existants
|
||||
dropMarkers.forEach(marker => marker.remove());
|
||||
dropMarkers = [];
|
||||
|
||||
features.forEach(feature => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'marker';
|
||||
el.style.backgroundColor = getCompletionColor(feature.properties.completion);
|
||||
el.style.width = '15px';
|
||||
el.style.height = '15px';
|
||||
el.style.borderRadius = '50%';
|
||||
el.style.border = '2px solid white';
|
||||
el.style.cursor = 'pointer';
|
||||
|
||||
const marker = new maplibregl.Marker(el)
|
||||
.setLngLat(feature.geometry.coordinates)
|
||||
.addTo(map);
|
||||
|
||||
// Ajouter l'événement de clic
|
||||
el.addEventListener('click', () => {
|
||||
const element = overpassData[feature.properties.id];
|
||||
if (element) {
|
||||
showMissingFieldsPopup(element);
|
||||
}
|
||||
});
|
||||
|
||||
dropMarkers.push(marker);
|
||||
});
|
||||
}
|
||||
|
||||
function draw_circle_containing_all_features(map) {
|
||||
if (features.length === 0) return;
|
||||
|
||||
// Calculer le centre à partir de toutes les features
|
||||
const center = features.reduce((acc, feature) => {
|
||||
return [
|
||||
acc[0] + feature.properties.center[0],
|
||||
acc[1] + feature.properties.center[1]
|
||||
];
|
||||
}, [0, 0]).map(coord => coord / features.length);
|
||||
|
||||
// Calculer le rayon nécessaire pour couvrir tous les points
|
||||
const radius = features.reduce((maxRadius, feature) => {
|
||||
const distance = turf.distance(
|
||||
center,
|
||||
feature.properties.center,
|
||||
{ units: 'kilometers' }
|
||||
);
|
||||
return Math.max(maxRadius, distance);
|
||||
}, 0);
|
||||
|
||||
const circle = turf.circle(center, radius, { steps: 64, units: 'kilometers' });
|
||||
map.addSource('circle', { type: 'geojson', data: circle });
|
||||
map.addLayer({
|
||||
id: 'circle',
|
||||
type: 'fill',
|
||||
source: 'circle',
|
||||
paint: {
|
||||
'fill-color': 'blue',
|
||||
'fill-opacity': 0.25
|
||||
}
|
||||
});
|
||||
|
||||
// Ajouter un marqueur au centre du cercle
|
||||
new maplibregl.Marker()
|
||||
.setLngLat(center)
|
||||
.addTo(map);
|
||||
}
|
||||
|
||||
function updateCircles(map) {
|
||||
if (!map) return;
|
||||
|
||||
const zoom = map.getZoom();
|
||||
// Ajuster la taille de base en fonction du zoom
|
||||
// Plus le zoom est faible (loin), plus le rayon est grand
|
||||
const baseRadius = Math.min(100, 200 / Math.pow(1.2, zoom));
|
||||
|
||||
features.forEach(feature => {
|
||||
const source = map.getSource(feature.id);
|
||||
if (!source) return;
|
||||
|
||||
const completion = feature.properties.completion;
|
||||
const radius = baseRadius * (1 + (completion / 100));
|
||||
|
||||
// Mettre à jour le rayon du cercle
|
||||
const circle = turf.circle(
|
||||
feature.properties.center,
|
||||
0.5* radius / 1000, // Convertir en kilomètres
|
||||
{ steps: 64, units: 'kilometers' }
|
||||
);
|
||||
|
||||
// Mettre à jour la source avec le nouveau cercle
|
||||
source.setData(circle);
|
||||
|
||||
// Mettre à jour la couleur du cercle
|
||||
const layer = map.getLayer(feature.id);
|
||||
if (layer) {
|
||||
map.setPaintProperty(feature.id, 'fill-color', getCompletionColor(completion));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openJOSMQuery(map, query) {
|
||||
const bounds = map.getBounds();
|
||||
|
||||
|
@ -639,58 +800,20 @@
|
|||
contextMenu.style.top = `${y}px`;
|
||||
|
||||
contextMenu.innerHTML = '<div class="spinner-border spinner-border-sm" role="status"></div> Recherche du code INSEE...';
|
||||
|
||||
try {
|
||||
// Récupérer les coordonnées du clic
|
||||
const { lng, lat } = e.lngLat;
|
||||
|
||||
// Appeler l'API pour obtenir le code INSEE
|
||||
const response = await fetch(`https://api-adresse.data.gouv.fr/reverse/?lon=${lng}&lat=${lat}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.features && data.features.length > 0) {
|
||||
const feature = data.features[0];
|
||||
const city = feature.properties.city;
|
||||
const inseeCode = feature.properties.citycode;
|
||||
|
||||
contextMenu.innerHTML = `
|
||||
<div class="mb-2">
|
||||
<strong>Ville :</strong> ${city}<br>
|
||||
<strong>Code INSEE :</strong> ${inseeCode}
|
||||
</div>
|
||||
<a href="/admin/labourer/${inseeCode}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-tractor"></i> Labourer cette zone
|
||||
</a>
|
||||
`;
|
||||
} else {
|
||||
contextMenu.innerHTML = 'Aucune information trouvée pour cette position';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la recherche du code INSEE:', error);
|
||||
contextMenu.innerHTML = 'Erreur lors de la recherche du code INSEE';
|
||||
}
|
||||
});
|
||||
|
||||
// Fermer le menu contextuel lors d'un clic ailleurs
|
||||
document.addEventListener('click', function(e) {
|
||||
if (contextMenu && !contextMenu.contains(e.target)) {
|
||||
contextMenu.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Gestionnaires d'événements pour les boutons de type de marqueur
|
||||
document.getElementById('circleMarkersBtn').addEventListener('click', function() {
|
||||
currentMarkerType = 'circle';
|
||||
this.classList.add('active');
|
||||
document.getElementById('dropMarkersBtn').classList.remove('active');
|
||||
updateMarkers();
|
||||
dropMarkers = updateMarkers(features, map, currentMarkerType, dropMarkers, overpassData);
|
||||
});
|
||||
|
||||
document.getElementById('dropMarkersBtn').addEventListener('click', function() {
|
||||
currentMarkerType = 'drop';
|
||||
this.classList.add('active');
|
||||
document.getElementById('circleMarkersBtn').classList.remove('active');
|
||||
updateMarkers();
|
||||
dropMarkers = updateMarkers(features, map, currentMarkerType, dropMarkers, overpassData);
|
||||
});
|
||||
|
||||
document.getElementById('openInJOSM').addEventListener('click', openInJOSM);
|
||||
|
@ -720,8 +843,6 @@
|
|||
|
||||
// Charger les lieux
|
||||
loadPlaces(map);
|
||||
|
||||
draw_circle_containing_all_features(map);
|
||||
});
|
||||
|
||||
sortTable();
|
||||
|
|
|
@ -86,9 +86,9 @@
|
|||
<td class="{{ commerce.noteContent ? 'filled' : '' }}">{{ commerce.noteContent }}</td>
|
||||
<td class="{{ commerce.siret ? 'filled' : '' }}"> <a href="https://annuaire-entreprises.data.gouv.fr/etablissement/{{ commerce.siret }}" > {{ commerce.siret }}</a></td>
|
||||
<td>
|
||||
<a href="https://www.openstreetmap.org/{{ commerce.osmKind }}/{{ commerce.osmId }}" >
|
||||
<a href="https://www.openstreetmap.org/{{ commerce.osmKind }}/{{ commerce.osmId }}" title="{{ commerce.osmKind }} - {{ commerce.osmId }} " >
|
||||
<i class="bi bi-globe"></i>
|
||||
{{ commerce.osmId }}
|
||||
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
|
|
|
@ -21,8 +21,10 @@
|
|||
<i class="bi bi-house-fill"></i>
|
||||
Rue
|
||||
</th>
|
||||
<th>
|
||||
<i class="bi bi-globe"></i>
|
||||
Site web ({{ stats.getAvecSite() }} / {{ stats.places|length }})</th>
|
||||
Site web ({{ stats.getAvecSite() }} / {{ stats.places|length }})
|
||||
</th>
|
||||
<th>
|
||||
<i class="bi bi-wheelchair"></i>
|
||||
<i class="bi bi-person-fill-slash"></i>
|
||||
|
|
62
templates/admin/stats_history.html.twig
Normal file
62
templates/admin/stats_history.html.twig
Normal file
|
@ -0,0 +1,62 @@
|
|||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h2>Évolution du taux de complétion</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="completionHistoryChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const ctx = document.getElementById('completionHistoryChart').getContext('2d');
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [
|
||||
{% for stat in statsHistory %}
|
||||
'{{ stat.date|date('d/m/Y') }}'{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
],
|
||||
datasets: [{
|
||||
label: 'Taux de complétion (%)',
|
||||
data: [
|
||||
{% for stat in statsHistory %}
|
||||
{{ stat.completionPercent }}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
],
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
tension: 0.3,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Évolution du taux de complétion au fil du temps'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 100,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Complétion (%)'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Date'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
|
@ -53,9 +53,8 @@
|
|||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script src='{{ asset('js/maplibre/maplibre-gl.js') }}'></script>
|
||||
<script type="module">
|
||||
import { colorizePercentageCells, setupCitySearch, getLabourerUrl, handleAddCityFormSubmit, colorizePercentageCellsRelative } from '{{ asset('js/utils.js') }}';
|
||||
|
||||
<script src='{{ asset('js/utils.js') }}'></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialiser les tooltips Bootstrap
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||
|
@ -129,25 +128,29 @@
|
|||
<tbody>
|
||||
{% for stat in stats %}
|
||||
<tr>
|
||||
<td><a href="{{ path('app_admin_stats', {'zip_code': stat.zone}) }}" title="Voir les statistiques de cette ville">
|
||||
<td><a href="{{ path('app_admin_stats', {'insee_code': stat.zone}) }}" title="Voir les statistiques de cette ville">
|
||||
{{ stat.name }}
|
||||
{% if not stat.name and stat.zone starts with '751' %}
|
||||
Paris {{ stat.zone|slice(-2) }}e.
|
||||
|
||||
{% endif %}
|
||||
</a></td>
|
||||
<td>{{ stat.zone }}</td>
|
||||
<td>{{ stat.completionPercent }}%</td>
|
||||
<td>{{ stat.places|length }}</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{{ path('app_admin_stats', {'zip_code': stat.zone}) }}" class="btn btn-sm btn-primary" title="Voir les statistiques de cette ville">
|
||||
<a href="{{ path('app_admin_stats', {'insee_code': stat.zone}) }}" class="btn btn-sm btn-primary" title="Voir les statistiques de cette ville">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a href="{{ path('app_admin_labourer', {'zip_code': stat.zone}) }}"
|
||||
<a href="{{ path('app_admin_labourer', {'insee_code': stat.zone}) }}"
|
||||
class="btn btn-sm btn-success btn-labourer"
|
||||
data-zip-code="{{ stat.zone }}"
|
||||
title="Labourer cette ville"
|
||||
>
|
||||
<i class="bi bi-recycle"></i>
|
||||
</a>
|
||||
<a href="{{ path('app_admin_delete_by_zone', {'zip_code': stat.zone}) }}"
|
||||
<a href="{{ path('app_admin_delete_by_zone', {'insee_code': stat.zone}) }}"
|
||||
class="btn btn-sm btn-danger"
|
||||
onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette zone ?')"
|
||||
title="Supprimer cette ville"
|
||||
|
|
|
@ -126,22 +126,24 @@
|
|||
<div class="lien-OpenStreetMap">
|
||||
<a href="https://www.openstreetmap.org/{{commerce.osmKind}}/{{ commerce_overpass['@attributes'].id }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-globe"></i> {{ 'display.view_on_osm'|trans }}
|
||||
{{ asset('img/logo-osm.png') }}
|
||||
<img src="{{ asset('img/logo-osm.png') }}" alt="OpenStreetMap" class="img-fluid thumbnail">
|
||||
</a>
|
||||
<button onclick="openInJOSM('{{commerce.osmKind}}', '{{ commerce_overpass['@attributes'].id }}')"
|
||||
title="Ouvrir dans JOSM"
|
||||
class="btn btn-outline-secondary ms-2">
|
||||
<i class="bi bi-pencil"></i>
|
||||
{{ asset('img/josm.png') }}
|
||||
<img src="{{ asset('img/josm.png') }}" alt="JOSM" class="img-fluid thumbnail">
|
||||
</button>
|
||||
<button onclick="openInPanoramax()" title="Ouvrir dans Panoramax" class="btn btn-outline-secondary ms-2">
|
||||
<i class="bi bi-camera"></i>
|
||||
{{ asset('img/logo-panoramax.png') }}
|
||||
|
||||
<img src="{{ asset('img/panoramax.svg') }}" alt="Panoramax" class="img-fluid thumbnail">
|
||||
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% if commerce.stats %}
|
||||
<a href="{{ path('app_admin_stats', {'zip_code': commerce.stats.zone}) }}" class="btn btn-outline-secondary">
|
||||
<a href="{{ path('app_admin_stats', {'insee_code': commerce.stats.zone}) }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-bar-chart"></i> zone {{commerce.stats.zone}}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
|
|
@ -108,12 +108,15 @@
|
|||
</div>
|
||||
|
||||
{% for stat in stats %}
|
||||
<a href="{{ path('app_admin_stats', {'zip_code': stat.zone}) }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||
<a href="{{ path('app_admin_stats', {'insee_code': stat.zone}) }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex flex-column">
|
||||
<span class="zone">{{ stat.zone }}</span>
|
||||
<span class="name">{{ stat.name }}</span>
|
||||
</div>
|
||||
<span class="badge bg-primary rounded-pill">{{ stat.placesCount }} lieux</span>
|
||||
<div class="d-flex">
|
||||
<span class="badge bg-primary rounded-pill">{{ stat.placesCount }} lieux</span>
|
||||
<span class="badge rounded-pill completion {% if stat.completionPercent > 80 %}bg-success{% else %}bg-info{% endif %}" >{{ stat.completionPercent }}%</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
|
|
|
@ -86,4 +86,4 @@ display:
|
|||
note: "Champ libre, pour des informations complémentaires. Ne pas mettre de publicité"
|
||||
disclaimer:
|
||||
title: "Attention"
|
||||
text: "Ce site est un travail en cours, ceci est une démonstration sur un objet fictif qui utilise les données de l'API OpenStreetMap."
|
||||
text: "Ce site est un travail en cours, qui utilise les données de l'API OpenStreetMap."
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue