recup sources

This commit is contained in:
Tykayn 2025-09-01 18:28:23 +02:00 committed by tykayn
parent 86622a19ea
commit 65fe2a35f9
155 changed files with 50969 additions and 0 deletions

360
assets/app.js Normal file
View file

@ -0,0 +1,360 @@
/*
* Welcome to your app's main JavaScript file!
*
* We recommend including the built version of this JavaScript file
* (and its CSS file) in your base layout (base.html.twig).
*/
// any CSS you import will output into a single css file (app.css in this case)
import './styles/app.css';
import jQuery from 'jquery';
window.$ = jQuery;
window.jQuery = jQuery;
import 'tablesort/tablesort.css';
// start the Stimulus application
// import './bootstrap';
import './utils.js';
import './opening_hours.js';
import './josm.js';
import './edit.js';
import './table-map-toggles.js';
import './stats-charts.js';
import './dashboard-charts.js';
import Chart from 'chart.js/auto';
import ChartDataLabels from 'chartjs-plugin-datalabels';
import maplibregl from 'maplibre-gl';
import {
genererCouleurPastel,
setupCitySearch,
handleAddCityFormSubmit,
enableLabourageForm,
getLabourerUrl,
adjustListGroupFontSize,
toggleCompletionInfo,
updateMapHeightForLargeScreens
} from './utils.js';
import tableSortJs from 'table-sort-js/table-sort.js';
import 'chartjs-adapter-date-fns';
console.log('TableSort', tableSortJs)
// Charger table-sortable (version non minifiée locale)
// import '../assets/js/table-sortable.js';
window.Chart = Chart;
window.genererCouleurPastel = genererCouleurPastel;
window.setupCitySearch = setupCitySearch;
window.handleAddCityFormSubmit = handleAddCityFormSubmit;
window.getLabourerUrl = getLabourerUrl;
window.ChartDataLabels = ChartDataLabels;
window.maplibregl = maplibregl;
window.toggleCompletionInfo = toggleCompletionInfo;
window.updateMapHeightForLargeScreens = updateMapHeightForLargeScreens;
// window.Tablesort = Tablesort;
Chart.register(ChartDataLabels);
// Attendre le chargement du DOM
document.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded');
if(updateMapHeightForLargeScreens){
window.addEventListener('resize', updateMapHeightForLargeScreens);
}
const randombg = genererCouleurPastel();
// Appliquer la couleur au body
document.querySelectorAll('body, .edit-land, .body-landing').forEach(element => {
element.style.backgroundColor = randombg;
});
// Gestion du bouton pour afficher tous les champs
const btnShowAllFields = document.querySelector('#showAllFields');
if (btnShowAllFields) {
console.log('btnShowAllFields détecté');
btnShowAllFields.addEventListener('click', () => {
// Sélectionner tous les inputs dans le formulaire
const form = document.querySelector('form');
if (form) {
// Sélectionner tous les inputs sauf #validation_messages
const hiddenInputs = form.querySelectorAll('#advanced_tags');
hiddenInputs.forEach(input => {
input.classList.toggle('d-none');
});
}
});
}
const btnClosedCommerce = document.querySelector('#closedCommerce');
if (btnClosedCommerce) {
btnClosedCommerce.addEventListener('click', () => {
if (!confirm('Êtes-vous sûr de vouloir signaler ce commerce comme fermé ?')) {
return;
}
window.location.href = '/closed_commerce/' + document.getElementById('app_public_closed_commerce').value;
});
}
openingHoursFormManager.init();
// Vérifier si l'élément avec l'ID 'userChangesHistory' existe avant d'appeler la fonction
if (document.getElementById('userChangesHistory')) {
listChangesets();
} else {
console.log('userChangesHistory non trouvé');
}
document.querySelectorAll('input[type="text"]').forEach(input => {
input.addEventListener('blur', updateCompletionProgress);
});
const form = document.querySelector('form')
if (form) {
form.addEventListener('submit', check_validity);
updateCompletionProgress()
}
updateCompletionProgress();
// Focus sur le premier champ texte au chargement
// const firstTextInput = document.querySelector('input.form-control');
// if (firstTextInput) {
// firstTextInput.focus();
// console.log('focus sur le premier champ texte', firstTextInput);
// } else {
// console.log('pas de champ texte trouvé');
// }
parseCuisine();
// Modifier la fonction de recherche existante
const searchInput = document.getElementById('app_admin_labourer');
const suggestionList = document.getElementById('suggestionList');
if (searchInput && suggestionList) {
let timeoutId;
searchInput.addEventListener('input', () => {
clearTimeout(timeoutId);
const query = searchInput.value.trim();
if (query.length < 2) {
suggestionList.innerHTML = '';
return;
}
timeoutId = setTimeout(async () => {
const suggestions = await searchInseeCode(query);
suggestionList.innerHTML = '';
if (suggestions.length === 0) {
const li = document.createElement('li');
li.style.cssText = `
padding: 8px 12px;
color: #666;
font-style: italic;
`;
li.textContent = 'Aucun résultat trouvé';
suggestionList.appendChild(li);
return;
}
suggestions.forEach(suggestion => {
const li = document.createElement('li');
li.style.cssText = `
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #eee;
`;
li.textContent = suggestion.label;
li.addEventListener('mouseenter', () => {
li.style.backgroundColor = '#f0f0f0';
});
li.addEventListener('mouseleave', () => {
li.style.backgroundColor = 'white';
});
li.addEventListener('click', () => {
searchInput.value = suggestion.insee;
suggestionList.innerHTML = '';
labourer();
});
suggestionList.appendChild(li);
});
}, 300);
});
}
if(enableLabourageForm){
enableLabourageForm();
}
adjustListGroupFontSize('.list-group-item');
// Activer le tri naturel sur tous les tableaux avec la classe table-sort
if (tableSortJs) {
tableSortJs();
}else{
console.log('pas de tablesort')
}
// Initialisation du tri et filtrage sur les tableaux du dashboard et de la page stats
// if (document.querySelector('#dashboard-table')) {
// $('#dashboard-table').tableSortable({
// pagination: false,
// showPaginationLabel: true,
// searchField: '#dashboard-table-search',
// responsive: false
// });
// }
// if (document.querySelector('#stats-table')) {
// $('#stats-table').tableSortable({
// pagination: false,
// showPaginationLabel: true,
// searchField: '#stats-table-search',
// responsive: false
// });
// }
// Correction pour le formulaire de labourage
const labourerForm = document.getElementById('labourerForm');
if (labourerForm) {
labourerForm.addEventListener('submit', async function(e) {
e.preventDefault();
const zipInput = document.getElementById('selectedZipCode');
const cityInput = document.getElementById('citySearch');
let insee = zipInput.value;
if (!insee && cityInput && cityInput.value.trim().length > 0) {
// Recherche du code INSEE via l'API
const response = await fetch(`https://geo.api.gouv.fr/communes?nom=${encodeURIComponent(cityInput.value.trim())}&fields=nom,code&limit=1`);
const data = await response.json();
if (data.length > 0) {
insee = data[0].code;
}
}
if (insee) {
window.location.href = `/add-city-without-labourage/${insee}`;
} else {
alert('Veuillez sélectionner une ville valide.');
}
});
}
// Ajouter un écouteur pour l'événement 'load' de MapLibre afin d'ajuster la hauteur de la carte
if (window.maplibregl && document.getElementById('map')) {
// On suppose que la carte est initialisée ailleurs et accessible via window.mapInstance
// Sinon, on peut essayer de détecter l'instance automatiquement
let mapInstance = window.mapInstance;
if (!mapInstance && window.maplibreMap) {
mapInstance = window.maplibreMap;
}
// Si l'instance n'est pas trouvée, essayer de la récupérer via une variable globale courante
if (!mapInstance && window.map) {
mapInstance = window.map;
}
if (mapInstance && typeof mapInstance.on === 'function') {
mapInstance.on('load', function() {
updateMapHeightForLargeScreens();
});
}
}
//updateMapHeightForLargeScreens();
console.log('window.followupSeries',window.followupSeries)
if (!window.followupSeries) return;
const series = window.followupSeries;
// Données bornes de recharge
const chargingStationCount = (series['charging_station_count'] || []).map(point => ({ x: point.date, y: point.value }));
const chargingStationCompletion = (series['charging_station_completion'] || []).map(point => ({ x: point.date, y: point.value }));
// Données bornes incendie
const fireHydrantCount = (series['fire_hydrant_count'] || []).map(point => ({ x: point.date, y: point.value }));
const fireHydrantCompletion = (series['fire_hydrant_completion'] || []).map(point => ({ x: point.date, y: point.value }));
// Graphique bornes de recharge
const chargingStationChart = document.getElementById('chargingStationChart');
if (chargingStationChart) {
new Chart(chargingStationChart, {
type: 'line',
data: {
datasets: [
{
label: 'Nombre de bornes de recharge',
data: chargingStationCount,
borderColor: 'blue',
backgroundColor: 'rgba(0,0,255,0.1)',
fill: false,
yAxisID: 'y',
},
{
label: 'Complétion (%)',
data: chargingStationCompletion,
borderColor: 'green',
backgroundColor: 'rgba(0,255,0,0.1)',
fill: false,
yAxisID: 'y1',
}
]
},
options: {
parsing: false,
responsive: true,
scales: {
x: { type: 'time', time: { unit: 'day' }, title: { display: true, text: 'Date' } },
y: { beginAtZero: true, title: { display: true, text: 'Nombre' } },
y1: { beginAtZero: true, position: 'right', title: { display: true, text: 'Complétion (%)' }, grid: { drawOnChartArea: false } }
}
}
});
}
// Graphique bornes incendie
const fireHydrantChart = document.getElementById('fireHydrantChart');
if (fireHydrantChart) {
new Chart(fireHydrantChart, {
type: 'line',
data: {
datasets: [
{
label: 'Nombre de bornes incendie',
data: fireHydrantCount,
borderColor: 'red',
backgroundColor: 'rgba(255,0,0,0.1)',
fill: false,
yAxisID: 'y',
},
{
label: 'Complétion (%)',
data: fireHydrantCompletion,
borderColor: 'orange',
backgroundColor: 'rgba(255,165,0,0.1)',
fill: false,
yAxisID: 'y1',
}
]
},
options: {
parsing: false,
responsive: true,
scales: {
x: { type: 'time', time: { unit: 'day' }, title: { display: true, text: 'Date' } },
y: { beginAtZero: true, title: { display: true, text: 'Nombre' } },
y1: { beginAtZero: true, position: 'right', title: { display: true, text: 'Complétion (%)' }, grid: { drawOnChartArea: false } }
}
}
});
}
});

