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()

File diff suppressed because one or more lines are too long

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,100 +23,168 @@ 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>`;
} }
map.on('click', function(e) { const mapContainer = document.getElementById("map");
const radiusInPixels = 10; // Rayon autour du clic
const features = map.queryRenderedFeatures([ if (mapContainer && typeof maplibregl !== 'undefined') {
[e.point.x - radiusInPixels, e.point.y - radiusInPixels], const map = new maplibregl.Map({
[e.point.x + radiusInPixels, e.point.y + radiusInPixels] container: mapContainer,
], { style: window.MAP_STYLE_LIGHT,
layers: ['interventions'] center: [4.3572, 50.8477],
zoom: 11,
minzoom: 0,
maxzoom: 24
}); });
if (!features.length) { async function fetchAndMergeGeoJSON(urls) {
return; 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
};
} }
// Générer le contenu de la popup avec tous les points proches map.on('load', async () => {
const popupContent = features.map((feature) => { if (typeof interventions_geojson_urls !== 'undefined' && interventions_geojson_urls.length > 0) {
const props = feature.properties; const mergedGeoJSON = await fetchAndMergeGeoJSON(interventions_geojson_urls);
return `
<div>
<strong><a href="${interventionDetailUrlPattern.replace('/0/', '/' + props.id + '/')}">${props.code || gettext('Intervention')}</a></strong><br/>
${gettext("Statut")}: ${statusFormatter(props.status)}<br/>
${gettext("Titre")}: ${props.title || '-'}
</div>
<hr/>
`;
}).join('');
// Afficher la popup à l’endroit du clic map.addSource('interventions', {
new maplibregl.Popup() 'type': 'geojson',
.setLngLat(e.lngLat) 'data': mergedGeoJSON
.setHTML(popupContent) });
.addTo(map);
});
map.on('mouseenter', 'interventions', () => { map.addLayer({
map.getCanvas().style.cursor = 'pointer'; 'id': 'interventions',
}); 'type': 'circle',
map.on('mouseleave', 'interventions', () => { 'source': 'interventions',
map.getCanvas().style.cursor = ''; minzoom: 0,
}); maxzoom: 24,
'paint': {
function populateLegend() { 'circle-radius': [
const statuses = ['to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished']; 'interpolate',
const legendContainer = document.getElementById('map-legend'); ['linear'],
['zoom'],
if (!legendContainer) return; 10, 5,
22, 15
const legendItems = statuses.map(status => { ],
return `<div class="mb-1">${statusFormatter(status)}</div>`; 'circle-color': typeof INTERVENTION_STATUS_COLOR_EXPR !== 'undefined' ? INTERVENTION_STATUS_COLOR_EXPR : '#0094ff',
'circle-opacity': 0.7
}
});
}
}); });
legendContainer.innerHTML = ` map.on('click', function(e) {
${legendItems.join('')} const radiusInPixels = 10;
`;
const features = map.queryRenderedFeatures([
[e.point.x - radiusInPixels, e.point.y - radiusInPixels],
[e.point.x + radiusInPixels, e.point.y + radiusInPixels]
], {
layers: ['interventions']
});
if (!features.length) {
return;
}
const popupContent = features.map((feature) => {
const props = feature.properties;
const detailUrl = (typeof interventionDetailUrlPattern !== 'undefined')
? interventionDetailUrlPattern.replace('/0/', '/' + props.id + '/')
: '#';
return `
<div>
<strong><a href="${detailUrl}">${props.code || gettext('Intervention')}</a></strong><br/>
${gettext("Statut")}: ${statusFormatter(props.status)}<br/>
${gettext("Titre")}: ${props.title || '-'}
</div>
<hr/>
`;
}).join('');
new maplibregl.Popup()
.setLngLat(e.lngLat)
.setHTML(popupContent)
.addTo(map);
});
map.on('mouseenter', 'interventions', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'interventions', () => {
map.getCanvas().style.cursor = '';
});
function populateLegend() {
const statuses = ['to_be_processed', 'assigned', 'in_progress', 'on_pause', 'finished'];
const legendContainer = document.getElementById('map-legend');
if (!legendContainer) return;
const legendItems = statuses.map(status => {
return `<div class="mb-1">${statusFormatter(status)}</div>`;
});
legendContainer.innerHTML = legendItems.join('');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', populateLegend);
} else {
populateLegend();
}
} }
document.addEventListener('DOMContentLoaded', populateLegend);
// ### chart ### // ### chart ###
const series = [
{
name: gettext('Correctif'),
type: 'line',
smooth: true,
data: seriesData['Correctif'],
itemStyle: { color: '#dc3545' } // Bootstrap "danger"
},
{
name: gettext('Préventif'),
type: 'line',
smooth: true,
data: seriesData['Préventif'],
itemStyle: { color: '#ffc107' } // Bootstrap "warning"
},
{
name: gettext('Amélioratif'),
type: 'line',
smooth: true,
data: seriesData['Amélioratif'],
itemStyle: { color: '#28a745' } // Bootstrap "success"
}
];
const option = {
legend: { data: ['Correctif', 'Préventif', 'Amélioratif'] },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: seriesData.dates },
yAxis: { type: 'value' },
series: series
};
const chartDom = document.getElementById('chartNbrInterventions'); const chartDom = document.getElementById('chartNbrInterventions');
const myChart = echarts.init(chartDom); if (chartDom && typeof echarts !== 'undefined' && typeof seriesData !== 'undefined') {
myChart.setOption(option); const series = [
{
name: gettext('Correctif'),
type: 'line',
smooth: true,
data: seriesData['Correctif'] || [],
itemStyle: { color: '#dc3545' }
},
{
name: gettext('Préventif'),
type: 'line',
smooth: true,
data: seriesData['Préventif'] || [],
itemStyle: { color: '#ffc107' }
},
{
name: gettext('Amélioratif'),
type: 'line',
smooth: true,
data: seriesData['Amélioratif'] || [],
itemStyle: { color: '#28a745' }
}
];
window.addEventListener('resize', () => myChart.resize()); const option = {
legend: { data: [gettext('Correctif'), gettext('Préventif'), gettext('Amélioratif')] },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: seriesData.dates || [] },
yAxis: { type: 'value' },
series: series
};
const myChart = echarts.init(chartDom);
myChart.setOption(option);
window.addEventListener('resize', () => myChart.resize());
}