refacto labourage form

This commit is contained in:
Tykayn 2025-06-18 00:41:24 +02:00 committed by tykayn
parent 31b2bd4689
commit 5441abd405
12 changed files with 674 additions and 424 deletions

View file

@ -179,6 +179,291 @@ 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 = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 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();
timeoutId = setTimeout(() => {
if (!searchOngoing) {
searchOngoing = true;
performSearch(query);
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 => {
const citySuggestions = data.map(city => ({
name: city.nom,
postcode: city.codesPostaux[0],
insee: city.code
}));
displaySuggestions(citySuggestions);
})
.catch(error => {
console.error('Erreur lors de la recherche:', error);
clearSuggestions();
});
}
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)) {
clearSuggestions();
}
});
// 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 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;
}
// Afficher le spinner
submitButton.disabled = true;
const originalContent = submitButton.innerHTML;
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Labourer...';
// Rediriger
window.location.href = getLabourerUrl(zipCodeInput.value);
}
/**
* 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);
if (!isNaN(percentage)) {
const alpha = percentage / 100;
cell.style.backgroundColor = `rgba(${color}, ${alpha})`;
}
});
}
/**
* 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);
if (!isNaN(value) && value > maxValue) {
maxValue = value;
}
});
// 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
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 => {
item.style.fontSize = fontSize + 'rem';
});
}
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;

View file

View file

@ -1,166 +0,0 @@
// 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);
if (!searchInput || !suggestionList) return;
let timeoutId = null;
searchInput.addEventListener('input', function () {
clearTimeout(timeoutId);
const query = this.value.trim();
if (query.length < 2) {
clearSuggestions();
return;
}
timeoutId = setTimeout(() => performSearch(query), 300);
});
function performSearch(query) {
fetch(`https://geo.api.gouv.fr/communes?nom=${encodeURIComponent(query)}&fields=nom,code,codesPostaux&limit=5`)
.then(response => response.json())
.then(data => {
const citySuggestions = data.map(city => ({
name: city.nom,
postcode: city.codesPostaux[0],
insee: city.code
}));
displaySuggestions(citySuggestions);
})
.catch(error => {
console.error('Erreur lors de la recherche:', error);
clearSuggestions();
});
}
function displaySuggestions(suggestions) {
clearSuggestions();
suggestions.forEach(suggestion => {
const li = document.createElement('li');
li.className = 'list-group-item';
li.textContent = `${suggestion.name} (${suggestion.postcode})`;
li.addEventListener('click', () => {
searchInput.value = suggestion.name;
clearSuggestions();
if (onSelect) onSelect(suggestion);
});
suggestionList.appendChild(li);
});
}
function clearSuggestions() {
suggestionList.innerHTML = '';
}
// Fermer les suggestions en cliquant en dehors
document.addEventListener('click', function (e) {
if (!searchInput.contains(e.target) && !suggestionList.contains(e.target)) {
clearSuggestions();
}
});
}
// 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 getLabourerUrl(zipCode) {
return `/admin/labourer/${zipCode}`;
}
// 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;
}
// Afficher le spinner
submitButton.disabled = true;
const originalContent = submitButton.innerHTML;
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Labourer...';
// Rediriger
window.location.href = getLabourerUrl(zipCodeInput.value);
}
/**
* 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);
if (!isNaN(percentage)) {
const alpha = percentage / 100;
cell.style.backgroundColor = `rgba(${color}, ${alpha})`;
}
});
}
/**
* 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);
if (!isNaN(value) && value > maxValue) {
maxValue = value;
}
});
// 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
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 => {
item.style.fontSize = fontSize + 'rem';
});
}
function check_validity() {
if (!document.getElementById('editLand')) {
return;
}
// ... suite du code ...
}

View file

@ -39,7 +39,7 @@ final class AdminController extends AbstractController
// Récupérer les stats existantes pour la zone
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
$urls = $stats->getAllCTCUrlsMap();
$statsHistory = $this->entityManager->getRepository(StatsHistory::class)
->createQueryBuilder('sh')
@ -90,6 +90,7 @@ final class AdminController extends AbstractController
'maptiler_token' => $_ENV['MAPTILER_TOKEN'],
'mapbox_token' => $_ENV['MAPBOX_TOKEN'],
'statsHistory' => $statsHistory,
'CTC_urls' => $urls,
]);
}

View file

@ -86,6 +86,87 @@ class Stats
#[ORM\Column(nullable: true)]
private ?int $avec_name = null;
public function getCTCurlBase(): ?string
{
$base = 'https://complete-tes-commerces.fr/';
$zone = $this->zone;
$departement = substr($zone, 0, 2);
$insee_code = $zone;
$slug = strtolower(str_replace(' ', '-', $this->getName()));
$url = $base . $departement . '/' . $insee_code . '-'.$slug.'/json/' . $slug ;
return $url;
}
public function getLastStats(): string
{
return $this->getParametricJsonFromCTC('_last_stats');
}
public function getDailyStats(): string
{
return $this->getParametricJsonFromCTC('_dailystats');
}
public function getOSMClosedSirets(): string
{
return $this->getParametricJsonFromCTC('_osm_closed_siret');
}
public function getOSMDisusedShops(): string
{
return $this->getParametricJsonFromCTC('_osm_disused_shops');
}
public function getOSMNoSiteShops(): string
{
return $this->getParametricJsonFromCTC('_osm_no_site_shops');
}
public function getPanoFile(): string
{
return $this->getParametricJsonFromCTC('_pano_file');
}
public function getClosedPanoFile(): string
{
return $this->getParametricJsonFromCTC('_closed_pano_file');
}
public function getSireneMatches(): string
{
return $this->getParametricJsonFromCTC('_sirene_matches');
}
public function getParametricJsonFromCTC($suffixe): string
{
$url = $this->getCTCurlBase().$suffixe.'.json';
return $url;
}
public function getAllCTCUrlsMap(): ?array
{
$functions = [
'getLastStats',
'getDailyStats',
'getOSMClosedSirets',
'getOSMDisusedShops',
'getOSMNoSiteShops',
];
$urls = [];
foreach ($functions as $function) {
$urls[$function] = $this->$function();
}
return $urls;
}
// calcule le pourcentage de complétion de la zone
public function computeCompletionPercent(): ?int
{

View file

@ -22,163 +22,6 @@
</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">
@ -387,6 +230,167 @@
<!-- 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}}`;
@ -896,5 +900,66 @@ window.updateMarkers = updateMarkers;
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 %}
{% endblock %}

View file

@ -86,6 +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>
{# (si siret clos) #}
</td>
<td>
<a href="https://www.openstreetmap.org/{{ commerce.osmKind }}/{{ commerce.osmId }}" title="{{ commerce.osmKind }} - {{ commerce.osmId }} " >
<i class="bi bi-globe"></i>

View file

@ -42,6 +42,10 @@
Siret
</th>
<th>
<i class="bi bi-building"></i>
Siret clos
</th>
<th>
Osm id</th>
<th>

View file

@ -23,7 +23,6 @@
overflow-y: auto;
width: 100%;
z-index: 1000;
display: none;
}
.suggestion-item {
padding: 8px 12px;
@ -50,58 +49,6 @@
</style>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src='{{ asset('js/maplibre/maplibre-gl.js') }}'></script>
<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"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
});
// Colorer les cellules de pourcentage
colorizePercentageCells('td:nth-child(3)');
// colorier selon le nombre de lieux
colorizePercentageCellsRelative('td:nth-child(4)', '154, 205, 50');
const searchInput = document.getElementById('citySearch');
const suggestionList = document.getElementById('citySuggestions');
if (searchInput && suggestionList) {
setupCitySearch('citySearch', 'citySuggestions', function(suggestion) {
document.getElementById('selectedZipCode').value = suggestion.postcode;
});
}
// Attacher le submit du formulaire en JS
const labourerForm = document.getElementById('labourerForm');
if (labourerForm) {
labourerForm.addEventListener('submit', handleAddCityFormSubmit);
}
// Gestionnaire pour les boutons de labourage
document.querySelectorAll('.btn-labourer').forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
console.log('submit', this.dataset);
const zipCode = this.dataset.zipCode;
if (!zipCode) return;
// Désactiver le bouton et ajouter le spinner
this.disabled = true;
const originalContent = this.innerHTML;
this.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Labourer...';
// Rediriger vers l'URL de labourage
window.location.href = getLabourerUrl(zipCode);
});
});
});
</script>
{% endblock %}
{% block body %}
<div class="container">
@ -167,34 +114,30 @@
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<h3 class="card-title">Labourer une ville</h3>
<p class="card-text">
<i class="bi bi-info-circle"
data-bs-toggle="tooltip"
data-bs-placement="right"
data-bs-html="true"
title="<strong>Qu'est-ce que le labourage ?</strong><br><br>Le labourage consiste à rechercher automatiquement tous les commerces et lieux d'intérêt dans une ville sur OpenStreetMap. Cette action permet de :<br><br>- Découvrir les commerces existants<br>- Mesurer leur taux de complétion<br>- Identifier les informations manquantes<br>- Faciliter la mise à jour des données"></i>
Rechercher une ville pour labourer ses commerces
</p>
<form id="labourerForm">
<div class="search-container">
<input type="text"
id="citySearch"
class="form-control"
placeholder="Rechercher une ville..."
autocomplete="off">
<div id="citySuggestions" class="suggestion-list"></div>
</div>
<input type="hidden" name="zip_code" id="selectedZipCode">
<button type="submit" class="btn btn-primary mt-3 btn-labourer">Labourer cette ville</button>
</form>
</div>
</div>
</div>
</div>
{% include 'public/labourage-form.html.twig' %}
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src='{{ asset('js/maplibre/maplibre-gl.js') }}'></script>
<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"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
});
// Colorer les cellules de pourcentage
colorizePercentageCells('td:nth-child(3)');
// colorier selon le nombre de lieux
colorizePercentageCellsRelative('td:nth-child(4)', '154, 205, 50');
enableLabourageForm();
performSearch('dieppe');
});
</script>
{% endblock %}

View file

@ -184,6 +184,7 @@
{% block javascripts %}
{{ parent() }}
<script src="{{ asset('js/mapbox/mapbox-gl.js') }}"></script>
<script src='{{ asset('js/utils.js') }}'></script>
<script>
{% if commerce is not empty and mapbox_token is not empty and maptiler_token is not empty and commerce_overpass['@attributes'].lon is defined and commerce_overpass['@attributes'].lat is defined %}
mapboxgl.accessToken = '{{ mapbox_token }}';

View file

@ -106,24 +106,36 @@
<p>Visualisez un tableau de bord de la complétion des commerces et autres lieux d'intérêt pour votre ville grâce à OpenStreetMap</p>
</div>
{% for stat in stats %}
<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">
{% set sorted_stats = stats|sort((a, b) => a.zone <=> b.zone) %}
{% for stat in sorted_stats %}
{% if stat.zone != 'undefined' %}
<a href="{{ path('app_admin_stats', {'insee_code': stat.zone}) }}" class="list-group-item list-group-item-action d-flex p-4 rounded-3 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>
<div class="d-flex">
<div class="d-flex flex-column">
<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>
{% endif %}
{% endfor %}
{% include 'public/labourage-form.html.twig' %}
</div>
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src='{{ asset('js/utils.js') }}'></script>
<script type="module">
import { adjustListGroupFontSize } from '{{ asset('js/utils.js') }}';
document.addEventListener('DOMContentLoaded', function() {
adjustListGroupFontSize('.list-group-item');
});
</script>
<script>
// Créer le formulaire email
const emailFormHtml = `
@ -225,17 +237,9 @@
});
}, 500);
});
enableLabourageForm();
});
</script>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script type="module">
import { adjustListGroupFontSize } from '{{ asset('js/utils.js') }}';
document.addEventListener('DOMContentLoaded', function() {
adjustListGroupFontSize('.list-group-item');
});
</script>
{% endblock %}

View file

@ -0,0 +1,29 @@
<div class="row mt-4 labourer-form-container" id="labourerFormContainer">
<div class="col-12">
<div class="card">
<div class="card-body">
<h3 class="card-title">Labourer une ville</h3>
<p class="card-text">
<i class="bi bi-info-circle"
data-bs-toggle="tooltip"
data-bs-placement="right"
data-bs-html="true"
title="<strong>Qu'est-ce que le labourage ?</strong><br><br>Le labourage consiste à rechercher automatiquement tous les commerces et lieux d'intérêt dans une ville sur OpenStreetMap. Cette action permet de :<br><br>- Découvrir les commerces existants<br>- Mesurer leur taux de complétion<br>- Identifier les informations manquantes<br>- Faciliter la mise à jour des données"></i>
Rechercher une ville pour labourer ses commerces
</p>
<form id="labourerForm">
<div class="search-container">
<input type="text"
id="citySearch"
class="form-control"
placeholder="Rechercher une ville..."
autocomplete="off">
<div id="citySuggestions" class="suggestion-list"></div>
</div>
<input type="hidden" name="zip_code" id="selectedZipCode">
<button type="submit" class="btn btn-primary mt-3 btn-labourer">Labourer cette ville</button>
</form>
</div>
</div>
</div>
</div>