5
assets/bootstrap.js vendored Normal file
View file

@ -0,0 +1,5 @@
import { startStimulusApp } from '@symfony/stimulus-bundle';
const app = startStimulusApp();
// register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController);

15
assets/controllers.json Normal file
View file

@ -0,0 +1,15 @@
{
"controllers": {
"@symfony/ux-turbo": {
"turbo-core": {
"enabled": true,
"fetch": "eager"
},
"mercure-turbo-stream": {
"enabled": false,
"fetch": "eager"
}
}
},
"entrypoints": []
}

View file

@ -0,0 +1,79 @@
const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
const tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/;
// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
document.addEventListener('submit', function (event) {
generateCsrfToken(event.target);
}, true);
// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie
// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked
document.addEventListener('turbo:submit-start', function (event) {
const h = generateCsrfHeaders(event.detail.formSubmission.formElement);
Object.keys(h).map(function (k) {
event.detail.formSubmission.fetchRequest.headers[k] = h[k];
});
});
// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
document.addEventListener('turbo:submit-end', function (event) {
removeCsrfToken(event.detail.formSubmission.formElement);
});
export function generateCsrfToken (formElement) {
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
if (!csrfField) {
return;
}
let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
let csrfToken = csrfField.value;
if (!csrfCookie && nameCheck.test(csrfToken)) {
csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
}
csrfField.dispatchEvent(new Event('change', { bubbles: true }));
if (csrfCookie && tokenCheck.test(csrfToken)) {
const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
}
}
export function generateCsrfHeaders (formElement) {
const headers = {};
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
if (!csrfField) {
return headers;
}
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
headers[csrfCookie] = csrfField.value;
}
return headers;
}
export function removeCsrfToken (formElement) {
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
if (!csrfField) {
return;
}
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
const cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0';
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
}
}
/* stimulusFetch: 'lazy' */
export default 'csrf-protection-controller';

View file

@ -0,0 +1,16 @@
import { Controller } from '@hotwired/stimulus';
/*
* This is an example Stimulus controller!
*
* Any element with a data-controller="hello" attribute will cause
* this controller to be executed. The name "hello" comes from the filename:
* hello_controller.js -> "hello"
*
* Delete this file or adapt it for your use!
*/
export default class extends Controller {
connect() {
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
}
}

229
assets/dashboard-charts.js Normal file
View file

