From ad4170db146e4a2bd0e4153649dd65064175a627 Mon Sep 17 00:00:00 2001 From: Tykayn Date: Sat, 21 Jun 2025 10:26:55 +0200 Subject: [PATCH] add bubble chart, fix dashboard perf --- assets/app.js | 6 + assets/utils.js | 279 ++++++--------------------- package-lock.json | 21 ++ package.json | 1 + src/Controller/AdminController.php | 77 +++++++- src/Service/Motocultrice.php | 30 ++- templates/public/dashboard.html.twig | 160 +++++++++++++-- templates/public/home.html.twig | 9 +- 8 files changed, 335 insertions(+), 248 deletions(-) diff --git a/assets/app.js b/assets/app.js index 8376723..3abd89a 100644 --- a/assets/app.js +++ b/assets/app.js @@ -7,11 +7,17 @@ // any CSS you import will output into a single css file (app.css in this case) import './styles/app.css'; + +// start the Stimulus application +// import './bootstrap'; + import './utils.js'; import './opening_hours.js'; import './josm.js'; import './edit.js'; +import Chart from 'chart.js/auto'; +window.Chart = Chart; // Attendre le chargement du DOM document.addEventListener('DOMContentLoaded', () => { diff --git a/assets/utils.js b/assets/utils.js index e6cd43a..8215bba 100644 --- a/assets/utils.js +++ b/assets/utils.js @@ -1,5 +1,4 @@ function colorHeadingTable() { - const headers = document.querySelectorAll('th'); headers.forEach(header => { const text = header.textContent; @@ -13,9 +12,7 @@ function colorHeadingTable() { }); } - function check_validity(e) { - list_inputs_good_to_fill = [ 'input[name="commerce_tag_value__contact:email"]', 'input[name="commerce_tag_value__contact:phone"]', @@ -73,20 +70,16 @@ function check_validity(e) { document.querySelector('#validation_messages').classList.add('is-invalid'); } } -// Générer une couleur pastel aléatoire + const genererCouleurPastel = () => { - // Utiliser des valeurs plus claires (180-255) pour obtenir des tons pastel const r = Math.floor(Math.random() * 75 + 180); const g = Math.floor(Math.random() * 75 + 180); const b = Math.floor(Math.random() * 75 + 180); return `rgb(${r}, ${g}, ${b})`; }; - async function searchInseeCode(query) { try { - - // Afficher l'indicateur de chargement document.querySelector('#loading_search_insee').classList.remove('d-none'); const response = await fetch(`https://geo.api.gouv.fr/communes?nom=${query}&fields=nom,code,codesPostaux&limit=10`); @@ -104,21 +97,16 @@ async function searchInseeCode(query) { } } - function updateMapHeightForLargeScreens() { - const mapFound = document.querySelector('#map'); if (mapFound && window.innerHeight > 800 && window.innerWidth > 800) { - mapFound.style.height = window.innerWidth * 0.8 + 'px'; } else { console.log('window.innerHeight', window.innerHeight); } } -// lister les changesets de l'utilisateur osm-commerces async function listChangesets() { - // Ajouter le header Accept pour demander du JSON const options = { headers: { 'Accept': 'application/json' @@ -128,7 +116,6 @@ async function listChangesets() { const data = await changesets.json(); console.log(data.changesets.length); - // Grouper les changesets par période const now = new Date(); const last24h = new Date(now - 24 * 60 * 60 * 1000); const last7days = new Date(now - 7 * 24 * 60 * 60 * 1000); @@ -154,7 +141,6 @@ async function listChangesets() { } }); - // Afficher les statistiques const historyDiv = document.getElementById('userChangesHistory'); if (historyDiv) { historyDiv.innerHTML = ` @@ -170,8 +156,6 @@ async function listChangesets() { } } - - function openInPanoramax() { const center = map.getCenter(); const zoom = map.getZoom(); @@ -179,147 +163,111 @@ function openInPanoramax() { window.open(panoramaxUrl); } - function enableLabourageForm() { - - // Récupérer les éléments du formulaire const citySearchInput = document.getElementById('citySearch'); const citySuggestionsList = document.getElementById('citySuggestions'); if (citySearchInput && citySuggestionsList) { - // Configurer la recherche de ville avec la fonction existante setupCitySearch('citySearch', 'citySuggestions', function (result_search) { - console.log('code_insee', result_search.insee); - // Activer le spinner dans le bouton de labourage const labourageBtn = document.querySelector('.btn-labourer'); if (labourageBtn) { labourageBtn.innerHTML = ' Chargement...'; labourageBtn.disabled = true; } - console.log('result_search', result_search, getLabourerUrl(result_search)); window.location.href = getLabourerUrl(result_search); }); } } -// Fonction pour gérer la recherche de villes -/** - * Configure la recherche de ville avec autocomplétion - * @param {string} inputId - ID de l'input de recherche - * @param {string} suggestionListId - ID de la liste des suggestions - * @param {Function} onSelect - Callback appelé lors de la sélection d'une ville - */ + function setupCitySearch(inputId, suggestionListId, onSelect) { const searchInput = document.getElementById(inputId); const suggestionList = document.getElementById(suggestionListId); - window.searchInput = searchInput; - window.suggestionList = suggestionList; - window.onSelect = onSelect; - if (!searchInput || !suggestionList) return; let timeoutId = null; - let searchOngoing = false; + searchInput.addEventListener('input', function () { - console.log('input', this.value); clearTimeout(timeoutId); const query = this.value.trim(); - - + if (query.length < 3) { + clearSuggestions(); + return; + } timeoutId = setTimeout(() => { if (!searchOngoing) { searchOngoing = true; - performSearch(query); - searchOngoing = false; + performSearch(query).then(() => { + searchOngoing = false; + }); } }, 300); }); -} - - -function performSearch(query) { - console.log('performSearch', query); - fetch(`https://geo.api.gouv.fr/communes?nom=${encodeURIComponent(query)}&fields=nom,code,codesPostaux&limit=5`) - .then(response => response.json()) - .then(data => { + async function performSearch(query) { + try { + const response = await fetch(`https://geo.api.gouv.fr/communes?nom=${encodeURIComponent(query)}&fields=nom,code,codesPostaux&limit=5`); + const data = await response.json(); const citySuggestions = data.map(city => ({ name: city.nom, postcode: city.codesPostaux[0], - insee: city.code + insee: city.code, + display_name: `${city.nom} (${city.codesPostaux[0]})` })); displaySuggestions(citySuggestions); - }) - .catch(error => { - console.error('Erreur lors de la recherche:', error); - clearSuggestions(); - }); -} + } catch (error) { + console.error("Erreur de recherche:", error); + } + } -function displaySuggestions(suggestions) { - console.log('displaySuggestions', suggestions); - clearSuggestions(); - suggestions.forEach(suggestion => { - const li = document.createElement('li'); - li.className = 'list-group-item p-2'; - li.textContent = `${suggestion.name} (${suggestion.postcode})`; - li.addEventListener('click', () => { - searchInput.value = suggestion.name; - clearSuggestions(); - if (onSelect) onSelect(suggestion); - }); - window.suggestionList.appendChild(li); - }); - - window.suggestionList.classList.remove('d-none'); - console.log('window.suggestionList', window.suggestionList); -} - -function clearSuggestions() { - window.suggestionList.innerHTML = ''; -} - -// Fermer les suggestions en cliquant en dehors -document.addEventListener('click', function (e) { - if (window.searchInput && !window.searchInput?.contains(e.target) && !window.suggestionList?.contains(e.target)) { + function displaySuggestions(suggestions) { clearSuggestions(); + suggestions.forEach(suggestion => { + const item = document.createElement('div'); + item.classList.add('suggestion-item'); + item.textContent = suggestion.display_name; + item.addEventListener('click', () => { + searchInput.value = suggestion.display_name; + clearSuggestions(); + if (onSelect) { + onSelect(suggestion); + } + }); + suggestionList.appendChild(item); + }); + suggestionList.style.display = 'block'; } -}); -// Fonction pour formater l'URL de labourage -/** - * Génère l'URL de labourage pour un code postal donné - * @param {string} zipCode - Le code postal - * @returns {string} L'URL de labourage - */ + function clearSuggestions() { + suggestionList.innerHTML = ''; + suggestionList.style.display = 'none'; + } + + document.addEventListener('click', (e) => { + if (!searchInput.contains(e.target) && !suggestionList.contains(e.target)) { + clearSuggestions(); + } + }); +} + function getLabourerUrl(obj) { - - return `/admin/labourer/${obj.insee}`; -} - -// Fonction pour gérer la soumission du formulaire d'ajout de ville -function handleAddCityFormSubmit(event) { - event.preventDefault(); - const form = event.target; - const submitButton = form.querySelector('button[type="submit"]'); - const zipCodeInput = form.querySelector('input[name="zip_code"]'); - if (!zipCodeInput.value) { - return; + if (obj && obj.insee) { + return `/admin/labourer_insee/${obj.insee}`; } - // Afficher le spinner - submitButton.disabled = true; - const originalContent = submitButton.innerHTML; - submitButton.innerHTML = ' Labourer...'; - // Rediriger - window.location.href = getLabourerUrl(zipCodeInput.value); + return '#'; +} + +function handleAddCityFormSubmit(event) { + event.preventDefault(); + const zipCode = document.getElementById('selectedZipCode').value; + if (zipCode && zipCode.match(/^\d{5}$/)) { + window.location.href = `/admin/labourer/${zipCode}`; + } else { + alert('Veuillez sélectionner une ville valide avec un code postal.'); + } } -/** - * Colore les cellules d'un tableau en fonction des pourcentages - * @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') - */ function colorizePercentageCells(selector, color = '154, 205, 50') { document.querySelectorAll(selector).forEach(cell => { const percentage = parseInt(cell.textContent); @@ -330,16 +278,9 @@ function colorizePercentageCells(selector, color = '154, 205, 50') { }); } -/** - * Colore les cellules d'un tableau avec un gradient relatif à la valeur maximale - * @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') - */ function colorizePercentageCellsRelative(selector, color = '154, 205, 50') { - // Récupérer toutes les cellules const cells = document.querySelectorAll(selector); - // Trouver la valeur maximale let maxValue = 0; cells.forEach(cell => { const value = parseInt(cell.textContent); @@ -348,28 +289,20 @@ function colorizePercentageCellsRelative(selector, color = '154, 205, 50') { } }); - // Appliquer le gradient relatif à la valeur max cells.forEach(cell => { const value = parseInt(cell.textContent); if (!isNaN(value)) { - const alpha = value / maxValue; // Ratio relatif au maximum + const alpha = value / maxValue; cell.style.backgroundColor = `rgba(${color}, ${alpha})`; } }); } -/** - * Ajuste dynamiquement la taille du texte des éléments list-group-item selon leur nombre - * @param {string} selector - Le sélecteur CSS des éléments à ajuster - * @param {number} [minFont=0.8] - Taille de police minimale en rem - * @param {number} [maxFont=1.2] - Taille de police maximale en rem - */ function adjustListGroupFontSize(selector, minFont = 0.8, maxFont = 1.2) { const items = document.querySelectorAll(selector); const count = items.length; let fontSize = maxFont; if (count > 0) { - // Plus il y a d'items, plus la taille diminue, mais jamais en dessous de minFont fontSize = Math.max(minFont, maxFont - (count - 5) * 0.05); } items.forEach(item => { @@ -377,97 +310,13 @@ function adjustListGroupFontSize(selector, minFont = 0.8, maxFont = 1.2) { }); } -function check_validity() { - if (!document.getElementById('editLand')) { - return; - } - - const form = document.getElementById('editLand'); - const fields = { - 'name': { - required: true, - message: 'Le nom est requis' - }, - 'contact:street': { - required: true, - message: 'La rue est requise' - }, - 'contact:housenumber': { - required: true, - message: 'Le numéro est requis' - }, - 'contact:phone': { - pattern: /^(?:(?:\+|00)33|0)\s*[1-9](?:[\s.-]*\d{2}){4}$/, - message: 'Le numéro de téléphone n\'est pas valide' - }, - 'contact:email': { - pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, - message: 'L\'adresse email n\'est pas valide' - }, - 'contact:website': { - pattern: /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/, - message: 'L\'URL du site web n\'est pas valide' - } - }; - - let isValid = true; - const errorMessages = {}; - - // Supprimer les messages d'erreur précédents - document.querySelectorAll('.error-message').forEach(el => el.remove()); - - // Vérifier chaque champ - for (const [fieldName, rules] of Object.entries(fields)) { - const input = form.querySelector(`[name="${fieldName}"]`); - if (!input) continue; - - const value = input.value.trim(); - let fieldError = null; - - // Ne valider que si le champ n'est pas vide - if (value) { - if (rules.pattern && !rules.pattern.test(value)) { - fieldError = rules.message; - } - } else if (rules.required) { - // Si le champ est vide et requis - fieldError = rules.message; - } - - if (fieldError) { - isValid = false; - errorMessages[fieldName] = fieldError; - - // Créer et afficher le message d'erreur - const errorDiv = document.createElement('div'); - errorDiv.className = 'error-message text-danger small mt-1'; - errorDiv.textContent = fieldError; - input.parentNode.appendChild(errorDiv); - - // Ajouter une classe d'erreur au champ - input.classList.add('is-invalid'); - } else { - input.classList.remove('is-invalid'); - } - } - - return isValid; -} - - -// Exporter les fonctions dans window window.setupCitySearch = setupCitySearch; -window.getLabourerUrl = getLabourerUrl; window.handleAddCityFormSubmit = handleAddCityFormSubmit; window.colorizePercentageCells = colorizePercentageCells; window.colorizePercentageCellsRelative = colorizePercentageCellsRelative; -window.adjustListGroupFontSize = adjustListGroupFontSize; window.check_validity = check_validity; -window.enableLabourageForm = enableLabourageForm; -window.performSearch = performSearch; window.openInPanoramax = openInPanoramax; window.listChangesets = listChangesets; -window.updateMapHeightForLargeScreens = updateMapHeightForLargeScreens; -window.searchInseeCode = searchInseeCode; -window.genererCouleurPastel = genererCouleurPastel; -window.check_validity = check_validity; \ No newline at end of file +window.colorHeadingTable = colorHeadingTable; +window.adjustListGroupFontSize = adjustListGroupFontSize; +window.genererCouleurPastel = genererCouleurPastel; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 83aaf54..5695a9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "@babel/core": "^7.17.0", "@babel/preset-env": "^7.16.0", "@symfony/webpack-encore": "^5.0.0", + "chart.js": "^4.5.0", "core-js": "^3.38.0", "regenerator-runtime": "^0.13.9", "webpack": "^5.74.0", @@ -1694,6 +1695,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "dev": true, + "license": "MIT" + }, "node_modules/@nuxt/friendly-errors-webpack-plugin": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@nuxt/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-2.6.0.tgz", @@ -2469,6 +2477,19 @@ "node": ">=4" } }, + "node_modules/chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", diff --git a/package.json b/package.json index d340290..bfe5fab 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "@babel/core": "^7.17.0", "@babel/preset-env": "^7.16.0", "@symfony/webpack-encore": "^5.0.0", + "chart.js": "^4.5.0", "core-js": "^3.38.0", "regenerator-runtime": "^0.13.9", "webpack": "^5.74.0", diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php index 39427b1..e4128db 100644 --- a/src/Controller/AdminController.php +++ b/src/Controller/AdminController.php @@ -97,6 +97,7 @@ final class AdminController extends AbstractController } elseif ($updateExisting) { // Mettre à jour les données depuis Overpass uniquement si updateExisting est true $existingPlace->update_place_from_overpass_data($placeData); + $stats->addPlace($existingPlace); $this->entityManager->persist($existingPlace); $updatedCount++; } @@ -452,11 +453,14 @@ final class AdminController extends AbstractController } // Récupérer toutes les données - $places = $this->motocultrice->labourer($insee_code); + $places_overpass = $this->motocultrice->labourer($insee_code); $processedCount = 0; $updatedCount = 0; - - foreach ($places as $placeData) { + $deletedCount = 0; + + $overpass_osm_ids = array_map(fn($place) => $place['id'], $places_overpass); + + foreach ($places_overpass as $placeData) { // Vérifier si le lieu existe déjà $existingPlace = $this->entityManager->getRepository(Place::class) ->findOneBy(['osmId' => $placeData['id']]); @@ -477,8 +481,11 @@ final class AdminController extends AbstractController ->setSiret($this->motocultrice->find_siret($placeData['tags']) ?? '') ->setAskedHumainsSupport(false) ->setLastContactAttemptDate(null) - ->setNote('') - ->setPlaceCount(0); + ->setNote($this->motocultrice->find_tag($placeData['tags'], 'note') ? true : false) + ->setNoteContent($this->motocultrice->find_tag($placeData['tags'], 'note') ?? '') + ->setPlaceCount(0) + // ->setOsmData($placeData['modified'] ?? null) + ; // Mettre à jour les données depuis Overpass $place->update_place_from_overpass_data($placeData); @@ -487,17 +494,63 @@ final class AdminController extends AbstractController $stats->addPlace($place); $processedCount++; } elseif ($updateExisting) { - // Mettre à jour les données depuis Overpass uniquement si updateExisting est true + // Mettre à jour les données depuis Overpass et s'assurer qu'il est marqué comme "vivant" + $existingPlace->setDead(false); $existingPlace->update_place_from_overpass_data($placeData); + $stats->addPlace($existingPlace); $this->entityManager->persist($existingPlace); $updatedCount++; } } - // Flush final - $this->entityManager->flush(); + // Supprimer les lieux qui ne sont plus dans la réponse Overpass + $db_places = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]); + foreach ($db_places as $db_place) { + if (!in_array($db_place->getOsmId(), $overpass_osm_ids)) { + $this->entityManager->remove($db_place); + $deletedCount++; + } + } + + // Récupérer tous les commerces de la zone qui n'ont pas été supprimés + $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' => $insee_code]); + if(!$stats) { + $stats = new Stats(); + $stats->setZone($insee_code); + } + + $urls = $stats->getAllCTCUrlsMap(); - // Mettre à jour les statistiques finales + $statsHistory = $this->entityManager->getRepository(StatsHistory::class) + ->createQueryBuilder('sh') + ->where('sh.stats = :stats') + ->setParameter('stats', $stats) + ->orderBy('sh.id', 'DESC') + ->setMaxResults(365) + ->getQuery() + ->getResult(); + + // Calculer les statistiques + $calculatedStats = $this->motocultrice->calculateStats($commerces); + + // Mettre à jour les stats pour la zone donnée + $stats->setPlacesCount($calculatedStats['places_count']); + $stats->setAvecHoraires($calculatedStats['counters']['avec_horaires']); + $stats->setAvecAdresse($calculatedStats['counters']['avec_adresse']); + $stats->setAvecSite($calculatedStats['counters']['avec_site']); + $stats->setAvecAccessibilite($calculatedStats['counters']['avec_accessibilite']); + $stats->setAvecNote($calculatedStats['counters']['avec_note']); + $stats->setCompletionPercent($calculatedStats['completion_percent']); + + // Associer les stats à chaque commerce + foreach ($commerces as $commerce) { + $commerce->setStats($stats); + $this->entityManager->persist($commerce); + } + $stats->computeCompletionPercent(); // Calculer les statistiques de fraîcheur des données OSM @@ -564,8 +617,12 @@ final class AdminController extends AbstractController $message = 'Labourage terminé avec succès. ' . $processedCount . ' nouveaux lieux traités.'; if ($updateExisting) { - $message .= ' ' . $updatedCount . ' lieux existants mis à jour pour la zone '.$stats->getName().' ('.$stats->getZone().').'; + $message .= ' ' . $updatedCount . ' lieux existants mis à jour.'; } + if ($deletedCount > 0) { + $message .= ' ' . $deletedCount . ' lieux ont été supprimés.'; + } + $message .= ' Zone : '.$stats->getName().' ('.$stats->getZone().').'; $this->addFlash('success', $message); } catch (\Exception $e) { $this->addFlash('error', 'Erreur lors du labourage : ' . $e->getMessage()); diff --git a/src/Service/Motocultrice.php b/src/Service/Motocultrice.php index b75f001..c72c099 100644 --- a/src/Service/Motocultrice.php +++ b/src/Service/Motocultrice.php @@ -162,16 +162,33 @@ out meta;'; public function labourer(string $zone): array { + $query = $this->get_query_places($zone); + $url = 'https://overpass-api.de/api/interpreter?data=' . urlencode($query); + try { - $query = $this->get_query_places($zone); - $response = $this->client->request('POST', $this->overpassApiUrl, [ - 'body' => ['data' => $query] + $response = $this->client->request('GET', $url, [ + 'timeout' => 90, // Augmenter le timeout pour les zones très denses ]); + if ($response->getStatusCode() !== 200) { + throw new \Exception('L\'API Overpass a retourné un code de statut non-200 : ' . $response->getStatusCode()); + } + $data = json_decode($response->getContent(), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + // Tenter de récupérer le corps de la réponse brute en cas d'erreur JSON pour le débogage + $rawResponse = $response->getContent(false); // ne pas lancer d'exception si la réponse n'est pas décodable + throw new \Exception('Réponse JSON invalide depuis l\'API Overpass. Erreur: ' . json_last_error_msg() . '. Réponse brute : ' . substr($rawResponse, 0, 500)); + } $places = []; if (isset($data['elements'])) { + if (count($data['elements']) === 0) { + // Ce n'est pas une erreur, juste aucun lieu trouvé. On retourne un tableau vide. + return []; + } + foreach ($data['elements'] as $element) { if (isset($element['tags'])) { $places[] = [ @@ -198,8 +215,13 @@ out meta;'; gc_collect_cycles(); return $places; + + } catch (\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface $e) { + // Gérer spécifiquement les erreurs de transport (timeout, problème de connexion) + throw new \Exception('Erreur de communication avec l\'API Overpass (le service est peut-être indisponible ou la requête a expiré) : ' . $e->getMessage(), 0, $e); } catch (\Exception $e) { - throw new \Exception("Erreur lors de la requête Overpass : " . $e->getMessage()); + // Rattraper et relancer les autres exceptions (y compris celles que nous avons définies) + throw $e; } } diff --git a/templates/public/dashboard.html.twig b/templates/public/dashboard.html.twig index db5f5b5..b266bac 100644 --- a/templates/public/dashboard.html.twig +++ b/templates/public/dashboard.html.twig @@ -23,6 +23,7 @@ overflow-y: auto; width: 100%; z-index: 1000; + display: none; } .suggestion-item { padding: 8px 12px; @@ -57,8 +58,43 @@

