up historique
This commit is contained in:
parent
ad4170db14
commit
c274fd6a63
12 changed files with 448 additions and 616 deletions
118
assets/app.js
118
assets/app.js
|
@ -17,7 +17,107 @@ import './josm.js';
|
|||
import './edit.js';
|
||||
|
||||
import Chart from 'chart.js/auto';
|
||||
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||||
import { genererCouleurPastel, setupCitySearch, handleAddCityFormSubmit, enableLabourageForm, getLabourerUrl, adjustListGroupFontSize } from './utils.js';
|
||||
import Tablesort from 'tablesort';
|
||||
|
||||
window.Chart = Chart;
|
||||
window.genererCouleurPastel = genererCouleurPastel;
|
||||
window.setupCitySearch = setupCitySearch;
|
||||
window.handleAddCityFormSubmit = handleAddCityFormSubmit;
|
||||
window.getLabourerUrl = getLabourerUrl;
|
||||
|
||||
Chart.register(ChartDataLabels);
|
||||
|
||||
function initDashboardChart() {
|
||||
const chartCanvas = document.getElementById('statsBubbleChart');
|
||||
if (!chartCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!chartCanvas.dataset.stats || chartCanvas.dataset.stats.length <= 2) { // <= 2 pour ignorer '[]'
|
||||
console.log("Les données du graphique sont vides ou absentes, le graphique ne sera pas affiché.");
|
||||
return;
|
||||
}
|
||||
|
||||
const statsData = JSON.parse(chartCanvas.dataset.stats);
|
||||
|
||||
if (statsData && statsData.length > 0) {
|
||||
const bubbleChartData = statsData.map(stat => {
|
||||
const population = parseInt(stat.population, 10);
|
||||
const placesCount = parseInt(stat.placesCount, 10);
|
||||
const ratio = population > 0 ? (placesCount / population) * 1000 : 0;
|
||||
|
||||
return {
|
||||
x: population,
|
||||
y: ratio,
|
||||
r: Math.sqrt(placesCount) * 2,
|
||||
label: stat.name,
|
||||
completion: stat.completionPercent || 0
|
||||
};
|
||||
});
|
||||
|
||||
new Chart(chartCanvas.getContext('2d'), {
|
||||
type: 'bubble',
|
||||
data: {
|
||||
datasets: [{
|
||||
label: 'Villes',
|
||||
data: bubbleChartData,
|
||||
backgroundColor: bubbleChartData.map(d => `rgba(255, 99, 132, ${d.completion / 100})`),
|
||||
borderColor: 'rgba(255, 99, 132, 1)',
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
datalabels: {
|
||||
anchor: 'center',
|
||||
align: 'center',
|
||||
color: '#000',
|
||||
font: {
|
||||
weight: 'bold'
|
||||
},
|
||||
formatter: (value, context) => {
|
||||
return context.dataset.data[context.dataIndex].label;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
const d = context.raw;
|
||||
return [
|
||||
`${d.label}`,
|
||||
`Population: ${d.x.toLocaleString()}`,
|
||||
`Lieux / 1000 hab: ${d.y.toFixed(2)}`,
|
||||
`Total lieux: ${Math.round(Math.pow(d.r / 2, 2))}`,
|
||||
`Complétion: ${d.completion}%`
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'logarithmic',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Population (échelle log)'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Commerces pour 1000 habitants'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Attendre le chargement du DOM
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
@ -79,6 +179,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
updateCompletionProgress();
|
||||
|
||||
// Activer le tri sur tous les tableaux désignés
|
||||
document.querySelectorAll('.js-sort-table').forEach(table => {
|
||||
new Tablesort(table);
|
||||
});
|
||||
|
||||
// Focus sur le premier champ texte au chargement
|
||||
const firstTextInput = document.querySelector('input.form-control');
|
||||
if (firstTextInput) {
|
||||
|
@ -94,10 +199,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
parseCuisine();
|
||||
|
||||
// Tri automatique des tableaux
|
||||
const tables = document.querySelectorAll('table');
|
||||
tables.forEach(table => {
|
||||
table.classList.add('js-sort-table');
|
||||
});
|
||||
// const tables = document.querySelectorAll('table');
|
||||
// tables.forEach(table => {
|
||||
// table.classList.add('js-sort-table');
|
||||
// });
|
||||
|
||||
|
||||
// Modifier la fonction de recherche existante
|
||||
|
@ -160,4 +265,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
enableLabourageForm();
|
||||
initDashboardChart();
|
||||
|
||||
adjustListGroupFontSize('.list-group-item');
|
||||
});
|
||||
|
|
|
@ -71,7 +71,7 @@ function check_validity(e) {
|
|||
}
|
||||
}
|
||||
|
||||
const genererCouleurPastel = () => {
|
||||
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);
|
||||
|
@ -163,23 +163,26 @@ function openInPanoramax() {
|
|||
window.open(panoramaxUrl);
|
||||
}
|
||||
|
||||
function enableLabourageForm() {
|
||||
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) {
|
||||
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;
|
||||
if (form) {
|
||||
const labourageBtn = form.querySelector('.btn-labourer');
|
||||
if (labourageBtn) {
|
||||
labourageBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Chargement...';
|
||||
labourageBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
window.location.href = getLabourerUrl(result_search);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setupCitySearch(inputId, suggestionListId, onSelect) {
|
||||
export function setupCitySearch(inputId, suggestionListId, onSelect) {
|
||||
const searchInput = document.getElementById(inputId);
|
||||
const suggestionList = document.getElementById(suggestionListId);
|
||||
|
||||
|
@ -251,14 +254,14 @@ function setupCitySearch(inputId, suggestionListId, onSelect) {
|
|||
});
|
||||
}
|
||||
|
||||
function getLabourerUrl(obj) {
|
||||
export function getLabourerUrl(obj) {
|
||||
if (obj && obj.insee) {
|
||||
return `/admin/labourer_insee/${obj.insee}`;
|
||||
return `/admin/labourer/${obj.insee}`;
|
||||
}
|
||||
return '#';
|
||||
}
|
||||
|
||||
function handleAddCityFormSubmit(event) {
|
||||
export function handleAddCityFormSubmit(event) {
|
||||
event.preventDefault();
|
||||
const zipCode = document.getElementById('selectedZipCode').value;
|
||||
if (zipCode && zipCode.match(/^\d{5}$/)) {
|
||||
|
@ -268,9 +271,9 @@ function handleAddCityFormSubmit(event) {
|
|||
}
|
||||
}
|
||||
|
||||
function colorizePercentageCells(selector, color = '154, 205, 50') {
|
||||
export function colorizePercentageCells(selector, color = '154, 205, 50') {
|
||||
document.querySelectorAll(selector).forEach(cell => {
|
||||
const percentage = parseInt(cell.textContent);
|
||||
const percentage = parseInt(cell.textContent.replace('%', ''), 10);
|
||||
if (!isNaN(percentage)) {
|
||||
const alpha = percentage / 100;
|
||||
cell.style.backgroundColor = `rgba(${color}, ${alpha})`;
|
||||
|
@ -278,45 +281,66 @@ function colorizePercentageCells(selector, color = '154, 205, 50') {
|
|||
});
|
||||
}
|
||||
|
||||
function colorizePercentageCellsRelative(selector, color = '154, 205, 50') {
|
||||
export function colorizePercentageCellsRelative(selector, color = '154, 205, 50') {
|
||||
let min = Infinity;
|
||||
let max = -Infinity;
|
||||
const cells = document.querySelectorAll(selector);
|
||||
|
||||
let maxValue = 0;
|
||||
cells.forEach(cell => {
|
||||
const value = parseInt(cell.textContent);
|
||||
if (!isNaN(value) && value > maxValue) {
|
||||
maxValue = value;
|
||||
const value = parseInt(cell.textContent.replace('%', ''), 10);
|
||||
if (!isNaN(value)) {
|
||||
min = Math.min(min, value);
|
||||
max = Math.max(max, value);
|
||||
}
|
||||
});
|
||||
|
||||
cells.forEach(cell => {
|
||||
const value = parseInt(cell.textContent);
|
||||
if (!isNaN(value)) {
|
||||
const alpha = value / maxValue;
|
||||
cell.style.backgroundColor = `rgba(${color}, ${alpha})`;
|
||||
}
|
||||
});
|
||||
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)})`;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function adjustListGroupFontSize(selector, minFont = 0.8, maxFont = 1.2) {
|
||||
const items = document.querySelectorAll(selector);
|
||||
const count = items.length;
|
||||
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);
|
||||
}
|
||||
items.forEach(item => {
|
||||
listItems.forEach(item => {
|
||||
item.style.fontSize = fontSize + 'rem';
|
||||
});
|
||||
}
|
||||
|
||||
window.setupCitySearch = setupCitySearch;
|
||||
window.handleAddCityFormSubmit = handleAddCityFormSubmit;
|
||||
window.colorizePercentageCells = colorizePercentageCells;
|
||||
window.colorizePercentageCellsRelative = colorizePercentageCellsRelative;
|
||||
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++;
|
||||
if (properties.note) completed++;
|
||||
|
||||
return {
|
||||
percentage: total > 0 ? (completed / total) * 100 : 0,
|
||||
completed: completed,
|
||||
total: total
|
||||
};
|
||||
}
|
||||
|
||||
window.check_validity = check_validity;
|
||||
window.colorHeadingTable = colorHeadingTable;
|
||||
window.openInPanoramax = openInPanoramax;
|
||||
window.listChangesets = listChangesets;
|
||||
window.colorHeadingTable = colorHeadingTable;
|
||||
window.adjustListGroupFontSize = adjustListGroupFontSize;
|
||||
window.genererCouleurPastel = genererCouleurPastel;
|
||||
window.adjustListGroupFontSize = adjustListGroupFontSize;
|
||||
window.calculateCompletion = calculateCompletion;
|
23
package-lock.json
generated
23
package-lock.json
generated
|
@ -10,8 +10,10 @@
|
|||
"@babel/preset-env": "^7.16.0",
|
||||
"@symfony/webpack-encore": "^5.0.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"chartjs-plugin-datalabels": "^2.2.0",
|
||||
"core-js": "^3.38.0",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"tablesort": "^5.6.0",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-cli": "^5.1.0"
|
||||
}
|
||||
|
@ -2490,6 +2492,16 @@
|
|||
"pnpm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chartjs-plugin-datalabels": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz",
|
||||
"integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"chart.js": ">=3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chrome-trace-event": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
|
||||
|
@ -5099,6 +5111,17 @@
|
|||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tablesort": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tablesort/-/tablesort-5.6.0.tgz",
|
||||
"integrity": "sha512-cZZXK3G089PbpxH8N7vN7Z21SEKqXAaCiSVOmZdR/v7z8TFCsF/OFr0rzjhQuFlQQHy9uQtW9P2oQFJzJFGVrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16",
|
||||
"npm": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
"@babel/preset-env": "^7.16.0",
|
||||
"@symfony/webpack-encore": "^5.0.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"chartjs-plugin-datalabels": "^2.2.0",
|
||||
"core-js": "^3.38.0",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"tablesort": "^5.6.0",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-cli": "^5.1.0"
|
||||
},
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
/* These style add the up/down arrows for the current sorted column
|
||||
* To support more columns, just augment these styles in like form.
|
||||
*/
|
||||
table.js-sort-asc.js-sort-0 thead tr:last-child > :nth-child(1):after,
|
||||
table.js-sort-asc.js-sort-1 thead tr:last-child > :nth-child(2):after,
|
||||
table.js-sort-asc.js-sort-2 thead tr:last-child > :nth-child(3):after,
|
||||
table.js-sort-asc.js-sort-3 thead tr:last-child > :nth-child(4):after,
|
||||
table.js-sort-asc.js-sort-4 thead tr:last-child > :nth-child(5):after,
|
||||
table.js-sort-asc.js-sort-5 thead tr:last-child > :nth-child(6):after,
|
||||
table.js-sort-asc.js-sort-6 thead tr:last-child > :nth-child(7):after,
|
||||
table.js-sort-asc.js-sort-7 thead tr:last-child > :nth-child(8):after,
|
||||
table.js-sort-asc.js-sort-8 thead tr:last-child > :nth-child(9):after,
|
||||
table.js-sort-asc.js-sort-9 thead tr:last-child > :nth-child(10):after
|
||||
{
|
||||
content: "\25b2";
|
||||
font-size: 0.7em;
|
||||
padding-left: 3px;
|
||||
line-height: 0.7em;
|
||||
}
|
||||
table.js-sort-desc.js-sort-0 thead tr:last-child > :nth-child(1):after,
|
||||
table.js-sort-desc.js-sort-1 thead tr:last-child > :nth-child(2):after,
|
||||
table.js-sort-desc.js-sort-2 thead tr:last-child > :nth-child(3):after,
|
||||
table.js-sort-desc.js-sort-3 thead tr:last-child > :nth-child(4):after,
|
||||
table.js-sort-desc.js-sort-4 thead tr:last-child > :nth-child(5):after,
|
||||
table.js-sort-desc.js-sort-5 thead tr:last-child > :nth-child(6):after,
|
||||
table.js-sort-desc.js-sort-6 thead tr:last-child > :nth-child(7):after,
|
||||
table.js-sort-desc.js-sort-7 thead tr:last-child > :nth-child(8):after,
|
||||
table.js-sort-desc.js-sort-8 thead tr:last-child > :nth-child(9):after,
|
||||
table.js-sort-desc.js-sort-9 thead tr:last-child > :nth-child(10):after
|
||||
{
|
||||
content: "\25bc";
|
||||
font-size: 0.7em;
|
||||
padding-left: 3px;
|
||||
line-height: 0.7em;
|
||||
}
|
|
@ -1,251 +0,0 @@
|
|||
/**
|
||||
* sort-table.js
|
||||
* A pure JavaScript (no dependencies) solution to make HTML
|
||||
* Tables sortable
|
||||
*
|
||||
* Copyright (c) 2013 Tyler Uebele
|
||||
* Released under the MIT license. See included LICENSE.txt
|
||||
* or http://opensource.org/licenses/MIT
|
||||
*
|
||||
* latest version available at https://github.com/tyleruebele/sort-table
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sort the rows in a HTML Table
|
||||
*
|
||||
* @param Table The Table DOM object
|
||||
* @param col The zero-based column number by which to sort
|
||||
* @param dir Optional. The sort direction; pass 1 for asc; -1 for desc
|
||||
* @returns void
|
||||
*/
|
||||
function sortTable(Table, col, dir) {
|
||||
var sortClass, i;
|
||||
|
||||
if (!Table) {
|
||||
return;
|
||||
}
|
||||
// get previous sort column
|
||||
sortTable.sortCol = -1;
|
||||
sortClass = Table.className.match(/js-sort-\d+/);
|
||||
if (null != sortClass) {
|
||||
sortTable.sortCol = sortClass[0].replace(/js-sort-/, '');
|
||||
Table.className = Table.className.replace(new RegExp(' ?' + sortClass[0] + '\\b'), '');
|
||||
}
|
||||
// If sort column was not passed, use previous
|
||||
if ('undefined' === typeof col) {
|
||||
col = sortTable.sortCol;
|
||||
}
|
||||
|
||||
if ('undefined' !== typeof dir) {
|
||||
// Accept -1 or 'desc' for descending. All else is ascending
|
||||
sortTable.sortDir = dir == -1 || dir == 'desc' ? -1 : 1;
|
||||
} else {
|
||||
// sort direction was not passed, use opposite of previous
|
||||
sortClass = Table.className.match(/js-sort-(a|de)sc/);
|
||||
if (null != sortClass && sortTable.sortCol == col) {
|
||||
sortTable.sortDir = 'js-sort-asc' == sortClass[0] ? -1 : 1;
|
||||
} else {
|
||||
sortTable.sortDir = 1;
|
||||
}
|
||||
}
|
||||
Table.className = Table.className.replace(/ ?js-sort-(a|de)sc/g, '');
|
||||
|
||||
// update sort column
|
||||
Table.className += ' js-sort-' + col;
|
||||
sortTable.sortCol = col;
|
||||
|
||||
// update sort direction
|
||||
Table.className += ' js-sort-' + (sortTable.sortDir == -1 ? 'desc' : 'asc');
|
||||
|
||||
// get sort type
|
||||
if (col < Table.tHead.rows[Table.tHead.rows.length - 1].cells.length) {
|
||||
sortClass = Table.tHead.rows[Table.tHead.rows.length - 1].cells[col].className.match(/js-sort-[-\w]+/);
|
||||
}
|
||||
// Improved support for colspan'd headers
|
||||
for (i = 0; i < Table.tHead.rows[Table.tHead.rows.length - 1].cells.length; i++) {
|
||||
if (col == Table.tHead.rows[Table.tHead.rows.length - 1].cells[i].getAttribute('data-js-sort-colNum')) {
|
||||
sortClass = Table.tHead.rows[Table.tHead.rows.length - 1].cells[i].className.match(/js-sort-[-\w]+/);
|
||||
}
|
||||
}
|
||||
if (null != sortClass) {
|
||||
sortTable.sortFunc = sortClass[0].replace(/js-sort-/, '');
|
||||
} else {
|
||||
sortTable.sortFunc = 'string';
|
||||
}
|
||||
|
||||
// sort!
|
||||
var rows = [],
|
||||
TBody = Table.tBodies[0];
|
||||
|
||||
for (i = 0; i < TBody.rows.length; i++) {
|
||||
rows[i] = TBody.rows[i];
|
||||
}
|
||||
rows.sort(sortTable.compareRow);
|
||||
|
||||
while (TBody.firstChild) {
|
||||
TBody.removeChild(TBody.firstChild);
|
||||
}
|
||||
for (i = 0; i < rows.length; i++) {
|
||||
TBody.appendChild(rows[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two table rows based on current settings
|
||||
*
|
||||
* @param RowA A TR DOM object
|
||||
* @param RowB A TR DOM object
|
||||
* @returns {number} 1 if RowA is greater, -1 if RowB, 0 if equal
|
||||
*/
|
||||
sortTable.compareRow = function (RowA, RowB) {
|
||||
var valA, valB;
|
||||
if ('function' != typeof sortTable[sortTable.sortFunc]) {
|
||||
sortTable.sortFunc = 'string';
|
||||
}
|
||||
valA = sortTable[sortTable.sortFunc](RowA.cells[sortTable.sortCol]);
|
||||
valB = sortTable[sortTable.sortFunc](RowB.cells[sortTable.sortCol]);
|
||||
|
||||
return valA == valB ? 0 : sortTable.sortDir * (valA > valB ? 1 : -1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Strip all HTML, no exceptions
|
||||
* @param html
|
||||
* @returns {string}
|
||||
*/
|
||||
sortTable.stripTags = function (html) {
|
||||
return html.replace(/<\/?[a-z][a-z0-9]*\b[^>]*>/gi, '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function that converts a table cell (TD) to a comparable value
|
||||
* Converts innerHTML to a JS Date object
|
||||
*
|
||||
* @param Cell A TD DOM object
|
||||
* @returns {Date}
|
||||
*/
|
||||
sortTable.date = function (Cell) {
|
||||
return new Date(sortTable.stripTags(Cell.innerHTML));
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function that converts a table cell (TD) to a comparable value
|
||||
* Converts innerHTML to a JS Number object
|
||||
*
|
||||
* @param Cell A TD DOM object
|
||||
* @returns {Number}
|
||||
*/
|
||||
sortTable.number = function (Cell) {
|
||||
return Number(sortTable.stripTags(Cell.innerHTML).replace(/[^-\d.]/g, ''));
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function that converts a table cell (TD) to a comparable value
|
||||
* Converts innerHTML to a lower case string for insensitive compare
|
||||
*
|
||||
* @param Cell A TD DOM object
|
||||
* @returns {String}
|
||||
*/
|
||||
sortTable.string = function (Cell) {
|
||||
return sortTable.stripTags(Cell.innerHTML).toLowerCase();
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function that converts a table cell (TD) to a comparable value
|
||||
* Captures the last space-delimited token from innerHTML
|
||||
*
|
||||
* @param Cell A TD DOM object
|
||||
* @returns {String}
|
||||
*/
|
||||
sortTable.last = function (Cell) {
|
||||
return sortTable.stripTags(Cell.innerHTML).split(' ').pop().toLowerCase();
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function that converts a table cell (TD) to a comparable value
|
||||
* Captures the value of the first childNode
|
||||
*
|
||||
* @param Cell A TD DOM object
|
||||
* @returns {String}
|
||||
*/
|
||||
sortTable.input = function (Cell) {
|
||||
for (var i = 0; i < Cell.children.length; i++) {
|
||||
if ('object' == typeof Cell.children[i]
|
||||
&& 'undefined' != typeof Cell.children[i].value
|
||||
) {
|
||||
return Cell.children[i].value.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
return sortTable.string(Cell);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the click handler appropriate to the specified Table and column
|
||||
*
|
||||
* @param Table Table to sort
|
||||
* @param col Column to sort by
|
||||
* @returns {Function} Click Handler
|
||||
*/
|
||||
sortTable.getClickHandler = function (Table, col) {
|
||||
return function () {
|
||||
sortTable(Table, col);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Attach sortTable() calls to table header cells' onclick events
|
||||
* If the table(s) do not have a THead node, one will be created around the
|
||||
* first row
|
||||
*/
|
||||
sortTable.init = function () {
|
||||
var THead, Tables, Handler;
|
||||
if (document.querySelectorAll) {
|
||||
Tables = document.querySelectorAll('table.js-sort-table');
|
||||
} else {
|
||||
Tables = document.getElementsByTagName('table');
|
||||
}
|
||||
|
||||
for (var i = 0; i < Tables.length; i++) {
|
||||
// Because IE<8 doesn't support querySelectorAll, skip unclassed tables
|
||||
if (!document.querySelectorAll && null === Tables[i].className.match(/\bjs-sort-table\b/)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prevent repeat processing
|
||||
if (Tables[i].attributes['data-js-sort-table']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure table has a tHead element
|
||||
if (!Tables[i].tHead) {
|
||||
THead = document.createElement('thead');
|
||||
THead.appendChild(Tables[i].rows[0]);
|
||||
Tables[i].insertBefore(THead, Tables[i].children[0]);
|
||||
} else {
|
||||
THead = Tables[i].tHead;
|
||||
}
|
||||
|
||||
// Attach click events to table header
|
||||
for (var rowNum = 0; rowNum < THead.rows.length; rowNum++) {
|
||||
for (var cellNum = 0, colNum = 0; cellNum < THead.rows[rowNum].cells.length; cellNum++) {
|
||||
// Define which column the header should invoke sorting for
|
||||
THead.rows[rowNum].cells[cellNum].setAttribute('data-js-sort-colNum', colNum);
|
||||
Handler = sortTable.getClickHandler(Tables[i], colNum);
|
||||
window.addEventListener
|
||||
? THead.rows[rowNum].cells[cellNum].addEventListener('click', Handler)
|
||||
: window.attachEvent && THead.rows[rowNum].cells[cellNum].attachEvent('onclick', Handler);
|
||||
colNum += THead.rows[rowNum].cells[cellNum].colSpan;
|
||||
}
|
||||
}
|
||||
|
||||
// Mark table as processed
|
||||
Tables[i].setAttribute('data-js-sort-table', 'true')
|
||||
}
|
||||
};
|
||||
|
||||
// Run sortTable.init() when the page loads
|
||||
window.addEventListener
|
||||
? window.addEventListener('load', sortTable.init, false)
|
||||
: window.attachEvent && window.attachEvent('onload', sortTable.init)
|
||||
;
|
|
@ -11,6 +11,7 @@ use App\Entity\Stats;
|
|||
use App\Entity\StatsHistory;
|
||||
use App\Service\Motocultrice;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use function uuid_create;
|
||||
|
||||
final class AdminController extends AbstractController
|
||||
|
@ -234,102 +235,18 @@ final class AdminController extends AbstractController
|
|||
public function calculer_stats(string $insee_code): Response
|
||||
{
|
||||
// Récupérer tous les commerces de la zone
|
||||
$commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]);
|
||||
$commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code, 'dead' => false]);
|
||||
|
||||
// Récupérer les stats existantes pour la zone
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
||||
if(!$stats) {
|
||||
// Si aucune stat n'existe, on en crée une vide pour éviter les erreurs, mais sans la sauvegarder
|
||||
$stats = new Stats();
|
||||
$stats->setZone($insee_code);
|
||||
$stats->setName('Nouvelle zone non labourée');
|
||||
}
|
||||
|
||||
$urls = $stats->getAllCTCUrlsMap();
|
||||
|
||||
// Calculer les statistiques
|
||||
$calculatedStats = $this->motocultrice->calculateStats($commerces);
|
||||
|
||||
// Mettre à jour les stats pour la zone donnée
|
||||
$stats->setPlacesCount($calculatedStats['places_count']);
|
||||
$stats->setAvecHoraires($calculatedStats['counters']['avec_horaires']);
|
||||
$stats->setAvecAdresse($calculatedStats['counters']['avec_adresse']);
|
||||
$stats->setAvecSite($calculatedStats['counters']['avec_site']);
|
||||
$stats->setAvecAccessibilite($calculatedStats['counters']['avec_accessibilite']);
|
||||
$stats->setAvecNote($calculatedStats['counters']['avec_note']);
|
||||
$stats->setCompletionPercent($calculatedStats['completion_percent']);
|
||||
|
||||
// Associer les stats à chaque commerce
|
||||
foreach ($commerces as $commerce) {
|
||||
$commerce->setStats($stats);
|
||||
$this->entityManager->persist($commerce);
|
||||
}
|
||||
|
||||
$this->entityManager->persist($stats);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$stats->computeCompletionPercent();
|
||||
|
||||
// Calculer les statistiques de fraîcheur des données OSM
|
||||
$timestamps = [];
|
||||
foreach ($stats->getPlaces() as $place) {
|
||||
if ($place->getOsmDataDate()) {
|
||||
$timestamps[] = $place->getOsmDataDate()->getTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($timestamps)) {
|
||||
// Date la plus ancienne (min)
|
||||
$minTimestamp = min($timestamps);
|
||||
$stats->setOsmDataDateMin(new \DateTime('@' . $minTimestamp));
|
||||
|
||||
// Date la plus récente (max)
|
||||
$maxTimestamp = max($timestamps);
|
||||
$stats->setOsmDataDateMax(new \DateTime('@' . $maxTimestamp));
|
||||
|
||||
// Date moyenne
|
||||
$avgTimestamp = array_sum($timestamps) / count($timestamps);
|
||||
$stats->setOsmDataDateAvg(new \DateTime('@' . (int)$avgTimestamp));
|
||||
}
|
||||
|
||||
if($stats->getDateCreated() == null) {
|
||||
$stats->setDateCreated(new \DateTime());
|
||||
}
|
||||
|
||||
$stats->setDateModified(new \DateTime());
|
||||
|
||||
// Créer un historique des statistiques
|
||||
$statsHistory = new StatsHistory();
|
||||
$statsHistory->setDate(new \DateTime())
|
||||
->setStats($stats);
|
||||
|
||||
// Compter les Places avec email et SIRET
|
||||
$placesWithEmail = 0;
|
||||
$placesWithSiret = 0;
|
||||
foreach ($stats->getPlaces() as $place) {
|
||||
if ($place->getEmail() && $place->getEmail() !== '') {
|
||||
$placesWithEmail++;
|
||||
}
|
||||
if ($place->getSiret() && $place->getSiret() !== '') {
|
||||
$placesWithSiret++;
|
||||
}
|
||||
}
|
||||
|
||||
$statsHistory->setPlacesCount($stats->getPlaces()->count())
|
||||
->setOpeningHoursCount($stats->getAvecHoraires())
|
||||
->setAddressCount($stats->getAvecAdresse())
|
||||
->setWebsiteCount($stats->getAvecSite())
|
||||
->setSiretCount($placesWithSiret)
|
||||
->setEmailsCount($placesWithEmail)
|
||||
// ->setAccessibiliteCount($stats->getAvecAccessibilite())
|
||||
// ->setNoteCount($stats->getAvecNote())
|
||||
->setCompletionPercent($stats->getCompletionPercent())
|
||||
->setStats($stats);
|
||||
|
||||
$this->entityManager->persist($statsHistory);
|
||||
|
||||
|
||||
$this->entityManager->persist($stats);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$urls = $stats->getAllCTCUrlsMap();
|
||||
$statsHistory = $this->entityManager->getRepository(StatsHistory::class)
|
||||
->createQueryBuilder('sh')
|
||||
->where('sh.stats = :stats')
|
||||
|
@ -339,13 +256,54 @@ final class AdminController extends AbstractController
|
|||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
/*
|
||||
// La page de statistiques ne doit pas modifier les données, seulement les afficher.
|
||||
// La mise à jour des statistiques se fait lors du labourage.
|
||||
|
||||
// Calculer les statistiques
|
||||
$calculatedStats = $this->motocultrice->calculateStats($commerces);
|
||||
|
||||
// Mettre à jour les stats pour la zone donnée
|
||||
$stats->setPlacesCount($calculatedStats['places_count']);
|
||||
// ... (plus de setters) ...
|
||||
$stats->setCompletionPercent($calculatedStats['completion_percent']);
|
||||
|
||||
// ... (boucle foreach sur commerces) ...
|
||||
|
||||
$stats->computeCompletionPercent();
|
||||
|
||||
$this->entityManager->persist($stats);
|
||||
$this->entityManager->flush();
|
||||
*/
|
||||
|
||||
// Données pour le graphique des modifications par trimestre
|
||||
$modificationsByQuarter = [];
|
||||
foreach ($stats->getPlaces() as $commerce) {
|
||||
if ($commerce->getOsmDataDate()) {
|
||||
$date = $commerce->getOsmDataDate();
|
||||
$year = $date->format('Y');
|
||||
$quarter = ceil($date->format('n') / 3);
|
||||
$key = $year . '-Q' . $quarter;
|
||||
if (!isset($modificationsByQuarter[$key])) {
|
||||
$modificationsByQuarter[$key] = 0;
|
||||
}
|
||||
$modificationsByQuarter[$key]++;
|
||||
}
|
||||
}
|
||||
ksort($modificationsByQuarter); // Trier par clé (année-trimestre)
|
||||
|
||||
$overpass_query = $this->motocultrice->get_query_places($insee_code);
|
||||
$overpass_query_url = "https://overpass-turbo.eu/?Q=" . urlencode($overpass_query);
|
||||
|
||||
return $this->render('admin/stats.html.twig', [
|
||||
'stats' => $stats,
|
||||
'insee_code' => $insee_code,
|
||||
'query_places' => $this->motocultrice->get_query_places($insee_code),
|
||||
'counters' => $calculatedStats['counters'],
|
||||
'commerces' => $commerces,
|
||||
'urls' => $urls,
|
||||
'query_places' => $overpass_query,
|
||||
'overpass_query' => $overpass_query,
|
||||
'overpass_query_url' => $overpass_query_url,
|
||||
'modificationsByQuarter' => json_encode($modificationsByQuarter),
|
||||
'maptiler_token' => $_ENV['MAPTILER_TOKEN'],
|
||||
'mapbox_token' => $_ENV['MAPBOX_TOKEN'],
|
||||
'statsHistory' => $statsHistory,
|
||||
'CTC_urls' => $urls,
|
||||
]);
|
||||
|
@ -396,8 +354,9 @@ final class AdminController extends AbstractController
|
|||
* récupérer les commerces de la zone selon le code INSEE, créer les nouveaux lieux, et mettre à jour les existants
|
||||
*/
|
||||
#[Route('/admin/labourer/{insee_code}', name: 'app_admin_labourer')]
|
||||
public function labourer(string $insee_code, bool $updateExisting = true): Response
|
||||
public function labourer(Request $request, string $insee_code, bool $updateExisting = true): Response
|
||||
{
|
||||
$deleteMissing = $request->query->getBoolean('deleteMissing', true);
|
||||
|
||||
// Vérifier si le code INSEE est valide (composé uniquement de chiffres)
|
||||
if (!ctype_digit($insee_code) || $insee_code == 'undefined' || $insee_code == '') {
|
||||
|
@ -503,12 +462,14 @@ final class AdminController extends AbstractController
|
|||
}
|
||||
}
|
||||
|
||||
// Supprimer les lieux qui ne sont plus dans la réponse Overpass
|
||||
$db_places = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]);
|
||||
foreach ($db_places as $db_place) {
|
||||
if (!in_array($db_place->getOsmId(), $overpass_osm_ids)) {
|
||||
$this->entityManager->remove($db_place);
|
||||
$deletedCount++;
|
||||
// Supprimer les lieux qui ne sont plus dans la réponse Overpass, si activé
|
||||
if ($deleteMissing) {
|
||||
$db_places = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]);
|
||||
foreach ($db_places as $db_place) {
|
||||
if (!in_array($db_place->getOsmId(), $overpass_osm_ids)) {
|
||||
$this->entityManager->remove($db_place);
|
||||
$deletedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -652,16 +613,35 @@ final class AdminController extends AbstractController
|
|||
#[Route('/admin/delete_by_zone/{insee_code}', name: 'app_admin_delete_by_zone')]
|
||||
public function delete_by_zone(string $insee_code): Response
|
||||
{
|
||||
$commerces = $this->entityManager->getRepository(Place::class)->findBy(['zip_code' => $insee_code]);
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
||||
|
||||
foreach ($commerces as $commerce) {
|
||||
$this->entityManager->remove($commerce);
|
||||
}
|
||||
$this->entityManager->remove($stats);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Tous les commerces de la zone '.$insee_code.' ont été supprimés avec succès de OSM Mes commerces, mais pas dans OpenStreetMap.');
|
||||
if (!$stats) {
|
||||
$this->addFlash('error', 'Aucune statistique trouvée pour la zone ' . $insee_code);
|
||||
return $this->redirectToRoute('app_public_dashboard');
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Supprimer tous les StatsHistory associés
|
||||
foreach ($stats->getStatsHistories() as $history) {
|
||||
$this->entityManager->remove($history);
|
||||
}
|
||||
|
||||
// 2. Supprimer tous les Places associées
|
||||
foreach ($stats->getPlaces() as $place) {
|
||||
$this->entityManager->remove($place);
|
||||
}
|
||||
|
||||
// 3. Supprimer l'objet Stats lui-même
|
||||
$this->entityManager->remove($stats);
|
||||
|
||||
// 4. Appliquer les changements à la base de données
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'La zone ' . $insee_code . ' et toutes les données associées ont été supprimées avec succès.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->addFlash('error', 'Une erreur est survenue lors de la suppression de la zone ' . $insee_code . ': ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_public_dashboard');
|
||||
}
|
||||
|
|
|
@ -173,17 +173,28 @@ class PublicController extends AbstractController
|
|||
public function dashboard(): Response
|
||||
{
|
||||
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findAll();
|
||||
$stats_repo = $this->entityManager->getRepository(Stats::class)->findAll();
|
||||
|
||||
$stats_for_chart = [];
|
||||
foreach ($stats_repo as $stat) {
|
||||
if ($stat->getPlacesCount() > 0 && $stat->getName() !== null && $stat->getPopulation() > 0) {
|
||||
$stats_for_chart[] = [
|
||||
'name' => $stat->getName(),
|
||||
'placesCount' => $stat->getPlacesCount(),
|
||||
'completionPercent' => $stat->getCompletionPercent(),
|
||||
'population' => $stat->getPopulation(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Compter le nombre total de lieux
|
||||
$placesCount = $this->entityManager->getRepository(Place::class)->count([]);
|
||||
return $this->render('public/dashboard.html.twig', [
|
||||
'controller_name' => 'PublicController',
|
||||
'mapbox_token' => $_ENV['MAPBOX_TOKEN'],
|
||||
'maptiler_token' => $_ENV['MAPTILER_TOKEN'],
|
||||
'stats' => $stats,
|
||||
|
||||
'mapbox_token' => $_ENV['MAPBOX_TOKEN'] ?? null,
|
||||
'maptiler_token' => $_ENV['MAPTILER_TOKEN'] ?? null,
|
||||
'stats' => json_encode($stats_for_chart),
|
||||
'stats_list' => $stats_repo,
|
||||
'places_count' => $placesCount,
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
namespace App\Service;
|
||||
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
class Motocultrice
|
||||
{
|
||||
private $overpassApiUrl = 'https://overpass-api.de/api/interpreter';
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
{{ stats.name }} - {{ stats.completionPercent }}% complété</h1>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone}) }}" class="btn btn-primary" id="labourer">Labourer les mises à jour</a>
|
||||
<a href="{{ path('app_admin_labourer', {'insee_code': stats.zone, 'deleteMissing': 0}) }}" class="btn btn-primary" id="labourer">Labourer les mises à jour</a>
|
||||
<button id="openInJOSM" class="btn btn-secondary ms-2">
|
||||
<i class="bi bi-map"></i> Ouvrir dans JOSM
|
||||
</button>
|
||||
|
@ -83,7 +83,7 @@
|
|||
{% endif %}
|
||||
|
||||
{# Affichage de la fraîcheur des données OSM #}
|
||||
{% if stats.osmDataDateMin and stats.osmDataDateMax and stats.osmDataDateAvg %}
|
||||
{# {% if stats.osmDataDateMin and stats.osmDataDateMax and stats.osmDataDateAvg %}
|
||||
{% set now = "now"|date("U") %}
|
||||
{% set minDate = stats.osmDataDateMin|date("U") %}
|
||||
{% set maxDate = stats.osmDataDateMax|date("U") %}
|
||||
|
@ -147,7 +147,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %} #}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-12">
|
||||
|
@ -249,14 +249,14 @@
|
|||
<h2>Requête Overpass</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<pre class="p-4 bg-light">
|
||||
{{query_places|raw}}
|
||||
</pre>
|
||||
<a href="https://overpass-turbo.eu/?Q={{ query_places|url_encode }}" class="btn btn-primary" target="_blank">
|
||||
<pre class="p-4 bg-light">
|
||||
{{ overpass_query|raw }}
|
||||
</pre>
|
||||
<a href="{{ overpass_query_url }}" class="btn btn-primary" target="_blank">
|
||||
<i class="bi bi-box-arrow-up-right"></i> Exécuter dans Overpass Turbo
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="history">
|
||||
<h2>Historique des {{ statsHistory|length }} stats</h2>
|
||||
<table class="table table-bordered table-striped table-hover table-responsive js-sort-table">
|
||||
|
@ -305,18 +305,36 @@
|
|||
<div id="completionInfoContent" style="display: none;" class="mt-3">
|
||||
<p>Le score de complétion est calculé en fonction de plusieurs critères :</p>
|
||||
<ul>
|
||||
<li>Nom du commerce (obligatoire)</li>
|
||||
<li>Nom du commerce</li>
|
||||
<li>Adresse complète (numéro, rue, code postal)</li>
|
||||
<li>Horaires d'ouverture</li>
|
||||
<li>Site web</li>
|
||||
<li>Numéro de téléphone</li>
|
||||
<li>Accessibilité PMR</li>
|
||||
<li>Note descriptive</li>
|
||||
</ul>
|
||||
<p>Chaque critère rempli augmente le score de complétion. Un commerce parfaitement renseigné aura un score de 100%.</p>
|
||||
<p>Chaque critère rempli augmente le score de complétion d'une part égale.
|
||||
Un commerce parfaitement renseigné aura un score de 100%.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-calendar-event"></i> Fréquence des mises à jour (par trimestre)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="modificationsByQuarterChart" style="min-height: 250px; width: 100%;"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion mb-3" id="accordionStats">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingOne">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bouton caché pour JOSM -->
|
||||
<a id="josmButton" style="display: none;"></a>
|
||||
|
@ -326,9 +344,11 @@
|
|||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src='{{ asset('js/maplibre/maplibre-gl.js') }}'></script>
|
||||
<script src='{{ asset('js/maplibre/maplibre-gl.js') }}'></script>
|
||||
<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>
|
||||
<script>
|
||||
<script type="module">
|
||||
|
||||
|
||||
// Attendre que le DOM et tous les scripts soient chargés
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Vérifier que Chart.js est disponible
|
||||
|
@ -351,22 +371,27 @@
|
|||
let selectedFeature = null;
|
||||
|
||||
// Fonction pour calculer la distribution des taux de complétion
|
||||
function calculateCompletionDistribution(features) {
|
||||
function calculateCompletionDistribution(elements) {
|
||||
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]++;
|
||||
elements.forEach(element => {
|
||||
if (element.tags) {
|
||||
const completion = calculateCompletion(element.tags);
|
||||
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);
|
||||
function createCompletionChart(elements) {
|
||||
const chartElement = document.getElementById('completionChart');
|
||||
if (!chartElement) return;
|
||||
|
||||
const ctx = chartElement.getContext('2d');
|
||||
const distribution = calculateCompletionDistribution(elements);
|
||||
|
||||
if (completionChart) {
|
||||
completionChart.destroy();
|
||||
|
@ -401,19 +426,19 @@
|
|||
// 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();
|
||||
const response = await fetch(`https://overpass-api.de/api/interpreter?data={{ overpass_query|url_encode|raw }}`);
|
||||
const geojsonData = await response.json();
|
||||
|
||||
if (data.features && data.features.length > 0) {
|
||||
// Mettre à jour les statistiques
|
||||
const totallieux = data.features.length;
|
||||
document.getElementById('totallieux').textContent = totallieux;
|
||||
if (geojsonData.elements && geojsonData.elements.length > 0) {
|
||||
const totallieux = geojsonData.elements.length;
|
||||
const totallieuxElement = document.getElementById('totallieux');
|
||||
if (totallieuxElement) {
|
||||
totallieuxElement.textContent = totallieux;
|
||||
}
|
||||
|
||||
// Calculer et afficher la distribution des taux de complétion
|
||||
createCompletionChart(data.features);
|
||||
createCompletionChart(geojsonData.elements);
|
||||
|
||||
// Mettre à jour les marqueurs sur la carte
|
||||
dropMarkers = updateMarkers(data.features, map, currentMarkerType, dropMarkers, data);
|
||||
dropMarkers = updateMarkers(geojsonData.elements, map, currentMarkerType, dropMarkers, geojsonData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des lieux:', error);
|
||||
|
@ -477,6 +502,57 @@
|
|||
|
||||
// Charger les lieux au démarrage
|
||||
loadPlaces();
|
||||
|
||||
// Graphique des modifications par trimestre
|
||||
const modificationsData = JSON.parse('{{ modificationsByQuarter|raw }}');
|
||||
const quarterLabels = Object.keys(modificationsData);
|
||||
const quarterValues = Object.values(modificationsData);
|
||||
console.log('modificationsData', modificationsData);
|
||||
console.log('quarterLabels', quarterLabels);
|
||||
console.log('quarterValues', quarterValues);
|
||||
|
||||
if (quarterLabels.length > 0) {
|
||||
const chartElement = document.getElementById('modificationsByQuarterChart');
|
||||
if (chartElement) {
|
||||
const ctxQuarter = chartElement.getContext('2d');
|
||||
new Chart(ctxQuarter, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: quarterLabels,
|
||||
datasets: [{
|
||||
label: 'Nombre de modifications',
|
||||
data: quarterValues,
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.6)',
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
stepSize: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn('Canvas #modificationsByQuarterChart introuvable dans le DOM');
|
||||
}
|
||||
} else {
|
||||
console.warn('Aucune donnée de trimestre à afficher pour le graphique.');
|
||||
const chartElement = document.getElementById('modificationsByQuarterChart');
|
||||
if (chartElement) {
|
||||
chartElement.parentNode.innerHTML += '<div class="text-muted p-3">Aucune donnée de modification disponible pour cette zone.</div>';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -494,12 +570,12 @@
|
|||
let contextMenu = null; // Menu contextuel
|
||||
|
||||
|
||||
function calculateCompletion(element) {
|
||||
let completionCount = 0;
|
||||
let totalFields = 0;
|
||||
function calculateCompletion(element) {
|
||||
let completionCount = 0;
|
||||
let totalFields = 0;
|
||||
let missingFields = [];
|
||||
|
||||
const fieldsToCheck = [
|
||||
|
||||
const fieldsToCheck = [
|
||||
{ name: 'name', label: 'Nom du commerce' },
|
||||
{ name: 'contact:street', label: 'Rue' },
|
||||
{ name: 'contact:housenumber', label: 'Numéro' },
|
||||
|
@ -507,12 +583,12 @@ function calculateCompletion(element) {
|
|||
{ name: 'contact:website', label: 'Site web' },
|
||||
{ name: 'contact:phone', label: 'Téléphone' },
|
||||
{ name: 'wheelchair', label: 'Accessibilité PMR' }
|
||||
];
|
||||
];
|
||||
|
||||
fieldsToCheck.forEach(field => {
|
||||
totalFields++;
|
||||
fieldsToCheck.forEach(field => {
|
||||
totalFields++;
|
||||
if (element.tags && element.tags[field.name]) {
|
||||
completionCount++;
|
||||
completionCount++;
|
||||
} else {
|
||||
missingFields.push(field.label);
|
||||
}
|
||||
|
@ -537,10 +613,10 @@ function getCompletionColor(completion) {
|
|||
}
|
||||
|
||||
|
||||
function createPopupContent(element) {
|
||||
function createPopupContent(element) {
|
||||
const completion = calculateCompletion(element);
|
||||
let content = `
|
||||
<div class="mb-2">
|
||||
let content = `
|
||||
<div class="mb-2">
|
||||
<h5>${element.tags?.name || 'Sans nom'}</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<a class="btn btn-primary btn-sm" href="/admin/placeType/${element.type}/${element.id}">
|
||||
|
@ -550,9 +626,9 @@ function createPopupContent(element) {
|
|||
<i class="bi bi-map"></i> OSM
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (completion.percentage < 100) {
|
||||
content += `
|
||||
<div class="alert alert-warning mt-2">
|
||||
|
@ -565,17 +641,17 @@ function createPopupContent(element) {
|
|||
}
|
||||
|
||||
content += '<table class="table table-sm mt-2">';
|
||||
|
||||
// Ajouter tous les tags
|
||||
if (element.tags) {
|
||||
for (const tag in element.tags) {
|
||||
content += `<tr><td><strong>${tag}</strong></td><td>${element.tags[tag]}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Ajouter tous les tags
|
||||
if (element.tags) {
|
||||
for (const tag in element.tags) {
|
||||
content += `<tr><td><strong>${tag}</strong></td><td>${element.tags[tag]}</td></tr>`;
|
||||
content += '</table>';
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
content += '</table>';
|
||||
return content;
|
||||
}
|
||||
|
||||
function updateMarkers(features, map, currentMarkerType, dropMarkers, overpassData) {
|
||||
// Supprimer tous les marqueurs existants
|
||||
|
@ -822,7 +898,7 @@ window.updateMarkers = updateMarkers;
|
|||
const tempLink = document.createElement('a');
|
||||
tempLink.style.display = 'none';
|
||||
document.body.appendChild(tempLink);
|
||||
|
||||
|
||||
tempLink.href = josmUrl;
|
||||
tempLink.click();
|
||||
document.body.removeChild(tempLink);
|
||||
|
@ -908,7 +984,7 @@ window.updateMarkers = updateMarkers;
|
|||
document.getElementById('openInJOSM').addEventListener('click', openInJOSM);
|
||||
|
||||
// Attendre que la carte soit chargée avant d'ajouter les écouteurs d'événements
|
||||
map.on('load', function() {
|
||||
map.on('load', function() {
|
||||
map_is_loaded = true;
|
||||
// Changer le curseur au survol des marqueurs
|
||||
map.on('mouseenter', function(e) {
|
||||
|
@ -984,6 +1060,7 @@ window.updateMarkers = updateMarkers;
|
|||
icon.classList.add('bi-chevron-down');
|
||||
}
|
||||
}
|
||||
window.toggleCompletionInfo = toggleCompletionInfo;
|
||||
|
||||
|
||||
// infos depuis complète tes commerces : CTC
|
||||
|
@ -1102,7 +1179,6 @@ function makeDonutGraphOfTags() {
|
|||
labels: {
|
||||
boxWidth: 15,
|
||||
padding: 15,
|
||||
font: {
|
||||
size: 12
|
||||
}
|
||||
}
|
||||
|
@ -1115,10 +1191,11 @@ function makeDonutGraphOfTags() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
makeDonutGraphOfTags();
|
||||
markClosedSiretsOnTable();
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -117,8 +117,8 @@
|
|||
|
||||
<div class="small osm-modification-info">
|
||||
<div>
|
||||
{{ commerce.osmDataDate|date('Y-m-d H:i') }}
|
||||
<i class="bi bi-calendar"></i>
|
||||
{{ commerce.osmDataDate|date('d/m/Y H:i') }}
|
||||
</div>
|
||||
{% if commerce.osmUser %}
|
||||
<div>
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
Statistiques des villes (nombre de commerces)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="statsBubbleChart" style="min-height: 400px; width: 100%;"></canvas>
|
||||
<canvas id="statsBubbleChart" style="min-height: 400px; width: 100%;" data-stats="{{ stats|raw }}"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -110,7 +110,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for stat in stats %}
|
||||
{% for stat in stats_list %}
|
||||
<tr>
|
||||
<td><a href="{{ path('app_admin_stats', {'insee_code': stat.zone}) }}" title="Voir les statistiques de cette ville">
|
||||
{{ stat.name }}
|
||||
|
@ -121,8 +121,8 @@
|
|||
</a></td>
|
||||
<td>{{ stat.zone }}</td>
|
||||
<td>{{ stat.completionPercent }}%</td>
|
||||
<td>{{ stat.placesCount }}</td>
|
||||
<td>{{ (stat.placesCount / (stat.population or 1 ))|round(2) }}</td>
|
||||
<td>{{ stat.places|length }}</td>
|
||||
<td>{{ (stat.places|length / (stat.population or 1 ))|round(2) }}</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{{ path('app_admin_stats', {'insee_code': stat.zone}) }}" class="btn btn-sm btn-primary" title="Voir les statistiques de cette ville">
|
||||
|
@ -158,113 +158,5 @@
|
|||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
{# Les scripts sont maintenant gérés par Webpack Encore via app.js #}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const statsDataRaw = [
|
||||
{% for stat in stats %}
|
||||
{% if stat.placesCount > 0 and stat.name is not null and stat.population > 0 %}
|
||||
{
|
||||
label: '{{ (stat.name ~ " (" ~ stat.zone ~ ")")|e('js') }}',
|
||||
placesCount: {{ stat.placesCount }},
|
||||
completion: {{ stat.completionPercent|default(0) }},
|
||||
x: {{ stat.population }},
|
||||
y: {{ (stat.placesCount / stat.population * 1000)|round(2) }}
|
||||
},
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
const ctx = document.getElementById('statsBubbleChart');
|
||||
if (ctx && statsDataRaw.length > 0) {
|
||||
const statsData = statsDataRaw.map(d => ({
|
||||
...d,
|
||||
r: Math.sqrt(d.placesCount) * 2.5 // Utilise la racine carrée pour la taille, avec un facteur d'échelle
|
||||
}));
|
||||
|
||||
new Chart(ctx.getContext('2d'), {
|
||||
type: 'bubble',
|
||||
data: {
|
||||
datasets: [{
|
||||
label: 'Commerces par ville',
|
||||
data: statsData,
|
||||
backgroundColor: statsData.map(d => `hsla(120, 60%, 70%, ${d.completion / 120 + 0.2})`),
|
||||
borderColor: 'hsl(120, 60%, 40%)',
|
||||
borderWidth: 1,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const data = context.dataset.data[context.dataIndex];
|
||||
let label = data.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
label += `${data.placesCount} commerces, ${data.y} pour 1000 hab., ${data.completion}% complétion`;
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'logarithmic',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Population (échelle log)'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Commerces pour 1000 habitants'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// La fonction est maintenant globale grâce à l'import dans app.js
|
||||
if (typeof colorizePercentageCells === 'function') {
|
||||
colorizePercentageCells('td:nth-child(3)');
|
||||
}
|
||||
|
||||
// Gérer le formulaire de labourage
|
||||
const labourageForm = document.getElementById('labourerForm');
|
||||
const citySearchInput = document.getElementById('citySearch');
|
||||
const selectedZipCodeInput = document.getElementById('selectedZipCode');
|
||||
const labourageBtn = labourageForm.querySelector('button[type="submit"]');
|
||||
const originalBtnHtml = labourageBtn.innerHTML;
|
||||
|
||||
if (labourageForm && citySearchInput && typeof setupCitySearch === 'function') {
|
||||
setupCitySearch('citySearch', 'citySuggestions', function (suggestion) {
|
||||
// Afficher le spinner et désactiver le bouton
|
||||
labourageBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Chargement...';
|
||||
labourageBtn.disabled = true;
|
||||
citySearchInput.disabled = true;
|
||||
|
||||
if (suggestion.insee) {
|
||||
window.location.href = `/admin/labourer/${suggestion.insee}`;
|
||||
} else if (suggestion.postcode) {
|
||||
// Moins probable, mais en solution de repli
|
||||
window.location.href = `/admin/labourer/${suggestion.postcode}`;
|
||||
}
|
||||
});
|
||||
|
||||
labourageForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
alert("Veuillez rechercher et sélectionner une ville directement dans la liste de suggestions.");
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{# Le script du graphique est maintenant dans assets/app.js #}
|
||||
{% endblock %}
|
Loading…
Add table
Add a link
Reference in a new issue