@ -0,0 +1,229 @@
// Bubble chart du dashboard avec option de taille de bulle proportionnelle ou égale
let bubbleChart = null; // Déclaré en dehors pour garder la référence
function waitForChartAndDrawBubble() {
if (!window.Chart || !window.ChartDataLabels) {
setTimeout(waitForChartAndDrawBubble, 50);
return;
}
const chartCanvas = document.getElementById('bubbleChart');
const toggle = document.getElementById('toggleBubbleSize');
function drawBubbleChart(proportional) {
// Détruire toute instance Chart.js existante sur ce canvas (Chart.js v3+)
const existing = window.Chart.getChart(chartCanvas);
if (existing) {
try { existing.destroy(); } catch (e) { console.warn('Erreur destroy Chart:', e); }
}
if (bubbleChart) {
try { bubbleChart.destroy(); } catch (e) { console.warn('Erreur destroy Chart:', e); }
bubbleChart = null;
}
// Forcer le canvas à occuper toute la largeur/hauteur du conteneur en pixels
if (chartCanvas && chartCanvas.parentElement) {
const parentRect = chartCanvas.parentElement.getBoundingClientRect();
console.log('parentRect', parentRect)
chartCanvas.width = (parentRect.width);
chartCanvas.height = (parentRect.height);
chartCanvas.style.width = parentRect.width + 'px';
chartCanvas.style.height = parentRect.height + 'px';
}
if(!getBubbleData){
console.log('pas de getBubbleData')
return ;
}
const bubbleChartData = getBubbleData(proportional);
if(!bubbleChartData){
console.log('pas de bubbleChartData')
return ;
}
// Calcul de la régression linéaire (moindres carrés)
// On ne fait la régression que si on veut, mais l'axe X = fraicheur, Y = complétion
const validPoints = bubbleChartData.filter(d => d.x !== null && d.y !== null);
const n = validPoints.length;
let regressionLine = null, slope = 0, intercept = 0;
if (n >= 2) {
let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
validPoints.forEach(d => {
sumX += d.x;
sumY += d.y;
sumXY += d.x * d.y;
sumXX += d.x * d.x;
});
const meanX = sumX / n;
const meanY = sumY / n;
slope = (sumXY - n * meanX * meanY) / (sumXX - n * meanX * meanX);
intercept = meanY - slope * meanX;
const xMin = Math.min(...validPoints.map(d => d.x));
const xMax = Math.max(...validPoints.map(d => d.x));
regressionLine = [
{ x: xMin, y: slope * xMin + intercept },
{ x: xMax, y: slope * xMax + intercept }
];
}
window.Chart.register(window.ChartDataLabels);
bubbleChart = new window.Chart(chartCanvas.getContext('2d'), {
type: 'bubble',
data: {
datasets: [
{
label: 'Villes',
data: bubbleChartData,
backgroundColor: bubbleChartData.map(d => `rgba(94, 255, 121, ${d.completion / 100})`),
borderColor: 'rgb(94, 255, 121)',
datalabels: {
anchor: 'center',
align: 'center',
color: '#000',
display: true,
font: { weight: '400', size : "12px" },
formatter: (value, context) => {
return context.dataset.data[context.dataIndex].label;
}
}
},
regressionLine ? {
label: 'Régression linéaire',
type: 'line',
data: regressionLine,
borderColor: 'rgba(95, 168, 0, 0.7)',
borderWidth: 2,
pointRadius: 0,
fill: false,
order: 0,
tension: 0,
datalabels: { display: false }
} : null
].filter(Boolean)
},
options: {
plugins: {
datalabels: {
display: false
},
legend: { display: true },
tooltip: {
callbacks: {
label: (context) => {
const d = context.raw;
if (context.dataset.type === 'line') {
return `Régression: y = ${slope.toFixed(2)} × x + ${intercept.toFixed(2)}`;
}
return [
`${d.label}`,
`Fraîcheur moyenne: ${d.freshnessDays ? d.freshnessDays.toLocaleString() + ' jours' : 'N/A'}`,
`Complétion: ${d.y.toFixed(2)}%`,
`Population: ${d.population ? d.population.toLocaleString() : 'N/A'}`,
`Nombre de lieux: ${d.r.toFixed(2)}`,
`Budget: ${d.budget ? d.budget.toLocaleString() + ' €' : 'N/A'}`,
`Budget/habitant: ${d.budgetParHabitant ? d.budgetParHabitant.toFixed(2) + ' €' : 'N/A'}`,
`Budget/lieu: ${d.budgetParLieu ? d.budgetParLieu.toFixed(2) + ' €' : 'N/A'}`
];
}
}
}
},
scales: {
x: {
type: 'linear',
title: { display: true, text: 'Fraîcheur moyenne (jours, plus petit = plus récent)' }
},
y: {
title: { display: true, text: 'Taux de complétion (%)' },
min: 0,
max: 100
}
}
}
});
// Ajout du clic sur une bulle
chartCanvas.onclick = function(evt) {
const points = bubbleChart.getElementsAtEventForMode(evt, 'nearest', { intersect: true }, true);
if (points.length > 0) {
const firstPoint = points[0];
const dataIndex = firstPoint.index;
const stat = window.statsDataForBubble[dataIndex];
if (stat && stat.zone) {
window.location.href = '/admin/stats/' + stat.zone;
}
}
};
}
// Initial draw
console.log('[bubble chart] Initialisation avec taille proportionnelle ?', toggle?.checked);
if(drawBubbleChart){
drawBubbleChart(toggle && toggle.checked);
// Listener
toggle?.addEventListener('change', function() {
console.log('[bubble chart] Toggle changé, taille proportionnelle ?', toggle?.checked);
drawBubbleChart(toggle?.checked);
});
}
}
function getBubbleData(proportional) {
// Générer les données puis trier par rayon décroissant
const data = window.statsDataForBubble?.map(stat => {
const population = parseInt(stat.population, 10);
const placesCount = parseInt(stat.placesCount, 10);
const completion = parseInt(stat.completionPercent, 10);
// Fraîcheur moyenne : âge moyen en jours (plus récent à droite)
let freshnessDays = null;
if (stat.osmDataDateAvg) {
const now = new Date();
const avgDate = new Date(stat.osmDataDateAvg);
freshnessDays = Math.round((now - avgDate) / (1000 * 60 * 60 * 24));
}
// Pour l'axe X, on veut que les plus récents soient à droite (donc X = -freshnessDays)
const x = freshnessDays !== null ? -freshnessDays : 0;
// Budget
const budget = stat.budgetAnnuel ? parseFloat(stat.budgetAnnuel) : null;
const budgetParHabitant = (budget && population) ? budget / population : null;
const budgetParLieu = (budget && placesCount) ? budget / placesCount : null;
return {
x: x,
y: completion,
r: proportional ? Math.sqrt(placesCount) * 2 : 12,
label: stat.name,
completion: stat.completionPercent || 0,
zone: stat.zone,
budget,
budgetParHabitant,
budgetParLieu,
population,
placesCount,
freshnessDays
};
});
// Trier du plus gros au plus petit rayon
if(data){
data.sort((a, b) => b.r - a.r);
}
return data;
}
document.addEventListener('DOMContentLoaded', function() {
waitForChartAndDrawBubble();
// Forcer deleteMissing=1 sur le formulaire de labourage
const labourerForm = document.getElementById('labourerForm');
if (labourerForm) {
labourerForm.addEventListener('submit', function(e) {
e.preventDefault();
const zipCode = document.getElementById('selectedZipCode').value;
if (zipCode) {
window.location.href = '/admin/labourer/' + zipCode + '?deleteMissing=1';
}
});
}
});

126
assets/edit.js Normal file
View file

@ -0,0 +1,126 @@
/**
* mettre à jour la barre de progression
* pour le formulaire de modification
*/
function updateCompletionProgress() {
const inputs = document.querySelectorAll('input[data-important]');
let filledInputs = 0;
let totalInputs = inputs.length;
let missingFields = [];
inputs.forEach(input => {
if (input.value.trim() !== '') {
filledInputs++;
} else {
// Get the field label or name for display in the popup
let fieldName = '';
const label = input.closest('.row')?.querySelector('.form-label, .label-translated');
if (label) {
fieldName = label.textContent.trim();
} else {
// If no label found, try to get a meaningful name from the input
const name = input.getAttribute('name');
if (name) {
// Extract field name from the attribute (e.g., commerce_tag_value__contact:email -> Email)
const parts = name.split('__');
if (parts.length > 1) {
fieldName = parts[1].replace('commerce_tag_value_', '').replace('contact:', '');
// Capitalize first letter
fieldName = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
} else {
fieldName = name;
}
}
}
missingFields.push(fieldName || input.getAttribute('name') || 'Champ inconnu');
}
});
const completionPercentage = (filledInputs / totalInputs) * 100;
const progressBar = document.querySelector('#completion_progress .progress-bar');
if (progressBar) {
progressBar.style.width = completionPercentage + '%';
progressBar.setAttribute('aria-valuenow', completionPercentage);
// Create the completion display with a clickable question mark
const displayElement = document.querySelector('#completion_display');
// Format missing fields as an HTML list for better readability
let missingFieldsContent = '';
if (missingFields.length > 0) {
missingFieldsContent = '<ul class="list-unstyled mb-0">';
// Filter out empty or undefined field names and sort them alphabetically
missingFields
.filter(field => field && field.trim() !== '')
.sort()
.forEach(field => {
missingFieldsContent += `<li><i class="bi bi-exclamation-circle text-warning"></i> ${field}</li>`;
});
missingFieldsContent += '</ul>';
} else {
missingFieldsContent = 'Tous les champs importants sont remplis !';
}
displayElement.innerHTML = `Votre commerce est complété à ${Math.round(completionPercentage)}% <a href="#" class="missing-fields-info badge rounded-pill bg-warning text-dark ms-1" style="text-decoration: none; font-weight: bold;" data-bs-toggle="popover" data-bs-placement="bottom" title="Champs manquants" data-bs-html="true" data-bs-content="${missingFieldsContent.replace(/"/g, '&quot;')}">?</a>`;
// Initialize the Bootstrap popover
const popoverTrigger = displayElement.querySelector('.missing-fields-info');
if (popoverTrigger) {
// Add click handler to focus on the first missing field
popoverTrigger.addEventListener('click', function(e) {
e.preventDefault(); // Prevent scrolling to top
// Find the first missing field
const missingInput = document.querySelector('input[data-important]:not(.good_filled)');
if (missingInput) {
// Focus on the first missing field
missingInput.focus();
// Scroll to the field if needed
missingInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
// Use setTimeout to ensure this runs after the current execution context
setTimeout(() => {
if (typeof bootstrap !== 'undefined' && bootstrap.Popover) {
// Destroy existing popover if any
const existingPopover = bootstrap.Popover.getInstance(popoverTrigger);
if (existingPopover) {
existingPopover.dispose();
}
// Initialize new popover
new bootstrap.Popover(popoverTrigger, {
html: true,
trigger: 'click',
container: 'body'
});
} else {
console.warn('Bootstrap popover not available');
}
}, 0);
}
}
}
function parseCuisine() {
const cuisineInput = document.querySelector('input[name="commerce_tag_value__cuisine"]');
// Récupérer tous les checkboxes de type de cuisine
const cuisineCheckboxes = document.querySelectorAll('input[name="cuisine_type"]');
// Ajouter un écouteur d'événement sur chaque checkbox
cuisineCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', () => {
// Récupérer toutes les checkboxes cochées
const checkedCuisines = Array.from(document.querySelectorAll('input[name="cuisine_type"]:checked'))
.map(input => input.value);
// Mettre à jour l'input avec les valeurs séparées par des points-virgules
cuisineInput.value = checkedCuisines.join(';');
});
});
}
window.updateCompletionProgress = updateCompletionProgress;
window.parseCuisine = parseCuisine;

77
assets/josm.js Normal file
View file

