fix(thematics, home): mise à jour des thématiques (Sécurité, Stationnement, Propreté) et sécurisation du chargement de la carte sur l'accueil

This commit is contained in:
kdeterme 2026-09-18 19:22:59 +02:00
parent bbaa092550
commit aad07929b9
5 changed files with 620 additions and 610 deletions

View file

@ -9,7 +9,7 @@ def create_parking_thematic(apps, schema_editor):
thematic, _created = Thematic.objects.get_or_create( thematic, _created = Thematic.objects.get_or_create(
code='parking', code='parking',
defaults={ defaults={
'name_fr': 'Stationnement & Mobilité', 'name_fr': 'Stationnement',
'name_nl': 'Parkeren & Mobiliteit', 'name_nl': 'Parkeren & Mobiliteit',
'description': 'Thématique dédiée aux emplacements de stationnement et zones de mobilité (vélos, trottinettes, PMR, livraison)', 'description': 'Thématique dédiée aux emplacements de stationnement et zones de mobilité (vélos, trottinettes, PMR, livraison)',
'icon': 'bi-p-square', 'icon': 'bi-p-square',

View file

@ -13,7 +13,7 @@ def create_bike_parking_category(apps, schema_editor):
thematic, _ = Thematic.objects.get_or_create( thematic, _ = Thematic.objects.get_or_create(
code='parking', code='parking',
defaults={ defaults={
'name_fr': 'Stationnement & Mobilité', 'name_fr': 'Stationnement',
'name_nl': 'Parkeren & Mobiliteit', 'name_nl': 'Parkeren & Mobiliteit',
'description': 'Thématique dédiée aux emplacements de stationnement et zones de mobilité (vélos, trottinettes, PMR, livraison)', 'description': 'Thématique dédiée aux emplacements de stationnement et zones de mobilité (vélos, trottinettes, PMR, livraison)',
'icon': 'bi-p-square', 'icon': 'bi-p-square',

View file

@ -22,7 +22,7 @@ MAX_ASSET_FEATURES = 5000
@login_required @login_required
def parking_index(request): def parking_index(request):
"""Page d'accueil de la thématique Stationnement & Mobilité.""" """Page d'accueil de la thématique Stationnement."""
check_thematic_access(request.user, 'parking') check_thematic_access(request.user, 'parking')
parking_thematic = Thematic.objects.filter(code='parking').first() parking_thematic = Thematic.objects.filter(code='parking').first()

View file

@ -121,9 +121,9 @@
"pk": 11, "pk": 11,
"fields": { "fields": {
"code": "dpv_micro_projects_security", "code": "dpv_micro_projects_security",
"name_fr": "DPV Micro-projets Sécurité", "name_fr": "Sécurité",
"name_nl": "DPV Micro-projecten Beveiliging", "name_nl": "Veiligheid",
"description": "", "description": "Gestion de la sécurité publique et des équipements associés",
"icon": "bi-exclamation-diamond", "icon": "bi-exclamation-diamond",
"color": "#000000", "color": "#000000",
"uses_occupations": false "uses_occupations": false
@ -142,6 +142,32 @@
"uses_occupations": false "uses_occupations": false
} }
}, },
{
"model": "common.thematic",
"pk": 13,
"fields": {
"code": "parking",
"name_fr": "Stationnement",
"name_nl": "Parkeren",
"description": "Thématique dédiée aux emplacements de stationnement et zones de mobilité (vélos, trottinettes, PMR, livraison)",
"icon": "bi-p-square",
"color": "#0d6efd",
"uses_occupations": false
}
},
{
"model": "common.thematic",
"pk": 14,
"fields": {
"code": "clean",
"name_fr": "Propreté",
"name_nl": "Netheid",
"description": "Gestion de la propreté publique, corbeilles et conteneurs",
"icon": "bi-trash3",
"color": "#198754",
"uses_occupations": false
}
},
{ {
"model": "common.role", "model": "common.role",
"pk": 1, "pk": 1,

View file

@ -1,86 +1,3 @@
const map = new maplibregl.Map({
container: "map",
style: window.MAP_STYLE_LIGHT,
//style: 'https://tiles.openfreemap.org/styles/positron',
center: [4.3572, 50.8477],
zoom: 11,
minzoom: 0,
maxzoom: 24
});
async function fetchAndMergeGeoJSON(urls) {
const allFeatures = [];
for (const url of urls) {
const response = await fetch(url);
const data = await response.json();
if (data && data.features) {
allFeatures.push(...data.features);
}
}
return {
type: 'FeatureCollection',
features: allFeatures
};
}
map.on('load', async () => {
const mergedGeoJSON = await fetchAndMergeGeoJSON(interventions_geojson_urls);
map.addSource('interventions', {
'type': 'geojson',
'data': mergedGeoJSON
});
// map.addLayer({
// id: 'heatmap',
// type: 'heatmap',
// source: 'interventions',
// paint: {
// 'heatmap-weight': ['get', 'density'],
// 'heatmap-intensity': 1,
// 'heatmap-color': [
// 'interpolate',
// ['linear'],
// ['heatmap-density'],
// 0, 'rgba(33,102,172,0)',
// 0.2, 'rgb(103,169,207)',
// 0.4, 'rgb(209,229,240)',
// 0.6, 'rgb(253,219,199)',
// 0.8, 'rgb(239,138,98)',
// 1, 'rgb(178,24,43)'
// ],
// 'heatmap-radius': 15,
// 'heatmap-opacity': 0.75
// }
// });
map.addLayer({
'id': 'interventions',
'type': 'circle',
'source': 'interventions',
//filter: ['has', 'point_count'],
minzoom: 0,
maxzoom: 24,
'paint': {
'circle-radius': [
'interpolate',
['linear'],
['zoom'],
10, 5,
22, 15
],
//'circle-color': '#0094ff',
'circle-color': INTERVENTION_STATUS_COLOR_EXPR,
'circle-opacity': 0.7
}
});
});
function statusFormatter(value) { function statusFormatter(value) {
const icons = { const icons = {
'in_preparation': { label: gettext('En préparation'), icon: 'bi-pencil', color: 'secondary' }, 'in_preparation': { label: gettext('En préparation'), icon: 'bi-pencil', color: 'secondary' },
@ -97,7 +14,6 @@ function statusFormatter(value) {
'validated': { label: gettext('Validé'), icon: 'bi-award', color: 'success' }, 'validated': { label: gettext('Validé'), icon: 'bi-award', color: 'success' },
'invoiced': { label: gettext('Comptabilisé'), icon: 'bi-receipt', color: 'dark' }, 'invoiced': { label: gettext('Comptabilisé'), icon: 'bi-receipt', color: 'dark' },
'closed': { label: gettext('Finalisé'), icon: 'bi-archive', color: 'dark' }, 'closed': { label: gettext('Finalisé'), icon: 'bi-archive', color: 'dark' },
// 'archived': { label: gettext('Archivé'), icon: 'bi-box-seam', color: 'dark' },
'canceled': { label: gettext('Annulé'), icon: 'bi-x-circle', color: 'secondary' }, 'canceled': { label: gettext('Annulé'), icon: 'bi-x-circle', color: 'secondary' },
}; };
@ -107,8 +23,71 @@ function statusFormatter(value) {
return `<span class="badge text-bg-${entry.color} status-${value}"><i class="bi ${entry.icon} me-2"></i>${entry.label}</span>`; return `<span class="badge text-bg-${entry.color} status-${value}"><i class="bi ${entry.icon} me-2"></i>${entry.label}</span>`;
} }
const mapContainer = document.getElementById("map");
if (mapContainer && typeof maplibregl !== 'undefined') {
const map = new maplibregl.Map({
container: mapContainer,
style: window.MAP_STYLE_LIGHT,
center: [4.3572, 50.8477],
zoom: 11,
minzoom: 0,
maxzoom: 24
});
async function fetchAndMergeGeoJSON(urls) {
const allFeatures = [];
for (const url of urls) {
try {
const response = await fetch(url);
const data = await response.json();
if (data && data.features) {
allFeatures.push(...data.features);
}
} catch (err) {
console.warn('Error fetching geojson:', url, err);
}
}
return {
type: 'FeatureCollection',
features: allFeatures
};
}
map.on('load', async () => {
if (typeof interventions_geojson_urls !== 'undefined' && interventions_geojson_urls.length > 0) {
const mergedGeoJSON = await fetchAndMergeGeoJSON(interventions_geojson_urls);
map.addSource('interventions', {
'type': 'geojson',
'data': mergedGeoJSON
});
map.addLayer({
'id': 'interventions',
'type': 'circle',
'source': 'interventions',
minzoom: 0,
maxzoom: 24,
'paint': {
'circle-radius': [
'interpolate',
['linear'],
['zoom'],
10, 5,
22, 15
],
'circle-color': typeof INTERVENTION_STATUS_COLOR_EXPR !== 'undefined' ? INTERVENTION_STATUS_COLOR_EXPR : '#0094ff',
'circle-opacity': 0.7
}
});
}
});
map.on('click', function(e) { map.on('click', function(e) {
const radiusInPixels = 10; // Rayon autour du clic const radiusInPixels = 10;
const features = map.queryRenderedFeatures([ const features = map.queryRenderedFeatures([
[e.point.x - radiusInPixels, e.point.y - radiusInPixels], [e.point.x - radiusInPixels, e.point.y - radiusInPixels],
@ -121,12 +100,14 @@ map.on('click', function(e) {
return; return;
} }
// Générer le contenu de la popup avec tous les points proches
const popupContent = features.map((feature) => { const popupContent = features.map((feature) => {
const props = feature.properties; const props = feature.properties;
const detailUrl = (typeof interventionDetailUrlPattern !== 'undefined')
? interventionDetailUrlPattern.replace('/0/', '/' + props.id + '/')
: '#';
return ` return `
<div> <div>
<strong><a href="${interventionDetailUrlPattern.replace('/0/', '/' + props.id + '/')}">${props.code || gettext('Intervention')}</a></strong><br/> <strong><a href="${detailUrl}">${props.code || gettext('Intervention')}</a></strong><br/>
${gettext("Statut")}: ${statusFormatter(props.status)}<br/> ${gettext("Statut")}: ${statusFormatter(props.status)}<br/>
${gettext("Titre")}: ${props.title || '-'} ${gettext("Titre")}: ${props.title || '-'}
</div> </div>
@ -134,7 +115,6 @@ map.on('click', function(e) {
`; `;
}).join(''); }).join('');
// Afficher la popup à l’endroit du clic
new maplibregl.Popup() new maplibregl.Popup()
.setLngLat(e.lngLat) .setLngLat(e.lngLat)
.setHTML(popupContent) .setHTML(popupContent)
@ -158,49 +138,53 @@ function populateLegend() {
return `<div class="mb-1">${statusFormatter(status)}</div>`; return `<div class="mb-1">${statusFormatter(status)}</div>`;
}); });
legendContainer.innerHTML = ` legendContainer.innerHTML = legendItems.join('');
${legendItems.join('')}
`;
} }
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', populateLegend); document.addEventListener('DOMContentLoaded', populateLegend);
} else {
populateLegend();
}
}
// ### chart ### // ### chart ###
const chartDom = document.getElementById('chartNbrInterventions');
if (chartDom && typeof echarts !== 'undefined' && typeof seriesData !== 'undefined') {
const series = [ const series = [
{ {
name: gettext('Correctif'), name: gettext('Correctif'),
type: 'line', type: 'line',
smooth: true, smooth: true,
data: seriesData['Correctif'], data: seriesData['Correctif'] || [],
itemStyle: { color: '#dc3545' } // Bootstrap "danger" itemStyle: { color: '#dc3545' }
}, },
{ {
name: gettext('Préventif'), name: gettext('Préventif'),
type: 'line', type: 'line',
smooth: true, smooth: true,
data: seriesData['Préventif'], data: seriesData['Préventif'] || [],
itemStyle: { color: '#ffc107' } // Bootstrap "warning" itemStyle: { color: '#ffc107' }
}, },
{ {
name: gettext('Amélioratif'), name: gettext('Amélioratif'),
type: 'line', type: 'line',
smooth: true, smooth: true,
data: seriesData['Amélioratif'], data: seriesData['Amélioratif'] || [],
itemStyle: { color: '#28a745' } // Bootstrap "success" itemStyle: { color: '#28a745' }
} }
]; ];
const option = { const option = {
legend: { data: ['Correctif', 'Préventif', 'Amélioratif'] }, legend: { data: [gettext('Correctif'), gettext('Préventif'), gettext('Amélioratif')] },
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: seriesData.dates }, xAxis: { type: 'category', data: seriesData.dates || [] },
yAxis: { type: 'value' }, yAxis: { type: 'value' },
series: series series: series
}; };
const chartDom = document.getElementById('chartNbrInterventions');
const myChart = echarts.init(chartDom); const myChart = echarts.init(chartDom);
myChart.setOption(option); myChart.setOption(option);
window.addEventListener('resize', () => myChart.resize()); window.addEventListener('resize', () => myChart.resize());
}