From 90323a5013dc01995b25fd4a415fa47a4df15e12 Mon Sep 17 00:00:00 2001 From: kdeterme Date: Fri, 21 Aug 2026 11:40:21 +0200 Subject: [PATCH] feat: update exceptional transport widget with refined UI, new status filters, and improved structure detail display --- loko/assets/views/structures.py | 53 +++-- .../home/widgets/exceptional_transport.html | 210 +++++++++++------- 2 files changed, 158 insertions(+), 105 deletions(-) diff --git a/loko/assets/views/structures.py b/loko/assets/views/structures.py index 4ec617e..c1dabd8 100644 --- a/loko/assets/views/structures.py +++ b/loko/assets/views/structures.py @@ -723,19 +723,21 @@ def structures_edit_exceptional_transport(request, structure_id): @require_GET def structures_exceptional_transport_geojson(request): - """Retourne l'ensemble des ouvrages avec données de transport exceptionnel au format GeoJSON.""" + """Retourne l'ensemble des ouvrages pertinents (ponts, viaducs, tunnels, passerelles...) au format GeoJSON.""" if not request.user.is_authenticated or not can_view_exceptional_transport(request.user): return JsonResponse({'type': 'FeatureCollection', 'features': []}) - te_list = ( - StructureExceptionalTransport.objects - .select_related('structure', 'structure__main_location') + structures = ( + Structure.objects + .exclude(structure_type='gallery') + .exclude(name_fr__icontains='galerie technique') + .exclude(name_nl__icontains='technische koker') + .select_related('exceptional_transport_info', 'main_location') .all() ) features = [] - for te in te_list: - structure = te.structure + for structure in structures: loc = structure.main_location if not loc: continue @@ -760,9 +762,10 @@ def structures_exceptional_transport_geojson(request): continue detail_url = reverse('assets:structures_locations_detail', args=[loc.id]) + te = getattr(structure, 'exceptional_transport_info', None) h_str = "" - if te.clearance_height_left_m or te.clearance_height_center_m or te.clearance_height_right_m: + if te and (te.clearance_height_left_m or te.clearance_height_center_m or te.clearance_height_right_m): h_left = f"{te.clearance_height_left_m}m" if te.clearance_height_left_m else "-" h_center = f"{te.clearance_height_center_m}m" if te.clearance_height_center_m else "-" h_right = f"{te.clearance_height_right_m}m" if te.clearance_height_right_m else "-" @@ -775,33 +778,35 @@ def structures_exceptional_transport_geojson(request): 'coordinates': [lon, lat] }, 'properties': { - 'id': te.id, + 'id': te.id if te else None, + 'has_te_info': bool(te), 'structure_id': structure.id, 'structure_code': structure.code or '', 'name': structure.get_name(), 'name_fr': structure.name_fr or '', 'name_nl': structure.name_nl or '', 'structure_type': structure.get_structure_type_display() if hasattr(structure, 'get_structure_type_display') else (structure.structure_type or ''), + 'structure_type_code': structure.structure_type or '', 'location_id': loc.id, 'location_code': loc.code or '', 'detail_url': detail_url, - 'fod_vnr': te.fod_vnr, - 'national_id': te.national_id or '', - 'bridge_class': te.bridge_class or '', - 'passage_status': te.passage_status, - 'passage_status_display': str(te.get_passage_status_display()), - 'max_authorized_weight_tonnes': float(te.max_authorized_weight_tonnes) if te.max_authorized_weight_tonnes is not None else None, - 'min_guaranteed_capacity_tonnes': float(te.min_guaranteed_capacity_tonnes) if te.min_guaranteed_capacity_tonnes is not None else None, - 'max_calculated_weight_tonnes': float(te.max_calculated_weight_tonnes) if te.max_calculated_weight_tonnes is not None else None, - 'usable_width_m': float(te.usable_width_m) if te.usable_width_m is not None else None, + 'fod_vnr': te.fod_vnr if te else None, + 'national_id': te.national_id if te and te.national_id else '', + 'bridge_class': te.bridge_class if te and te.bridge_class else '', + 'passage_status': te.passage_status if te else 'not_evaluated', + 'passage_status_display': str(te.get_passage_status_display()) if te else str(_("Non évalué")), + 'max_authorized_weight_tonnes': float(te.max_authorized_weight_tonnes) if te and te.max_authorized_weight_tonnes is not None else None, + 'min_guaranteed_capacity_tonnes': float(te.min_guaranteed_capacity_tonnes) if te and te.min_guaranteed_capacity_tonnes is not None else None, + 'max_calculated_weight_tonnes': float(te.max_calculated_weight_tonnes) if te and te.max_calculated_weight_tonnes is not None else None, + 'usable_width_m': float(te.usable_width_m) if te and te.usable_width_m is not None else None, 'clearance_heights_str': h_str, - 'max_speed_kmh': te.max_speed_kmh, - 'is_braking_forbidden': te.is_braking_forbidden, - 'is_isolated_crossing_only': te.is_isolated_crossing_only, - 'safety_distance_m': float(te.safety_distance_m) if te.safety_distance_m is not None else None, - 'prescriptions_fr': te.prescriptions_fr or '', - 'prescriptions_nl': te.prescriptions_nl or '', - 'last_study_ref': te.last_study_ref or '', + 'max_speed_kmh': te.max_speed_kmh if te else None, + 'is_braking_forbidden': te.is_braking_forbidden if te else False, + 'is_isolated_crossing_only': te.is_isolated_crossing_only if te else False, + 'safety_distance_m': float(te.safety_distance_m) if te and te.safety_distance_m is not None else None, + 'prescriptions_fr': te.prescriptions_fr if te and te.prescriptions_fr else '', + 'prescriptions_nl': te.prescriptions_nl if te and te.prescriptions_nl else '', + 'last_study_ref': te.last_study_ref if te and te.last_study_ref else '', } }) diff --git a/loko/home/templates/home/widgets/exceptional_transport.html b/loko/home/templates/home/widgets/exceptional_transport.html index 25ccd53..9661285 100644 --- a/loko/home/templates/home/widgets/exceptional_transport.html +++ b/loko/home/templates/home/widgets/exceptional_transport.html @@ -13,14 +13,14 @@ {% translate "Transport Exceptionnel & Convois Lourds" %} - {% translate "Cartographie du réseau d'ouvrages d'art et conditions d'admissibilité" %} + {% translate "Cartographie du réseau d'ouvrages d'art (ponts, viaducs, tunnels, passerelles) et conditions de passage" %}
- 0 {% translate "ouvrages répertoriés" %} + 0 {% translate "ouvrages sur la carte" %} {% translate "Tous les ouvrages" %} @@ -33,7 +33,7 @@
-
+
@@ -41,7 +41,7 @@
-
+
- +
-
+
@@ -86,7 +89,7 @@
+ style="z-index: 5; font-size: 0.8rem; max-width: 300px; backdrop-filter: blur(8px);">
{% translate "Statut de passage" %}
@@ -100,16 +103,12 @@ {% translate "Sous conditions / prescriptions" %}
- - {% translate "Étude préalable requise" %} + + {% translate "Non évalué (fiche à compléter)" %}
- {% translate "Interdit" %} -
-
- - {% translate "Non évalué" %} + {% translate "Interdit / Étude requise" %}
@@ -218,13 +217,14 @@ function updateCounts() { let total = teFeatures.length; - let auth = 0, cond = 0, other = 0; + let auth = 0, cond = 0, notEval = 0, restricted = 0; teFeatures.forEach(f => { const st = f.properties.passage_status; if (st === 'authorized') auth++; else if (st === 'conditional') cond++; - else other++; + else if (st === 'not_evaluated' || !f.properties.has_te_info) notEval++; + else restricted++; }); const elTotal = document.getElementById('te-total-count'); @@ -235,8 +235,10 @@ if (elAuth) elAuth.textContent = auth; const elCond = document.getElementById('te-count-conditional'); if (elCond) elCond.textContent = cond; - const elOther = document.getElementById('te-count-other'); - if (elOther) elOther.textContent = other; + const elNotEval = document.getElementById('te-count-not-evaluated'); + if (elNotEval) elNotEval.textContent = notEval; + const elRestricted = document.getElementById('te-count-restricted'); + if (elRestricted) elRestricted.textContent = restricted; } function renderMapLayers() { @@ -291,7 +293,11 @@ ], 'circle-stroke-width': 2, 'circle-stroke-color': '#ffffff', - 'circle-opacity': 0.95 + 'circle-opacity': [ + 'case', + ['==', ['get', 'passage_status'], 'not_evaluated'], 0.7, + 0.95 + ] } }); @@ -352,63 +358,88 @@ } function buildSingleStructurePopupHtml(p) { + const hasTe = p.has_te_info && p.passage_status !== 'not_evaluated'; const statusBadge = getStatusBadgeHtml(p.passage_status, p.passage_status_display); - let metricsHtml = '
'; - metricsHtml += ` -
-
-
Poids max autorisé
-
${p.max_authorized_weight_tonnes ? p.max_authorized_weight_tonnes + ' t' : (p.max_calculated_weight_tonnes ? p.max_calculated_weight_tonnes + ' t (calc)' : '-')}
-
-
-
-
-
Classe de pont (BKL)
-
${p.bridge_class || '-'}
-
-
- `; - if (p.usable_width_m || p.clearance_heights_str) { + let typeBadge = p.structure_type ? `${p.structure_type}` : ''; + let bodyContent = ''; + + if (hasTe) { + let metricsHtml = '
'; metricsHtml += `
-
Largeur utile
-
${p.usable_width_m ? p.usable_width_m + ' m' : '-'}
+
Poids max autorisé
+
${p.max_authorized_weight_tonnes ? p.max_authorized_weight_tonnes + ' t' : (p.max_calculated_weight_tonnes ? p.max_calculated_weight_tonnes + ' t (calc)' : '-')}
-
Hauteurs (G/C/D)
-
${p.clearance_heights_str || '-'}
+
Classe de pont (BKL)
+
${p.bridge_class || '-'}
`; - } - metricsHtml += '
'; + if (p.usable_width_m || p.clearance_heights_str) { + metricsHtml += ` +
+
+
Largeur utile
+
${p.usable_width_m ? p.usable_width_m + ' m' : '-'}
+
+
+
+
+
Hauteurs (G/C/D)
+
${p.clearance_heights_str || '-'}
+
+
+ `; + } + metricsHtml += '
'; - // Restrictions badges - let restrictionsBadges = ''; - if (p.max_speed_kmh) { - restrictionsBadges += `Max ${p.max_speed_kmh} km/h`; - } - if (p.is_braking_forbidden) { - restrictionsBadges += `Freinage interdit`; - } - if (p.is_isolated_crossing_only) { - restrictionsBadges += `Convoi seul`; - } + // Restrictions badges + let restrictionsBadges = ''; + if (p.max_speed_kmh) { + restrictionsBadges += `Max ${p.max_speed_kmh} km/h`; + } + if (p.is_braking_forbidden) { + restrictionsBadges += `Freinage interdit`; + } + if (p.is_isolated_crossing_only) { + restrictionsBadges += `Convoi seul`; + } - // Prescriptions excerpt - let prescText = p.prescriptions_fr || p.prescriptions_nl || ''; - let prescHtml = ''; - if (prescText) { - let shortPresc = prescText.length > 130 ? prescText.substring(0, 130) + '...' : prescText; - prescHtml = ` -
-
Prescriptions :
- ${shortPresc} + // Prescriptions excerpt + let prescText = p.prescriptions_fr || p.prescriptions_nl || ''; + let prescHtml = ''; + if (prescText) { + let shortPresc = prescText.length > 130 ? prescText.substring(0, 130) + '...' : prescText; + prescHtml = ` +
+
Prescriptions :
+ ${shortPresc} +
+ `; + } + + bodyContent = ` + ${metricsHtml} + ${restrictionsBadges ? `
${restrictionsBadges}
` : ''} + ${prescHtml} + `; + } else { + // Not evaluated yet + let overpassNote = (p.structure_type_code === 'overpass') + ? `
Passerelle / Ouvrage piétonnier : gabarit de hauteur libre à renseigner.
` + : ''; + + bodyContent = ` +
+
Non évalué pour le transport exceptionnel
+
Cet ouvrage n'a pas encore de fiche technique TE enregistrée.
+ ${overpassNote} `; } @@ -416,7 +447,10 @@
- ${p.structure_code} +
+ ${p.structure_code} + ${typeBadge} +
${p.fod_vnr ? `FOD n°${p.fod_vnr}` : ''}
@@ -425,9 +459,7 @@
${statusBadge}
- ${metricsHtml} - ${restrictionsBadges ? `
${restrictionsBadges}
` : ''} - ${prescHtml} + ${bodyContent}
{% translate "Ouvrir la fiche de l'ouvrage" %} @@ -441,15 +473,19 @@ features.forEach((f, idx) => { const p = f.properties; const statusBadge = getStatusBadgeHtml(p.passage_status, p.passage_status_display); + let typeBadge = p.structure_type ? `${p.structure_type}` : ''; itemsHtml += `
- ${p.structure_code} +
+ ${p.structure_code} + ${typeBadge} +
${statusBadge}
${p.name || p.name_fr}
- Poids: ${p.max_authorized_weight_tonnes ? p.max_authorized_weight_tonnes + 't' : '-'} + ${p.max_authorized_weight_tonnes ? 'Poids: ' + p.max_authorized_weight_tonnes + 't' : 'Non évalué'} {% translate "Détail" %} @@ -487,6 +523,9 @@ } else if (status === 'forbidden') { colorClass = 'bg-danger'; icon = 'bi-slash-circle-fill'; + } else if (status === 'not_evaluated') { + colorClass = 'bg-secondary bg-opacity-75 text-white'; + icon = 'bi-clock-history'; } return `${display || status}`; } @@ -512,21 +551,24 @@ // Filter features const matches = teFeatures.filter(f => { + const p = f.properties; + const st = p.passage_status; + if (activeFilter !== 'all') { - const st = f.properties.passage_status; if (activeFilter === 'authorized' && st !== 'authorized') return false; if (activeFilter === 'conditional' && st !== 'conditional') return false; - if (activeFilter === 'other' && (st === 'authorized' || st === 'conditional')) return false; + if (activeFilter === 'not_evaluated' && st !== 'not_evaluated') return false; + if (activeFilter === 'restricted' && (st === 'authorized' || st === 'conditional' || st === 'not_evaluated')) return false; } - const p = f.properties; const code = (p.structure_code || '').toLowerCase(); const nameFr = (p.name_fr || '').toLowerCase(); const nameNl = (p.name_nl || '').toLowerCase(); const vnr = p.fod_vnr ? String(p.fod_vnr) : ''; const bkl = (p.bridge_class || '').toLowerCase(); + const typeStr = (p.structure_type || '').toLowerCase(); - return code.includes(query) || nameFr.includes(query) || nameNl.includes(query) || vnr.includes(query) || bkl.includes(query); + return code.includes(query) || nameFr.includes(query) || nameNl.includes(query) || vnr.includes(query) || bkl.includes(query) || typeStr.includes(query); }).slice(0, 8); if (matches.length === 0) { @@ -539,16 +581,20 @@ matches.forEach(f => { const p = f.properties; const statusBadge = getStatusBadgeHtml(p.passage_status, p.passage_status_display); + let typeBadge = p.structure_type ? `${p.structure_type}` : ''; html += ` `; @@ -608,10 +654,12 @@ filterExpr = ['==', ['get', 'passage_status'], 'authorized']; } else if (activeFilter === 'conditional') { filterExpr = ['==', ['get', 'passage_status'], 'conditional']; - } else if (activeFilter === 'other') { - filterExpr = ['all', - ['!=', ['get', 'passage_status'], 'authorized'], - ['!=', ['get', 'passage_status'], 'conditional'] + } else if (activeFilter === 'not_evaluated') { + filterExpr = ['==', ['get', 'passage_status'], 'not_evaluated']; + } else if (activeFilter === 'restricted') { + filterExpr = ['any', + ['==', ['get', 'passage_status'], 'forbidden'], + ['==', ['get', 'passage_status'], 'prior_study_required'] ]; }