@ -0,0 +1,77 @@
/**
* Ouvre les éléments sélectionnés dans JOSM
* @param {Object} map - L'instance de la carte MapLibre
* @param {boolean} map_is_loaded - État de chargement de la carte
* @param {Array|Object} osmElements - Élément(s) OSM à ouvrir (peut être un tableau d'objets ou un objet unique)
* @returns {void}
*/
function openInJOSM(map, map_is_loaded, osmElements) {
if (!map_is_loaded) {
alert('Veuillez attendre que la carte soit chargée');
return;
}
if (!osmElements || (Array.isArray(osmElements) && osmElements.length === 0)) {
alert('Aucun élément à ouvrir dans JOSM');
return;
}
const bounds = map.getBounds();
// Convertir en tableau si c'est un seul élément
const elements = Array.isArray(osmElements) ? osmElements : [osmElements];
// Séparer les éléments par type
const nodeIds = [];
const wayIds = [];
const relationIds = [];
elements.forEach(element => {
if (typeof element === 'string') {
// Si c'est juste un ID, on le traite comme un node par défaut
nodeIds.push(element);
} else {
// Sinon on utilise le type spécifié
switch (element.osm_type) {
case 'node':
nodeIds.push(element.osm_id);
break;
case 'way':
wayIds.push(element.osm_id);
break;
case 'relation':
relationIds.push(element.osm_id);
break;
}
}
});
// Construire les paramètres de sélection
const selectParams = [];
if (nodeIds.length > 0) selectParams.push(`node${nodeIds.join(',')}`);
if (wayIds.length > 0) selectParams.push(`way${wayIds.join(',')}`);
if (relationIds.length > 0) selectParams.push(`relation${relationIds.join(',')}`);
// Construire l'URL JOSM avec les paramètres de la boîte englobante
const josmUrl = `http://localhost:8111/load_and_zoom?` +
`left=${bounds.getWest()}&` +
`right=${bounds.getEast()}&` +
`top=${bounds.getNorth()}&` +
`bottom=${bounds.getSouth()}&` +
`select=${selectParams.join(',')}`;
// Utiliser le bouton caché pour ouvrir JOSM
// Créer un élément <a> temporaire
const tempLink = document.createElement('a');
tempLink.style.display = 'none';
document.body.appendChild(tempLink);
tempLink.href = josmUrl;
console.log('josmUrl', josmUrl);
tempLink.click();
document.body.removeChild(tempLink);
}
window.openInJOSM = openInJOSM;

0
assets/js/map-utils.js Normal file
View file

File diff suppressed because one or more lines are too long

0
assets/js/table-sortable.min.js vendored Normal file
View file

683
assets/opening_hours.js Normal file
View file