Tableau de bord

+ +
+
+
+
+ Statistiques des villes (nombre de commerces) +
+
+ +
+
+
+
+
+
+
+

Labourer une ville

+
+
+ +
+
+ + +
+
+
+
+
+ +

Statistiques par ville

@@ -69,6 +105,7 @@ Code postal Complétion Nombre de commerces + Lieux par habitants Actions @@ -84,7 +121,8 @@ {{ stat.zone }} {{ stat.completionPercent }}% - {{ stat.places|length }} + {{ stat.placesCount }} + {{ (stat.placesCount / (stat.population or 1 ))|round(2) }}
- - {% include 'public/labourage-form.html.twig' %}
{% endblock %} {% block javascripts %} {{ parent() }} - - + {# Les scripts sont maintenant gérés par Webpack Encore via app.js #} {% endblock %} \ No newline at end of file diff --git a/templates/public/home.html.twig b/templates/public/home.html.twig index 437bc69..bdca319 100644 --- a/templates/public/home.html.twig +++ b/templates/public/home.html.twig @@ -100,7 +100,7 @@
- +

Villes disponibles

Visualisez un tableau de bord de la complétion des commerces et autres lieux d'intérêt pour votre ville grâce à OpenStreetMap

@@ -238,7 +238,12 @@ }, 500); }); enableLabourageForm(); - + + function displayStatsBubble(){ + const statsBubble = document.querySelector('#stats_bubble'); + + } + }); {% endblock %}