osm-commerces/templates/admin/stats.html.twig
2025-06-18 00:41:24 +02:00

965 lines
36 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends 'base.html.twig' %}
{% block title %}{{ 'display.stats'|trans }}- {{ stats.zone }}
{{ stats.name }} {% endblock %}
{% block stylesheets %}
{{ parent() }}
<link href='{{ asset('js/maplibre/maplibre-gl.css') }}' rel='stylesheet' />
<style>
.completion-circle {
fill-opacity: 0.6;
stroke: #fff;
stroke-width: 3;
}
#distribution_completion {
height: 300px;
margin: 20px 0;
}
.completion-info {
margin-bottom: 2rem;
}
</style>
{% endblock %}
{% block body %}
<div class="container">
<div class="mt-4 p-4">
<div class="row">
<div class="col-md-6 col-12">
<h1 class="title">{{ 'display.stats'|trans }} - {{ stats.zone }}
{{ stats.name }} - {{ stats.completionPercent }}% complété</h1>
</div>
<div class="col-md-6 col-12">
<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>
</div>
</div>
{% if stats.population %}
<div class="row mb-3">
<div class="col-md-4 col-12">
<span class="badge bg-info">
<i class="bi bi-people"></i> Population&nbsp;: {{ stats.population|number_format(0, '.', ' ') }}
</span>
</div>
<div class="col-md-4 col-12">
<span class="badge bg-secondary">
<i class="bi bi-shop"></i> 1 lieu pour
{% set ratio = (stats.population and stats.places|length > 0) ? (stats.population / stats.places|length)|round(0, 'ceil') : '?' %}
{{ ratio|number_format(0, '.', ' ') }} habitants
</span>
</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 }} lieux avec note
</span>
</div>
</div>
{% endif %}
<div class="row">
<div class="col-md-3 col-12">
<span class="badge {% if stats.getCompletionPercent() > 85 %}bg-success{% else %}bg-warning{% endif %}">
{{ stats.getCompletionPercent() }} %
</span>
complété sur les critères donnés.
</div>
<div class="col-md-3 col-12">
<span class="badge bg-primary">
<i class="bi bi-building"></i> {{ stats.places | length}}
</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>
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>
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>
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>
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>
lieux avec note renseignée.
</div>
</div>
<div id="maploader">
<div class="spinner-border" role="status">
<i class="bi bi-load bi-spin"></i>
<span class="visually-hidden">Chargement de la carte...</span>
</div>
</div>
<div class="d-flex justify-content-end mb-2">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-primary" id="circleMarkersBtn">
<i class="bi bi-circle"></i> Cercles
</button>
<button type="button" class="btn btn-outline-primary active" id="dropMarkersBtn">
<i class="bi bi-geo-alt"></i> Gouttes
</button>
</div>
</div>
<div id="map" class="mt-4" style="height: 400px;"></div>
<div id="attribution">
<a href="https://www.openstreetmap.org/copyright">Données OpenStreetMap</a>
</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', {'insee_code': stats.zone}) }}" class="btn btn-primary">
<i class="bi bi-filetype-csv"></i>
Exporter en CSV
</a>
</div>
</div>
<table class="table table-bordered table-striped table-hover table-responsive js-sort-table">
{% include 'admin/stats/table-head.html.twig' %}
<tbody>
{% for commerce in stats.places %}
{% include 'admin/stats/row.html.twig' %}
{% endfor %}
</tbody>
</table>
</div>
<div class="card mt-4">
<div class="card-header">
<h2>Requête Overpass</h2>
</div>
<div class="card-body">
<pre class="p-4 bg-light">
{{query_places|raw}}
</pre>
<a href="https://overpass-turbo.eu/?Q={{ query_places|url_encode }}" class="btn btn-primary" target="_blank">
<i class="bi bi-box-arrow-up-right"></i> Exécuter dans Overpass Turbo
</a>
</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>
{% 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 src="{{ asset('js/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);
}
});
function toggleMarkerType() {
currentMarkerType = currentMarkerType === 'drop' ? 'circle' : 'drop';
loadPlaces();
}
// Gestionnaire d'événements pour le bouton de changement de type de marqueur
document.getElementById('circleMarkersBtn')?.addEventListener('click', toggleMarkerType);
document.getElementById('dropMarkersBtn')?.addEventListener('click', toggleMarkerType);
// Charger les lieux au démarrage
loadPlaces();
});
</script>
<script>
const request = `{{query_places|raw}}`;
const zip_code = `{{stats.zone}}`;
let mapElements = [];
let map_is_loaded = false;
let features = [];
let maplibre;
let map;
let overpassData = {}; // Stockage des données Overpass
let currentMarkerType = 'drop'; // Type de marqueur actuel
let dropMarkers = []; // Tableau pour stocker les marqueurs en goutte
let contextMenu = null; // Menu contextuel
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);
}
});
return {
percentage: (completionCount / totalFields) * 100,
missingFields: missingFields
};
}
// Fonctions utilitaires pour la gestion des marqueurs et popups sur la carte
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 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 btn-sm" href="https://openstreetmap.org/${element.type}/${element.id}" target="_blank">
<i class="bi bi-map"></i> OSM
</a>
</div>
</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 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.)
const buckets = Array(11).fill(0);
elements.forEach(element => {
const completion = calculateCompletion(element);
const bucketIndex = Math.floor(completion.percentage / 10);
buckets[bucketIndex]++;
});
return buckets;
}
function createCompletionChart(elements) {
const distribution = calculateCompletionDistribution(elements);
const ctx = document.createElement('canvas');
document.getElementById('distribution_completion').appendChild(ctx);
new Chart(ctx, {
type: 'line',
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 lieux',
data: distribution,
backgroundColor: 'rgba(0, 128, 0, 0.1)',
borderColor: 'rgba(0, 128, 0, 1)',
borderWidth: 2,
tension: 0.4,
fill: true,
pointBackgroundColor: 'rgba(0, 128, 0, 1)',
pointBorderColor: '#fff',
pointBorderWidth: 2,
pointRadius: 4,
pointHoverRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Nombre de lieux'
}
},
x: {
title: {
display: true,
text: 'Taux de complétion'
}
}
},
plugins: {
title: {
display: true,
text: 'Distribution du taux de complétion des lieux'
}
}
}
});
}
function createJOSMQuery(elements){
let query = '';
elements.forEach(element => {
query += `${element.type}${element.id},`;
});
// Enlever la virgule finale
query = query.replace(/,$/, '');
console.log('josm query', query);
return query;
}
let josm_elements = [];
features = [];
async function loadPlaces(map) {
try {
const request = `{{query_places |raw}}`;
const response = await fetch('https://overpass-api.de/api/interpreter', {
method: 'POST',
body: request
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Données reçues:', data);
// Stocker les données Overpass
data.elements.forEach(element => {
overpassData[element.id] = element;
});
// Mettre à jour les marqueurs
features = [];
josm_elements = [];
data.elements.forEach(element => {
const lat = element.lat || (element.center && element.center.lat);
const lon = element.lon || (element.center && element.center.lon);
if (lat && lon) {
const completion = calculateCompletion(element);
const feature = {
id: `marker-${element.id}`,
type: 'Feature',
properties: {
id: element.id,
name: element.tags?.name || 'Sans nom',
completion: completion.percentage,
center: [lon, lat]
},
geometry: {
type: 'Point',
coordinates: [lon, lat]
}
};
features.push(feature);
josm_elements.push(element);
}
});
// Afficher les marqueurs selon le type actuel
dropMarkers = updateMarkers(features, map, currentMarkerType, dropMarkers, overpassData);
// Ajuster la vue pour inclure tous les points
const points = features.map(f => f.properties.center);
if (points.length > 0) {
const bounds = new maplibregl.LngLatBounds(points[0], points[0]);
points.forEach(point => bounds.extend(point));
if (bounds?._sw && bounds?._ne) {
map.fitBounds(bounds, {
padding: 50,
maxZoom: 15
});
} else {
console.warn('Bounds invalides, utilisation des coordonnées par défaut');
}
}
createCompletionChart(data.elements);
// Cacher le spinner une fois le chargement terminé
document.getElementById('maploader').style.display = 'none';
} catch (error) {
console.error('Erreur lors du chargement des lieux:', error);
}
}
function openJOSMQuery(map, query) {
const bounds = map.getBounds();
const josmUrl = `http://localhost:8111/load_object?` +
`objects=${query}`;
// Créer un élément <a> temporaire
const tempLink = document.createElement('a');
tempLink.style.display = 'none';
document.body.appendChild(tempLink);
console.log('josmUrl', josmUrl);
tempLink.href = josmUrl;
tempLink.click();
document.body.removeChild(tempLink);
}
function openInJOSM() {
if (josm_elements.length === 0) {
alert('Veuillez sélectionner au moins un élément');
return;
}
const query = createJOSMQuery(josm_elements);
console.log('map', map);
openJOSMQuery(map, query);
}
document.addEventListener('DOMContentLoaded', function() {
console.log('DOMContentLoaded');
maplibre = new maplibregl.Map({
container: 'map',
style: 'https://api.maptiler.com/maps/streets-v2/style.json?key={{ maptiler_token }}',
center: [2.3488, 48.8534],
zoom: 12
});
map = maplibre;
// Créer le menu contextuel
contextMenu = document.createElement('div');
contextMenu.id = 'context-menu';
contextMenu.style.display = 'none';
contextMenu.style.position = 'absolute';
contextMenu.style.backgroundColor = 'white';
contextMenu.style.border = '1px solid #ccc';
contextMenu.style.padding = '10px';
contextMenu.style.borderRadius = '4px';
contextMenu.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)';
contextMenu.style.zIndex = '1000';
document.body.appendChild(contextMenu);
// Gestionnaire de clic droit sur la carte
map.on('contextmenu', async function(e) {
e.preventDefault();
// Vérifier si le clic est sur un marqueur
const clickedFeatures = map.queryRenderedFeatures(e.point, {
layers: features.map(f => `marker-${f.properties.id}`)
});
if (clickedFeatures.length > 0) {
return; // Ne rien faire si on clique sur un marqueur
}
// Afficher le menu contextuel
contextMenu.style.display = 'block';
// Obtenir la position de la carte dans la page
const mapContainer = map.getContainer();
const mapRect = mapContainer.getBoundingClientRect();
// Calculer la position relative au conteneur de la carte
const x = e.point.x + mapRect.left;
const y = e.point.y + mapRect.top;
// Positionner le menu contextuel
contextMenu.style.left = `${x}px`;
contextMenu.style.top = `${y}px`;
contextMenu.innerHTML = '<div class="spinner-border spinner-border-sm" role="status"></div> Recherche du code INSEE...';
});
document.getElementById('circleMarkersBtn').addEventListener('click', function() {
currentMarkerType = 'circle';
this.classList.add('active');
document.getElementById('dropMarkersBtn').classList.remove('active');
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');
dropMarkers = updateMarkers(features, map, currentMarkerType, dropMarkers, overpassData);
});
document.getElementById('openInJOSM').addEventListener('click', openInJOSM);
// Attendre que la carte soit chargée avant d'ajouter les écouteurs d'événements
map.on('load', function() {
console.log('Map loaded');
map_is_loaded = true;
// Changer le curseur au survol des marqueurs
map.on('mouseenter', function(e) {
const hoveredFeatures = map.queryRenderedFeatures(e.point, {
layers: features.map(f => `marker-${f.properties.id}`)
});
if (hoveredFeatures.length > 0) {
map.getCanvas().style.cursor = 'pointer';
}
});
map.on('mouseleave', function(e) {
const hoveredFeatures = map.queryRenderedFeatures(e.point, {
layers: features.map(f => `marker-${f.properties.id}`)
});
if (hoveredFeatures.length === 0) {
map.getCanvas().style.cursor = '';
}
});
// Charger les lieux
loadPlaces(map);
});
sortTable();
// Initialiser les popovers pour les cellules de complétion
const completionCells = document.querySelectorAll('.completion-cell');
completionCells.forEach(cell => {
new bootstrap.Popover(cell, {
trigger: 'hover',
html: true
});
// Fermer tous les popovers au clic sur une cellule
cell.addEventListener('click', function(e) {
e.stopPropagation();
completionCells.forEach(otherCell => {
if (otherCell !== cell) {
const popover = bootstrap.Popover.getInstance(otherCell);
if (popover) {
popover.hide();
}
}
});
});
});
// Fermer tous les popovers quand on clique ailleurs
document.addEventListener('click', function(e) {
if (!e.target.closest('.completion-cell')) {
completionCells.forEach(cell => {
const popover = bootstrap.Popover.getInstance(cell);
if (popover) {
popover.hide();
}
});
}
});
});
function toggleCompletionInfo() {
const content = document.getElementById('completionInfoContent');
const icon = document.getElementById('completionInfoIcon');
if (content.style.display === 'none') {
content.style.display = 'block';
icon.classList.remove('bi-chevron-down');
icon.classList.add('bi-chevron-up');
} else {
content.style.display = 'none';
icon.classList.remove('bi-chevron-up');
icon.classList.add('bi-chevron-down');
}
}
// infos depuis complète tes commerces : CTC
let CTC_urls = {{ CTC_urls|json_encode|raw }};
console.log('CTC_urls', CTC_urls);
// prendre les infos de CTC_urls.getOSMClosedSirets, on obtient un json. C'est une liste d'objets osm qui ont une propriété "siret".
// on regarde les lignes du tableau des lieux, et on regarde si le siret est dans la liste des sirets clos.
// si oui, on remplit la case des infos en cas de siret clos avec la propriété "vente", puis "radiation", puis "liquidation". avec chacun une icone bootstrap.
// on compte les sirets clos et on remplit l'entête du tableau en comptant.
async function markClosedSiretsOnTable() {
// 1. Récupérer la liste des SIRETs fermés via lAPI
const response = await fetch(CTC_urls.getOSMClosedSirets);
const closedSirets = await response.json(); // [{siret, vente, radiation, liquidation, ...}, ...]
const closedSiretMap = {};
closedSirets.forEach(obj => {
closedSiretMap[obj.siret] = obj;
});
// 2. Parcourir les lignes du tableau des lieux
let closedCount = 0;
document.querySelectorAll('table.lieux-table tbody tr').forEach(row => {
// Supposons que le SIRET est dans une cellule avec une classe ou un data-attribute
const siretCell = row.querySelector('.siret-cell');
if (!siretCell) return;
const siret = siretCell.textContent.trim();
if (closedSiretMap[siret]) {
closedCount++;
const info = closedSiretMap[siret];
// 3. Remplir la case infos avec les statuts
const infoCell = row.querySelector('.infos-cell');
let html = '';
if (info.vente) {
html += `<span title="Vente"><i class="bi bi-cash-coin text-warning"></i> Vente</span> `;
}
if (info.radiation) {
html += `<span title="Radiation"><i class="bi bi-x-circle text-danger"></i> Radiation</span> `;
}
if (info.liquidation) {
html += `<span title="Liquidation"><i class="bi bi-exclamation-triangle text-danger"></i> Liquidation</span> `;
}
infoCell.innerHTML = html;
row.classList.add('table-danger');
}
});
// 4. Mettre à jour lentête du tableau avec le nombre de SIRETs clos
const headerClosed = document.querySelector('.header-closed-count');
if (headerClosed) {
headerClosed.textContent = closedCount;
}
}
console.log('getOSMClosedSirets', CTC_urls.getOSMClosedSirets);
markClosedSiretsOnTable();
</script>
{% endblock %}