@ -0,0 +1,683 @@
/**
* Gestion du formulaire d'horaires d'ouverture
*
*/
const openingHoursFormManager = {
defaultOpeningHours: '',
inputSelector: '',
init: function (inputSelector = 'input[name="custom__opening_hours"]') {
// Rechercher l'élément par son attribut name plutôt que par son id
this.setInputSelector(inputSelector);
const openingHoursInput = document.querySelector(inputSelector);
if (!openingHoursInput) {
console.warn('Élément ', inputSelector, ' non trouvé');
return;
}
this.defaultOpeningHours = openingHoursInput.value;
// Créer la div de rendu si elle n'existe pas
let renderDiv = document.getElementById('opening_hours_render');
if (!renderDiv) {
renderDiv = document.createElement('div');
renderDiv.id = 'opening_hours_render';
renderDiv.classList.add('mt-4');
openingHoursInput.parentNode.insertBefore(renderDiv, openingHoursInput.nextSibling);
}
this.makeForm(inputSelector);
if (this.defaultOpeningHours !== '') {
this.parseOpeningHoursValue(inputSelector);
}
// Ajouter un écouteur d'événement keyup sur l'input des horaires
openingHoursInput.addEventListener('keyup', () => {
this.defaultOpeningHours = openingHoursInput.value;
this.parseOpeningHoursValue(inputSelector);
});
},
setInputSelector: function (inputSelector) {
this.inputSelector = inputSelector;
},
/**
* convertir les checkboxes et inputs en horaires OSM dans l'input de référence
* @param {string} inputSelector
*/
parseOpeningHoursValue: function (inputSelector = 'input[name="custom__opening_hours"]') {
// Analyser la chaîne d'horaires d'ouverture
const parsedOpeningHours = [];
// Masquer toutes les plages horaires par défaut
const allDayContainers = document.querySelectorAll('.jour-container');
allDayContainers.forEach(container => {
const checkbox = container.querySelector('input[type="checkbox"]');
const horairesContainer = container.querySelector('.horaires-container');
checkbox.checked = false;
horairesContainer.classList.add('d-none');
});
if (this.defaultOpeningHours) {
// Diviser les différentes règles (séparées par des points-virgules)
const rules = this.defaultOpeningHours.split(';').map(r => r.trim());
rules.forEach(rule => {
// Extraire les jours et les heures
const parts = rule.split(' ').filter(Boolean);
const days = parts[0];
const hours = parts.slice(1).join(' ');
// Convertir les jours en français
const daysMap = {
'Mo': 'lundi',
'Tu': 'mardi',
'We': 'mercredi',
'Th': 'jeudi',
'Fr': 'vendredi',
'Sa': 'samedi',
'Su': 'dimanche'
};
// Gérer les plages de jours (ex: Mo-Fr)
if (days.includes('-')) {
const [start, end] = days.split('-');
const startIndex = Object.keys(daysMap).indexOf(start);
const endIndex = Object.keys(daysMap).indexOf(end);
const dayRange = [];
for (let i = startIndex; i <= endIndex; i++) {
const day = Object.keys(daysMap)[i];
dayRange.push(day);
// Cocher la case du jour
const checkbox = document.querySelector(`#jour-${daysMap[day]}`);
if (checkbox) {
checkbox.checked = true;
const horairesContainer = checkbox.closest('.jour-container').querySelector('.horaires-container');
horairesContainer.classList.remove('d-none');
// Gérer le cas "fermé"
if (hours === 'off') {
const fermeCheckbox = horairesContainer.querySelector(`input[name="${daysMap[day]}-ferme"]`);
if (fermeCheckbox) {
fermeCheckbox.checked = true;
// Décocher les plages horaires
const plageInputs = horairesContainer.querySelectorAll('.time-range input[type="checkbox"]');
plageInputs.forEach(plageInput => {
plageInput.checked = false;
plageInput.dispatchEvent(new Event('change'));
});
}
} else {
// Décocher la deuxième plage si elle n'est pas présente dans l'input
if (hours && !hours.includes(',')) {
const plage2Checkbox = horairesContainer.querySelector(`input[name="${daysMap[day]}-plage2-active"]`);
if (plage2Checkbox) {
plage2Checkbox.checked = false;
}
}
// Remplir la première plage horaire si spécifiée
if (hours) {
const [startTime, endTime] = hours.split('-');
if (startTime && endTime) {
const [startHour, startMinute] = startTime.split(':');
const [endHour, endMinute] = endTime.split(':');
const startHourInput = horairesContainer.querySelector(`input[name="${daysMap[day]}-plage1-start-hour"]`);
const startMinuteInput = horairesContainer.querySelector(`input[name="${daysMap[day]}-plage1-start-minute"]`);
const endHourInput = horairesContainer.querySelector(`input[name="${daysMap[day]}-plage1-end-hour"]`);
const endMinuteInput = horairesContainer.querySelector(`input[name="${daysMap[day]}-plage1-end-minute"]`);
if (startHourInput) startHourInput.value = startHour;
if (startMinuteInput) startMinuteInput.value = startMinute;
if (endHourInput) endHourInput.value = endHour;
if (endMinuteInput) endMinuteInput.value = endMinute;
}
}
}
}
}
parsedOpeningHours.push({
days: dayRange,
hours: hours
});
} else {
// Jour unique
const day = daysMap[days];
const checkbox = document.querySelector(`#jour-${day}`);
if (checkbox) {
checkbox.checked = true;
const horairesContainer = checkbox.closest('.jour-container').querySelector('.horaires-container');
horairesContainer.classList.remove('d-none');
// Gérer le cas "fermé"
if (hours === 'off') {
const fermeCheckbox = horairesContainer.querySelector(`input[name="${day}-ferme"]`);
if (fermeCheckbox) {
fermeCheckbox.checked = true;
// Décocher les plages horaires
const plageInputs = horairesContainer.querySelectorAll('.time-range input[type="checkbox"]');
plageInputs.forEach(plageInput => {
plageInput.checked = false;
plageInput.dispatchEvent(new Event('change'));
});
}
} else {
// Décocher la deuxième plage si elle n'est pas présente dans l'input
if (hours && !hours.includes(',')) {
const plage2Checkbox = horairesContainer.querySelector(`input[name="${day}-plage2-active"]`);
if (plage2Checkbox) {
plage2Checkbox.checked = false;
}
}
// Remplir la première plage horaire si spécifiée
if (hours) {
const [startTime, endTime] = hours.split('-');
if (startTime && endTime) {
const [startHour, startMinute] = startTime.split(':');
const [endHour, endMinute] = endTime.split(':');
const startHourInput = horairesContainer.querySelector(`input[name="${day}-plage1-start-hour"]`);
const startMinuteInput = horairesContainer.querySelector(`input[name="${day}-plage1-start-minute"]`);
const endHourInput = horairesContainer.querySelector(`input[name="${day}-plage1-end-hour"]`);
const endMinuteInput = horairesContainer.querySelector(`input[name="${day}-plage1-end-minute"]`);
if (startHourInput) startHourInput.value = startHour;
if (startMinuteInput) startMinuteInput.value = startMinute;
if (endHourInput) endHourInput.value = endHour;
if (endMinuteInput) endMinuteInput.value = endMinute;
}
}
}
}
parsedOpeningHours.push({
days: [days],
hours: hours
});
}
});
}
this.renderOpeningHours(parsedOpeningHours);
console.log(parsedOpeningHours);
},
makeForm: function (inputSelector = 'input[name="custom__opening_hours"]') {
const customOpeningHours = document.querySelector(inputSelector);
console.log('makeForm customOpeningHours', customOpeningHours);
if (customOpeningHours) {
// Créer un conteneur flex pour aligner l'input et le formulaire
const container = document.createElement('div');
container.classList.add('d-flex', 'flex-column', 'flex-md-row', 'align-items-start', 'gap-3', 'w-100');
// Créer le formulaire
const form = document.createElement('form');
form.id = 'app_public_opening_hours';
form.classList.add('mt-3', 'flex-grow-1');
// Créer les cases à cocher pour chaque jour
const jours = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'];
const joursDiv = document.createElement('div');
joursDiv.classList.add('jours-ouverture', 'mb-4', 'row', 'mx-4');
jours.forEach(jour => {
const jourContainer = document.createElement('div');
jourContainer.classList.add('jour-container', 'col-12');
// Checkbox pour le jour
const divCheck = document.createElement('div');
divCheck.classList.add('form-check', 'mb-2');
const input = document.createElement('input');
input.type = 'checkbox';
input.id = `jour-${jour.toLowerCase()}`;
input.name = `jour-${jour.toLowerCase()}`;
input.classList.add('form-check-input');
const label = document.createElement('label');
label.htmlFor = `jour-${jour.toLowerCase()}`;
label.classList.add('form-check-label');
label.textContent = jour;
divCheck.appendChild(input);
divCheck.appendChild(label);
jourContainer.appendChild(divCheck);
// Conteneur pour les plages horaires
const horairesContainer = document.createElement('div');
horairesContainer.classList.add('horaires-container', 'ml-2', 'd-none', 'row');
// Option "fermé"
const fermeContainer = document.createElement('div');
fermeContainer.classList.add('col-12', 'mb-2');
const fermeCheck = document.createElement('div');
fermeCheck.classList.add('form-check');
const fermeInput = document.createElement('input');
fermeInput.type = 'checkbox';
fermeInput.id = `${jour.toLowerCase()}-ferme`;
fermeInput.name = `${jour.toLowerCase()}-ferme`;
fermeInput.classList.add('form-check-input');
const fermeLabel = document.createElement('label');
fermeLabel.htmlFor = `${jour.toLowerCase()}-ferme`;
fermeLabel.classList.add('form-check-label');
fermeLabel.textContent = 'Fermé';
fermeCheck.appendChild(fermeInput);
fermeCheck.appendChild(fermeLabel);
fermeContainer.appendChild(fermeCheck);
horairesContainer.appendChild(fermeContainer);
// Première plage horaire
const plage1 = this.createTimeRangeInputs(`${jour.toLowerCase()}-plage1`);
horairesContainer.appendChild(plage1);
// Deuxième plage horaire
const plage2 = this.createTimeRangeInputs(`${jour.toLowerCase()}-plage2`);
horairesContainer.appendChild(plage2);
jourContainer.appendChild(horairesContainer);
joursDiv.appendChild(jourContainer);
// Ajouter l'événement pour afficher/masquer les plages horaires
input.addEventListener('change', (e) => {
horairesContainer.classList.toggle('d-none', !e.target.checked);
this.convertToOSMOpeningHours(inputSelector);
});
// Ajouter l'événement pour gérer l'option "fermé"
fermeInput.addEventListener('change', (e) => {
const plageInputs = horairesContainer.querySelectorAll('.time-range input[type="checkbox"]');
plageInputs.forEach(plageInput => {
plageInput.checked = !e.target.checked;
plageInput.dispatchEvent(new Event('change'));
});
this.convertToOSMOpeningHours(inputSelector);
});
});
form.appendChild(joursDiv);
// Ajouter le formulaire au conteneur
container.appendChild(form);
// Insérer le conteneur après l'input original
const parent = customOpeningHours.parentNode;
if (parent) {
parent.insertBefore(container, customOpeningHours.nextSibling);
}
// Ajouter un debounce pour limiter les appels lors des modifications
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
// Appliquer le debounce à la fonction de conversion
const debouncedConvert = debounce(() => {
this.convertToOSMOpeningHours();
}, 300);
// Ajouter les listeners sur tous les inputs
const allInputs = form.querySelectorAll('input');
allInputs.forEach(input => {
input.addEventListener('change', debouncedConvert);
input.addEventListener('input', debouncedConvert);
});
} else {
console.log('pas d input opening hours détecté')
}
},
createTimeRangeInputs: function (prefix) {
const container = document.createElement('div');
container.classList.add('time-range', 'mb-2', 'col-12', 'col-md-6');
// Case à cocher pour activer la plage
const checkboxContainer = document.createElement('div');
checkboxContainer.classList.add('form-check', 'mb-2');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `${prefix}-active`;
checkbox.name = `${prefix}-active`;
checkbox.classList.add('form-check-input');
checkbox.checked = true;
const checkboxLabel = document.createElement('label');
checkboxLabel.htmlFor = `${prefix}-active`;
checkboxLabel.classList.add('form-check-label');
checkboxLabel.textContent = '';
checkboxContainer.appendChild(checkbox);
checkboxContainer.appendChild(checkboxLabel);
container.appendChild(checkboxContainer);
// Conteneur pour les inputs d'horaires
const timeContainer = document.createElement('div');
timeContainer.classList.add('ms-4', 'row', 'g-2');
// Heure de début
const startContainer = document.createElement('div');
startContainer.classList.add('col-6', 'd-flex', 'align-items-center', 'gap-2', 'start-hour');
const startHour = document.createElement('input');
startHour.type = 'number';
startHour.min = '0';
startHour.max = '23';
startHour.classList.add('form-control', 'form-control-sm');
startHour.style.width = '60px';
startHour.placeholder = 'HH';
startHour.name = `${prefix}-start-hour`;
// Définir les horaires par défaut selon la plage
startHour.value = prefix.includes('plage1') ? '08' : '14';
const startMinute = document.createElement('input');
startMinute.type = 'number';
startMinute.min = '0';
startMinute.max = '59';
startMinute.classList.add('form-control', 'form-control-sm');
startMinute.style.width = '60px';
startMinute.placeholder = 'MM';
startMinute.name = `${prefix}-start-minute`;
startMinute.value = '00';
// Heure de fin
const endContainer = document.createElement('div');
endContainer.classList.add('col-6', 'd-flex', 'align-items-center', 'gap-2', 'end-hour');
const endHour = document.createElement('input');
endHour.type = 'number';
endHour.min = '0';
endHour.max = '23';
endHour.classList.add('form-control', 'form-control-sm');
endHour.style.width = '60px';
endHour.placeholder = 'HH';
endHour.name = `${prefix}-end-hour`;
// Définir les horaires par défaut selon la plage
endHour.value = prefix.includes('plage1') ? '12' : '18';
const endMinute = document.createElement('input');
endMinute.type = 'number';
endMinute.min = '0';
endMinute.max = '59';
endMinute.classList.add('form-control', 'form-control-sm');
endMinute.style.width = '60px';
endMinute.placeholder = 'MM';
endMinute.name = `${prefix}-end-minute`;
endMinute.value = '00';
// Créer le texte avec les horaires
const timeText = document.createElement('div');
timeText.classList.add('d-flex', 'align-items-center', 'gap-2', 'flex-wrap');
// Texte de début
const startText = document.createElement('span');
startText.textContent = 'de';
timeText.appendChild(startText);
timeText.appendChild(startHour);
timeText.appendChild(document.createTextNode(':'));
timeText.appendChild(startMinute);
// Texte du milieu
const middleText = document.createElement('span');
middleText.textContent = 'à';
timeText.appendChild(middleText);
// Texte de fin
timeText.appendChild(endHour);
timeText.appendChild(document.createTextNode(':'));
timeText.appendChild(endMinute);
timeContainer.appendChild(timeText);
container.appendChild(timeContainer);
// Fonction pour mettre à jour le style des inputs
const updateInputsStyle = (isActive) => {
const inputs = timeContainer.querySelectorAll('input');
inputs.forEach(input => {
if (isActive) {
input.classList.remove('bg-light', 'text-muted');
input.disabled = false;
} else {
input.classList.add('bg-light', 'text-muted');
input.disabled = true;
}
});
};
// Ajouter l'événement sur la checkbox
checkbox.addEventListener('change', (e) => {
updateInputsStyle(e.target.checked);
});
// Appliquer le style initial
updateInputsStyle(checkbox.checked);
return container;
},
convertToOSMOpeningHours: function (inputSelector = 'input[name="custom__opening_hours"]') {
const jours = {
'Lundi': 'Mo',
'Mardi': 'Tu',
'Mercredi': 'We',
'Jeudi': 'Th',
'Vendredi': 'Fr',
'Samedi': 'Sa',
'Dimanche': 'Su'
};
let joursSelectionnes = [];
let horairesParJour = {};
// Parcourir les checkboxes des jours
Object.keys(jours).forEach(jour => {
const checkbox = document.getElementById(`jour-${jour.toLowerCase()}`);
if (checkbox && checkbox.checked) {
joursSelectionnes.push(jours[jour]);
// Vérifier si le jour est marqué comme fermé
const fermeCheckbox = document.getElementById(`${jour.toLowerCase()}-ferme`);
if (fermeCheckbox && fermeCheckbox.checked) {
horairesParJour[jours[jour]] = 'off';
} else {
// Récupérer les horaires pour ce jour
const prefix = jour.toLowerCase();
const plage1 = this.getTimeRange(`${prefix}-plage1`);
const plage2 = this.getTimeRange(`${prefix}-plage2`);
let horaires = [];
if (plage1) horaires.push(plage1);
if (plage2) horaires.push(plage2);
if (horaires.length > 0) {
horairesParJour[jours[jour]] = horaires.join(',');
}
}
}
});
// Optimiser les plages horaires identiques consécutives
let optimizedRules = [];
let currentRule = null;
let currentDays = [];
let currentHours = null;
joursSelectionnes.forEach((jour, index) => {
const hours = horairesParJour[jour];
if (currentHours === null) {
// Première règle
currentHours = hours;
currentDays = [jour];
} else if (hours === currentHours) {
// Mêmes horaires que la règle en cours
currentDays.push(jour);
} else {
// Horaires différents, on finalise la règle en cours
if (currentDays.length > 0) {
optimizedRules.push({
days: currentDays,
hours: currentHours
});
}
// On commence une nouvelle règle
currentDays = [jour];
currentHours = hours;
}
// Si c'est le dernier jour, on finalise la règle en cours
if (index === joursSelectionnes.length - 1 && currentDays.length > 0) {
optimizedRules.push({
days: currentDays,
hours: currentHours
});
}
});
// Construire la chaîne au format OSM
let osmFormat = optimizedRules.map(rule => {
const days = rule.days.length > 1 ? `${rule.days[0]}-${rule.days[rule.days.length - 1]}` : rule.days[0];
return rule.hours ? `${days} ${rule.hours}` : days;
}).join('; ');
// Mettre à jour l'input custom__opening_hours
const customOpeningHours = document.querySelector(inputSelector);
if (customOpeningHours) {
customOpeningHours.value = osmFormat;
}
// Mettre à jour le rendu visuel
this.renderOpeningHours(optimizedRules);
},
renderOpeningHours: function (rules) {
const container = document.getElementById('opening_hours_render');
console.log('renderOpeningHours', rules);
if (!container) return;
// Vider le conteneur
container.innerHTML = '';
// Créer le style pour les sections d'ouverture
const style = document.createElement('style');
style.textContent = `
.opening-hours-day {
background-color: #f8f9fa;
border-radius: 4px;
margin-bottom: 8px;
padding: 8px;
position: relative;
height: 40px;
}
.opening-hours-time {
background-color: #d4edda;
border-radius: 4px;
position: absolute;
height: 24px;
top: 8px;
}
.opening-hours-label {
position: absolute;
left: 8px;
top: 50%;
transform: translateY(-50%);
z-index: 1;
}
.opening-hours-closed {
background-color: #f8d7da;
}
`;
document.head.appendChild(style);
// Mapping des jours OSM vers français
const joursMap = {
'Mo': 'Lundi',
'Tu': 'Mardi',
'We': 'Mercredi',
'Th': 'Jeudi',
'Fr': 'Vendredi',
'Sa': 'Samedi',
'Su': 'Dimanche'
};
// Créer les lignes pour chaque jour
Object.entries(joursMap).forEach(([osmDay, frenchDay]) => {
const dayDiv = document.createElement('div');
dayDiv.classList.add('opening-hours-day');
// Ajouter le label du jour
const label = document.createElement('span');
label.classList.add('opening-hours-label');
label.textContent = frenchDay;
dayDiv.appendChild(label);
// Trouver les horaires pour ce jour
const rule = rules.find(r => r.days.includes(osmDay));
if (rule) {
if (rule.hours === 'off') {
// Jour fermé
dayDiv.classList.add('opening-hours-closed');
} else if (rule.hours) {
// Plages horaires spécifiques
const timeRanges = rule.hours.split(',');
timeRanges.forEach((timeRange, index) => {
const [start, end] = timeRange.split('-');
const [startHour, startMinute] = start.split(':');
const [endHour, endMinute] = end.split(':');
// Calculer la position et la largeur
const startMinutes = parseInt(startHour) * 60 + parseInt(startMinute);
const endMinutes = parseInt(endHour) * 60 + parseInt(endMinute);
const totalMinutes = 24 * 60;
const left = (startMinutes / totalMinutes) * 100;
const width = ((endMinutes - startMinutes) / totalMinutes) * 100;
const timeDiv = document.createElement('div');
timeDiv.classList.add('opening-hours-time');
timeDiv.style.left = `${left}%`;
timeDiv.style.width = `${width}%`;
timeDiv.title = `${start}-${end}`;
dayDiv.appendChild(timeDiv);
});
} else {
// Toute la journée
const timeDiv = document.createElement('div');
timeDiv.classList.add('opening-hours-time');
timeDiv.style.left = '0%';
timeDiv.style.width = '100%';
timeDiv.title = 'Toute la journée';
dayDiv.appendChild(timeDiv);
}
}
container.appendChild(dayDiv);
});
},
getTimeRange: function (prefix) {
const isActive = document.querySelector(`input[name="${prefix}-active"]`).checked;
if (!isActive) return null;
const startHour = document.querySelector(`input[name="${prefix}-start-hour"]`).value;
const startMinute = document.querySelector(`input[name="${prefix}-start-minute"]`).value;
const endHour = document.querySelector(`input[name="${prefix}-end-hour"]`).value;
const endMinute = document.querySelector(`input[name="${prefix}-end-minute"]`).value;
if (startHour && startMinute && endHour && endMinute) {
const start = `${startHour.padStart(2, '0')}:${startMinute.padStart(2, '0')}`;
const end = `${endHour.padStart(2, '0')}:${endMinute.padStart(2, '0')}`;
return `${start}-${end}`;
}
return null;
}
}
window.openingHoursFormManager = openingHoursFormManager;

44
assets/stats-charts.js Normal file
View file

@ -0,0 +1,44 @@
// Affichage du graphique de fréquence des mises à jour par trimestre sur la page stats d'une ville
document.addEventListener('DOMContentLoaded', function() {
function drawModificationsByQuarterChart() {
if (!window.Chart) {
setTimeout(drawModificationsByQuarterChart, 50);
return;
}
if (typeof window.modificationsByQuarter === 'undefined') return;
const modifData = window.modificationsByQuarter;
const modifLabels = Object.keys(modifData);
const modifCounts = Object.values(modifData);
const modifCanvas = document.getElementById('modificationsByQuarterChart');
if (modifCanvas && modifLabels.length > 0) {
new window.Chart(modifCanvas.getContext('2d'), {
type: 'bar',
data: {
labels: modifLabels,
datasets: [{
label: 'Nombre de lieux modifiés',
data: modifCounts,
backgroundColor: 'rgba(54, 162, 235, 0.7)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
title: { display: true, text: 'Fréquence des mises à jour par trimestre' }
},
scales: {
y: { beginAtZero: true, title: { display: true, text: 'Nombre de lieux' } },
x: { title: { display: true, text: 'Trimestre' } }
}
}
});
} else if (modifCanvas) {
modifCanvas.parentNode.innerHTML = '<div class="alert alert-info">Aucune donnée de modification disponible pour cette ville.</div>';
}
}
drawModificationsByQuarterChart();
});

192
assets/styles/app.css Normal file
View file

@ -0,0 +1,192 @@
body {
background-color: rgb(236, 236, 236);
transition: background-color ease 0.5s;
}
.body-landing .container {
background-color: white;
border-radius: 10px;
padding: 20px;
}
.main-footer {
padding-bottom: 20rem;
background-color: #dfe5eb;
}
#qrcode {
margin-bottom: 8rem;
}
input[data-important] {
border-color: #7a8fbb;
border-left-width: 5px;
}
input[data-important]:before {
content: ">" !important;
}
.filled, .good_filled {
border-color: rgba(0, 255, 0, 0.8) !important;
color: #082b0a !important;
}
.filled:hover, .good_filled:hover {
background-color: #d9ffd1 !important;
}
.no-name {
color: #df5a0d;
}
table {
max-height: 100vh;
max-width: 100vw;
}
table.js-sort-table th {
cursor: pointer;
}
table.js-sort-table th:hover {
background-color: #f0f0f0;
}
table.js-sort-table th:active {
background-color: #e0e0e0;
}
/*// formulaire de temps d'ouverture */
.form-check-input {
margin-right: 2rem;
}
.jour-container {
border: solid #dedede 1px;
}
#maploader {
position: relative;
top: 200px;
left: 50%;
z-index: 100;
}
.maplibregl-popup-content {
overflow: auto;
min-width: 300px;
max-height: 11rem !important;
}
.maplibregl-popup-content strong {
min-width: 10rem;
}
.maplibregl-popup-content table {
width: 100%;
max-height: 300px;
overflow: auto;
}
.maplibregl-popup-content h1,
.maplibregl-popup-content h2,
.maplibregl-popup-contenth3 {
font-size: 1rem;
}
.card {
overflow: auto;
}
#attribution {
font-size: 0.6rem;
}
#restaurant .form-check-label {
margin-top: 0;
display: block;
cursor: pointer;
}
#restaurant .form-check-label:hover {
background-color: #f0f0f0;
color: #1e40c6;
}
#advanced_tags {
border-left: solid 3px #ccc;
padding-left: 2rem;
}
.start-hour {
margin-left: -1rem;
}
.end-hour {
margin-left: -1rem;
}
.good_filled {
border-color: green;
}
.hidden {
display: none;
}
input[type="checkbox"] {
width: 20px;
height: 20px;
}
.is-invalid {
border: 1px solid red;
}
.is-invalid #validation_messages {
color: red;
}
img {
max-width: 100%;
max-height: 400px;
}
#completionHistoryChart {
min-height: 500px;
}
@media (max-width: 768px) {
.form-label {
margin-bottom: 0.5rem;
}
.mb-3 {
margin-bottom: 1rem !important;
}
}
table tbody {
max-height: 700px;
overflow: auto;
}
#table_container, .table-container, #table-container {
max-height: 700px;
overflow: auto;
display: block;
border: solid 3px rgb(255, 255, 255);
}
#citySuggestions {
z-index: 10;
}
body .card:hover {
transform: none !important;
box-shadow: none !important;
}

View file

@ -0,0 +1,19 @@
// Gestion du tri des tableaux
// import Tablesort from 'tablesort';
document.addEventListener('DOMContentLoaded', () => {
// Gestion du toggle gouttes/ronds sur la carte
const toggle = document.getElementById('toggleMarkers');
if (toggle && window.updateMarkers) {
toggle.addEventListener('change', (e) => {
window.updateMarkers(e.target.value);
});
}
});
// Exposer une fonction pour (ré)appliquer le tri si besoin
export function applyTableSort() {
document.querySelectorAll('.js-sort-table').forEach(table => {
new window.Tablesort(table);
});
}

384
assets/utils.js Normal file
View file

@ -0,0 +1,384 @@
function colorHeadingTable() {
const headers = document.querySelectorAll('th');
headers.forEach(header => {
const text = header.textContent;
const match = text.match(/\((\d+)\s*\/\s*(\d+)\)/);
if (match) {
const [_, completed, total] = match;
const ratio = completed / total;
const alpha = ratio.toFixed(2);
header.style.backgroundColor = `rgba(154, 205, 50, ${alpha})`;
}
});
}
function check_validity(e) {
list_inputs_good_to_fill = [
'input[name="commerce_tag_value__contact:email"]',
'input[name="commerce_tag_value__contact:phone"]',
'input[name="commerce_tag_value__contact:website"]',
'input[name="commerce_tag_value__contact:mastodon"]',
'input[name="commerce_tag_value__address"]',
'input[name="custom_opening_hours"]',
'input[name="commerce_tag_value__contact:street"]',
'input[name="commerce_tag_value__contact:housenumber"]',
'input[name="custom__cuisine"]',
]
list_inputs_good_to_fill.forEach(selector => {
const input = document.querySelector(selector);
if (input) {
if (input.value.trim() !== '') {
input.classList.add('good_filled');
} else {
input.classList.remove('good_filled');
}
}
});
let errors = [];
document.querySelectorAll('.is-invalid').forEach(input => {
input.classList.remove('is-invalid');
});
const nameInput = document.querySelector('input[name="commerce_tag_value__name"]');
if (!nameInput.value.trim()) {
errors.push("Le nom de l'établissement est obligatoire");
nameInput.classList.add('is-invalid');
}
const emailInput = document.querySelector('input[name="commerce_tag_value__contact:email"]');
if (emailInput && emailInput.value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(emailInput.value)) {
errors.push("L'adresse email n'est pas valide");
emailInput.classList.add('is-invalid');
}
}
const phoneInput = document.querySelector('input[name="commerce_tag_value__contact:phone"]');
if (phoneInput && phoneInput.value) {
const phoneRegex = /^(\+33|0)[1-9](\d{2}){4}$/;
if (!phoneRegex.test(phoneInput.value.replace(/\s/g, ''))) {
errors.push("Le numéro de téléphone n'est pas valide");
phoneInput.classList.add('is-invalid');
}
}
if (errors.length > 0) {
e.preventDefault();
document.querySelector('#validation_messages').innerHTML = errors.join('<br>');
document.querySelector('#validation_messages').classList.add('is-invalid');
}
}
export const genererCouleurPastel = () => {
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 {
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`);
const data = await response.json();
document.querySelector('#loading_search_insee').classList.add('d-none');
return data.map(commune => ({
label: `${commune.nom} (${commune.codesPostaux.join(', ')}, code insee ${commune.code})`,
insee: commune.code,
postcodes: commune.codesPostaux
}));
} catch (error) {
console.error('Erreur lors de la recherche du code INSEE:', error);
return [];
}
}
export function updateMapHeightForLargeScreens() {
const mapFound = document.querySelector('#map');
const canvasFound = document.querySelector('#map canvas');
const newHeight = window.innerHeight * 0.5 + 'px'
if (mapFound && window.innerHeight > 800 && window.innerWidth > 800) {
mapFound.style.height = newHeight;
} else {
console.log('window.innerHeight', window.innerHeight);
}
}
async function listChangesets() {
const options = {
headers: {
'Accept': 'application/json'
}
};
const changesets = await fetch('https://api.openstreetmap.org/api/0.6/changesets?display_name=osm-commerce-fr', options);
const data = await changesets.json();
console.log(data.changesets.length);
const now = new Date();
const last24h = new Date(now - 24 * 60 * 60 * 1000);
const last7days = new Date(now - 7 * 24 * 60 * 60 * 1000);
const last30days = new Date(now - 30 * 24 * 60 * 60 * 1000);
const stats = {
last24h: 0,
last7days: 0,
last30days: 0
};
data.changesets.forEach(changeset => {
const changesetDate = new Date(changeset.closed_at);
if (changesetDate >= last24h) {
stats.last24h++;
}
if (changesetDate >= last7days) {
stats.last7days++;
}
if (changesetDate >= last30days) {
stats.last30days++;
}
});
const historyDiv = document.getElementById('userChangesHistory');
if (historyDiv) {
historyDiv.innerHTML = `
<div id="changesets_history">
<p>Changesets créés :</p>
<div class="row">
<div class="col-6">Dernières 24h :</div> <div class="col-6 text-right">${stats.last24h}</div>
<div class="col-6">7 derniers jours :</div> <div class="col-6 text-right">${stats.last7days}</div>
<div class="col-6">30 derniers jours :</div> <div class="col-6 text-right">${stats.last30days}</div>
</div>
</div>
`;
}
}
function openInPanoramax() {
const center = map.getCenter();
const zoom = map.getZoom();
const panoramaxUrl = `https://api.panoramax.xyz/?focus=map&map=${zoom}/${center.lat}/${center.lng}`;
window.open(panoramaxUrl);
}
export function enableLabourageForm() {
const citySearchInput = document.getElementById('citySearch');
const citySuggestionsList = document.getElementById('citySuggestions');
if (citySearchInput && citySuggestionsList) {
const form = citySearchInput.closest('form');
setupCitySearch('citySearch', 'citySuggestions', function (result_search) {
if (form) {
const labourageBtn = form.querySelector('button[type="submit"]');
if (labourageBtn) {
// Remplir le champ caché avec le code INSEE
const inseeInput = form.querySelector('#selectedZipCode');
if (inseeInput) {
inseeInput.value = result_search.insee;
}
// Changer l'action du formulaire pour pointer vers la bonne URL
form.action = getLabourerUrl(result_search);
// Changer le texte du bouton et le désactiver
labourageBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Ajout de ' + result_search.name + '...';
labourageBtn.disabled = true;
// Soumettre le formulaire
form.submit();
}
}else{
console.warn('pas de form pour labourage trouvé')
}
});
}else{
console.warn('pas de labourage citySearchInput citySuggestionsList trouvé', citySearchInput,citySuggestionsList )
}
}
export function setupCitySearch(inputId, suggestionListId, onSelect) {
const searchInput = document.getElementById(inputId);
const suggestionList = document.getElementById(suggestionListId);
if (!searchInput || !suggestionList) return;
let timeoutId = null;
let searchOngoing = false;
searchInput.addEventListener('keyup', function () {
clearTimeout(timeoutId);
const query = this.value.trim();
if (query.length < 3) {
clearSuggestions();
return;
}
timeoutId = setTimeout(() => {
if (!searchOngoing) {
searchOngoing = true;
performSearch(query).then(() => {
searchOngoing = false;
});
}
}, 300);
});
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,
insee: city.code,
postcodes: city.codesPostaux,
postcode: city.codesPostaux && city.codesPostaux.length > 0 ? city.codesPostaux[0] : '',
display_name: `${city.nom} (${city.codesPostaux && city.codesPostaux.length > 0 ? city.codesPostaux[0] : ''})`
}));
displaySuggestions(citySuggestions);
} catch (error) {
console.error("Erreur de recherche:", error);
}
}
function displaySuggestions(suggestions) {
clearSuggestions();
suggestions.forEach(suggestion => {
const item = document.createElement('div');
item.classList.add('suggestion-item');
// Nouveau rendu : nom en gras, INSEE et CP en petit/gris
item.innerHTML = `
<span class="suggestion-name" style="font-weight:bold;">${suggestion.name}</span><br>
<span class="suggestion-details" style="font-size:0.95em;color:#666;">
<span>INSEE&nbsp;: <b>${suggestion.insee}</b></span>
<span style="margin-left:12px;">CP&nbsp;: <b>${Array.isArray(suggestion.postcodes) ? suggestion.postcodes.join(', ') : suggestion.postcode}</b></span>
</span>
`;
item.addEventListener('click', () => {
searchInput.value = suggestion.name;
clearSuggestions();
if (onSelect) {
onSelect(suggestion);
}
});
suggestionList.appendChild(item);
});
suggestionList.style.display = 'block';
}
function clearSuggestions() {
suggestionList.innerHTML = '';
suggestionList.style.display = 'none';
}
document.addEventListener('click', (e) => {
if (!searchInput.contains(e.target) && !suggestionList.contains(e.target)) {
clearSuggestions();
}
});
}
export function getLabourerUrl(obj) {
if (obj && obj.insee) {
return `/add-city-without-labourage/${obj.insee}`;
}
return '#';
}
export function handleAddCityFormSubmit(event) {
event.preventDefault();
const zipCode = document.getElementById('selectedZipCode').value;
if (zipCode && zipCode.match(/^\d{5}$/)) {
window.location.href = `/add-city-without-labourage/${zipCode}`;
} else {
alert('Veuillez sélectionner une ville valide avec un code postal.');
}
}
export function colorizePercentageCells(selector, color = '154, 205, 50') {
document.querySelectorAll(selector).forEach(cell => {
const percentage = parseInt(cell.textContent.replace('%', ''), 10);
if (!isNaN(percentage)) {
const alpha = percentage / 100;
cell.style.backgroundColor = `rgba(${color}, ${alpha})`;
}
});
}
export function colorizePercentageCellsRelative(selector, color = '154, 205, 50') {
let min = Infinity;
let max = -Infinity;
const cells = document.querySelectorAll(selector);
cells.forEach(cell => {
const value = parseInt(cell.textContent.replace('%', ''), 10);
if (!isNaN(value)) {
min = Math.min(min, value);
max = Math.max(max, value);
}
});
if (max > min) {
cells.forEach(cell => {
const value = parseInt(cell.textContent.replace('%', ''), 10);
if (!isNaN(value)) {
const ratio = (value - min) / (max - min);
cell.style.backgroundColor = `rgba(${color}, ${ratio.toFixed(2)})`;
}
});
}
}
export function adjustListGroupFontSize(selector, minFont = 0.8, maxFont = 1.2) {
const listItems = document.querySelectorAll(selector);
if (listItems.length === 0) return;
let fontSize = maxFont;
const count = listItems.length;
if (count > 0) {
fontSize = Math.max(minFont, maxFont - (count - 5) * 0.05);
}
listItems.forEach(item => {
item.style.fontSize = fontSize + 'rem';
});
}
export function calculateCompletion(properties) {
let completed = 0;
const total = 7; // Nombre de critères
if (properties.name) completed++;
if (properties['addr:housenumber'] && properties['addr:street']) completed++;
if (properties.opening_hours) completed++;
if (properties.website || properties['contact:website']) completed++;
if (properties.phone || properties['contact:phone']) completed++;
if (properties.wheelchair) completed++;
return {
percentage: total > 0 ? (completed / total) * 100 : 0,
completed: completed,
total: total
};
}
export function toggleCompletionInfo() {
const content = document.getElementById('completionInfoContent');
const icon = document.getElementById('completionInfoIcon');
if (content) {
const isVisible = content.style.display === 'block';
content.style.display = isVisible ? 'none' : 'block';
if (icon) {
icon.classList.toggle('bi-chevron-down', isVisible);
icon.classList.toggle('bi-chevron-up', !isVisible);
}
}
}
window.check_validity = check_validity;
window.colorHeadingTable = colorHeadingTable;
window.openInPanoramax = openInPanoramax;
window.listChangesets = listChangesets;
window.adjustListGroupFontSize = adjustListGroupFontSize;
window.calculateCompletion = calculateCompletion;
window.toggleCompletionInfo = toggleCompletionInfo;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

22
assets/vendor/installed.php vendored Normal file
View file

@ -0,0 +1,22 @@
<?php return array (
'@hotwired/stimulus' =>
array (
'version' => '3.2.2',
'dependencies' =>
array (
),
'extraFiles' =>
array (
),
),
'@hotwired/turbo' =>
array (
'version' => '7.3.0',
'dependencies' =>
array (
),
'extraFiles' =>
array (
),
),
);