feat: implement GIEP inspection question framework with localized labels and template filters

This commit is contained in:
kdeterme 2026-08-11 15:47:17 +02:00
parent e4d2ca45c5
commit 41aa78e382
19 changed files with 1405 additions and 62 deletions

View file

@ -0,0 +1,29 @@
# Generated by Django 6.0.7 on 2026-08-11 12:13
import django.db.models.deletion
from django.db import migrations, models
def populate_locations(apps, schema_editor):
NatureRWIZ = apps.get_model('assets', 'NatureRWIZ')
NatureRWIAsset = apps.get_model('assets', 'NatureRWIAsset')
zones = NatureRWIZ.objects.filter(geom__isnull=False)
for zone in zones:
NatureRWIAsset.objects.filter(geom__intersects=zone.geom, location__isnull=True).update(location=zone)
class Migration(migrations.Migration):
dependencies = [
('assets', '0018_remove_parkingspot_scooter_capacity'),
]
operations = [
migrations.AddField(
model_name='naturerwiasset',
name='location',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.naturerwiz', verbose_name='Zone GIEP / ZIEP'),
),
migrations.RunPython(populate_locations, reverse_code=migrations.RunPython.noop),
]

View file

@ -277,14 +277,10 @@ class NatureRWIZ(AbstractLocation):
geom = models.MultiPolygonField(srid=3812, null=True,blank=True) geom = models.MultiPolygonField(srid=3812, null=True,blank=True)
def get_active_assets(self): def get_active_assets(self):
if not self.geom: return NatureRWIAsset.objects.filter(location=self, status='active')
return NatureRWIAsset.objects.none()
return NatureRWIAsset.objects.filter(geom__intersects=self.geom, status='active')
def get_all_assets(self): def get_all_assets(self):
if not self.geom: return NatureRWIAsset.objects.filter(location=self)
return NatureRWIAsset.objects.none()
return NatureRWIAsset.objects.filter(geom__intersects=self.geom)
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse(
@ -451,6 +447,7 @@ GEOMETRY_TYPE_CHOICES = [
class NatureRWIAsset(AbstractGeoAsset): class NatureRWIAsset(AbstractGeoAsset):
location = models.ForeignKey(NatureRWIZ, on_delete=models.SET_NULL, null=True, blank=True, related_name="assets", verbose_name=_("Zone GIEP / ZIEP"))
geom = models.GeometryField(srid=3812, null=True, blank=True) geom = models.GeometryField(srid=3812, null=True, blank=True)
external_id = models.IntegerField(null=True, blank=True, verbose_name=_("External ID")) external_id = models.IntegerField(null=True, blank=True, verbose_name=_("External ID"))
@ -485,6 +482,16 @@ class NatureRWIAsset(AbstractGeoAsset):
verbose_name = _("RWI Asset") verbose_name = _("RWI Asset")
verbose_name_plural = _("RWI Assets") verbose_name_plural = _("RWI Assets")
def save(self, *args, **kwargs):
if not self.location_id and self.geom:
try:
matching_rwiz = NatureRWIZ.objects.filter(geom__intersects=self.geom).first()
if matching_rwiz:
self.location = matching_rwiz
except Exception:
pass
super().save(*args, **kwargs)
def get_absolute_url(self): def get_absolute_url(self):
return reverse('assets:water_assets_detail', kwargs={'asset_model': 'naturerwiasset', 'asset_id': self.id}) return reverse('assets:water_assets_detail', kwargs={'asset_model': 'naturerwiasset', 'asset_id': self.id})

View file

@ -298,6 +298,7 @@ def _get_asset_thematic(asset):
# Fallback: deduce thematic from asset class name # Fallback: deduce thematic from asset class name
class_name = asset.__class__.__name__.lower() class_name = asset.__class__.__name__.lower()
thematic_mappings = { thematic_mappings = {
'naturerwiz': 'water',
'naturerwiasset': 'water', 'naturerwiasset': 'water',
'trafficlight': 'trafficlights', 'trafficlight': 'trafficlights',
'publiclighting': 'publiclighting', 'publiclighting': 'publiclighting',

View file

@ -18,8 +18,8 @@
data-object-id="{{ asset_pk }}" data-object-id="{{ asset_pk }}"
data-target-code="{{ asset.code|default:asset.pk }}" data-target-code="{{ asset.code|default:asset.pk }}"
data-target-name="{{ asset.name_fr|default:asset.name_nl|default:asset }}" data-target-name="{{ asset.name_fr|default:asset.name_nl|default:asset }}"
data-target-type="{{ asset.category.name_fr|default:'Asset' }}" data-target-type="{% if asset.category %}{{ asset.category.name_fr }}{% else %}{% translate 'Zone / Équipement' %}{% endif %}"
data-target-status="{{ asset.get_status_display|default:asset.inspection_status|default:'-' }}"> data-target-status="{% if asset.get_status_display %}{{ asset.get_status_display }}{% else %}-{% endif %}">
<i class="bi bi-plus-lg me-1"></i>{% translate "Nouvelle inspection" %} <i class="bi bi-plus-lg me-1"></i>{% translate "Nouvelle inspection" %}
</button> </button>
{% endif %} {% endif %}
@ -86,7 +86,7 @@
<div class="modal-content border-0 shadow"> <div class="modal-content border-0 shadow">
<div class="modal-header bg-light py-2.5"> <div class="modal-header bg-light py-2.5">
<h5 class="modal-title fs-6 fw-bold" id="assetInspectionDetailModalLabel{{ insp.id }}"> <h5 class="modal-title fs-6 fw-bold" id="assetInspectionDetailModalLabel{{ insp.id }}">
<i class="bi bi-patch-check me-2 text-primary"></i>{% translate "Détail du contrôle" %} - {{ insp.inspection_date|date:"d/m/Y H:i" }} <i class="bi bi-patch-check me-2 text-primary"></i>{% translate "Détail de l'inspection" %} - {{ insp.inspection_date|date:"d/m/Y H:i" }}
</h5> </h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
@ -181,12 +181,60 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
const form = document.getElementById('quickInspectionForm'); const form = document.getElementById('quickInspectionForm');
if (form && !form.dataset.reloadHandlerAttached) { if (form && !form.dataset.ajaxHandlerAttached) {
form.dataset.reloadHandlerAttached = 'true'; form.dataset.ajaxHandlerAttached = 'true';
form.addEventListener('submit', function() { form.addEventListener('submit', function(e) {
setTimeout(function() { e.preventDefault();
const submitBtn = document.getElementById('submitQuickInspectionBtn');
const spinner = document.getElementById('submitQuickInspectionSpinner');
const icon = document.getElementById('submitQuickInspectionIcon');
const alertBox = document.getElementById('quickInspectionAlert');
if (alertBox) {
alertBox.classList.add('d-none');
alertBox.textContent = '';
}
if (submitBtn) submitBtn.disabled = true;
if (spinner) spinner.classList.remove('d-none');
if (icon) icon.classList.add('d-none');
const formData = new FormData(form);
fetch(form.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(res => {
if (res.redirected) {
window.location.href = res.url;
return null;
}
return res.json();
})
.then(data => {
if (!data) return;
if (data.success) {
const modalEl = document.getElementById('quickInspectionModal');
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) modal.hide();
window.location.reload();
} else {
if (alertBox) {
alertBox.textContent = data.error || "{% filter escapejs %}{% translate 'Erreur lors de l\'enregistrement.' %}{% endfilter %}";
alertBox.classList.remove('d-none');
}
if (submitBtn) submitBtn.disabled = false;
if (spinner) spinner.classList.add('d-none');
if (icon) icon.classList.remove('d-none');
}
})
.catch(err => {
console.error("Erreur lors de l'enregistrement de l'inspection:", err);
window.location.reload(); window.location.reload();
}, 600); });
}); });
} }
}); });

View file

@ -1,7 +1,7 @@
{% load i18n %} {% load i18n %}
<!-- Modale de Saisie Rapide d'Inspection --> <!-- Modale de Saisie Rapide d'Inspection -->
<div class="modal fade" id="quickInspectionModal" tabindex="-1" aria-labelledby="quickInspectionModalLabel" aria-hidden="true"> <div class="modal fade" id="quickInspectionModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="quickInspectionModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered"> <div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content shadow"> <div class="modal-content shadow">
<div class="modal-header bg-light"> <div class="modal-header bg-light">
@ -144,7 +144,7 @@
</div> </div>
<!-- Modal caméra Canvas (mode continu pour inspection) --> <!-- Modal caméra Canvas (mode continu pour inspection) -->
<div class="modal fade" id="inspCameraModal" tabindex="-1" aria-labelledby="inspCameraModalLabel" aria-hidden="true" style="z-index: 1060;"> <div class="modal fade" id="inspCameraModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="inspCameraModalLabel" aria-hidden="true" style="z-index: 1060;">
<div class="modal-dialog modal-fullscreen-sm-down d-flex m-0 h-100 mx-auto"> <div class="modal-dialog modal-fullscreen-sm-down d-flex m-0 h-100 mx-auto">
<div class="modal-content bg-dark text-white d-flex flex-column"> <div class="modal-content bg-dark text-white d-flex flex-column">
<div class="modal-header border-0 flex-shrink-0 py-2"> <div class="modal-header border-0 flex-shrink-0 py-2">
@ -176,7 +176,7 @@
</div> </div>
<!-- Modale de Choix d'Asset (Zone dense / Sélection multiple) --> <!-- Modale de Choix d'Asset (Zone dense / Sélection multiple) -->
<div class="modal fade" id="targetChooserModal" tabindex="-1" aria-labelledby="targetChooserModalLabel" aria-hidden="true"> <div class="modal fade" id="targetChooserModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="targetChooserModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content shadow"> <div class="modal-content shadow">
<div class="modal-header bg-light"> <div class="modal-header bg-light">

View file

@ -3,6 +3,7 @@
{% load i18n %} {% load i18n %}
{% load documents_tags %} {% load documents_tags %}
{% load document_viewer_tags %} {% load document_viewer_tags %}
{% load asset_tags %}
{% block content %} {% block content %}
<div class="row mt-2" style="height: calc(100vh - 200px);"> <div class="row mt-2" style="height: calc(100vh - 200px);">
@ -224,6 +225,9 @@
</div> </div>
</div> </div>
<!-- Inspections & Contrôles Terrain GIEP -->
{% asset_inspections location %}
<!-- Interventions --> <!-- Interventions -->
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <div class="card-header">

View file

@ -169,13 +169,13 @@ def asset_location_link(context, asset):
location = None location = None
location_type = None location_type = None
if hasattr(asset, 'street'): if hasattr(asset, 'street') and getattr(asset, 'street', None):
location = getattr(asset, 'street', None) location = getattr(asset, 'street', None)
location_type = 'street' location_type = 'street'
elif hasattr(asset, 'location'): elif hasattr(asset, 'location') and getattr(asset, 'location', None):
location = getattr(asset, 'location', None) location = getattr(asset, 'location', None)
location_type = 'location' location_type = 'location'
elif hasattr(asset, 'intersection'): elif hasattr(asset, 'intersection') and getattr(asset, 'intersection', None):
location = getattr(asset, 'intersection', None) location = getattr(asset, 'intersection', None)
location_type = 'intersection' location_type = 'intersection'

View file

@ -192,15 +192,7 @@ def water_assets_geojson(request):
naturerwiasset_qs = NatureRWIAsset.objects.all() naturerwiasset_qs = NatureRWIAsset.objects.all()
if sections_ids: if sections_ids:
# Filter by intersecting NatureRWIZ geometries naturerwiasset_qs = naturerwiasset_qs.filter(location_id__in=sections_ids)
rwiz_geoms = NatureRWIZ.objects.filter(id__in=sections_ids, geom__isnull=False).values_list('geom', flat=True)
if rwiz_geoms:
query = Q()
for g in rwiz_geoms:
query |= Q(geom__intersects=g)
naturerwiasset_qs = naturerwiasset_qs.filter(query)
else:
naturerwiasset_qs = naturerwiasset_qs.none()
if categories: if categories:
naturerwiasset_qs = naturerwiasset_qs.filter(category__id__in=categories) naturerwiasset_qs = naturerwiasset_qs.filter(category__id__in=categories)

View file

@ -104,3 +104,304 @@ msgid "%(counter)s inspection trouvée"
msgid_plural "%(counter)s inspections trouvées" msgid_plural "%(counter)s inspections trouvées"
msgstr[0] "%(counter)s inspection trouvée" msgstr[0] "%(counter)s inspection trouvée"
msgstr[1] "%(counter)s inspections trouvées" msgstr[1] "%(counter)s inspections trouvées"
# --- GIEP Translations ---
msgid "Formulaire d'inspection GIEP"
msgstr "Formulaire d'inspection GIEP"
# --- GIEP Translations ---
msgid "Eaux Pluviales"
msgstr "Eaux Pluviales"
msgid "Conditions météorologiques"
msgstr "Conditions météorologiques"
msgid "Date de la dernière pluie"
msgstr "Date de la dernière pluie"
msgid "Orage récent (-1h)"
msgstr "Orage récent (-1h)"
msgid "Ouvrages inspectés"
msgstr "Ouvrages inspectés"
msgid "Ouvrages GIEP inspectés"
msgstr "Ouvrages GIEP inspectés"
msgid "Ajouter un ouvrage"
msgstr "Ajouter un ouvrage"
msgid "Ouvrage"
msgstr "Ouvrage"
msgid "Supprimer"
msgstr "Supprimer"
msgid "Non renseigné"
msgstr "Non renseigné"
msgid "Non vérifié"
msgstr "Non vérifié"
msgid "Oui"
msgstr "Oui"
msgid "Non"
msgstr "Non"
msgid "Aujourd'hui"
msgstr "Aujourd'hui"
msgid "Moins de 24h"
msgstr "Moins de 24h"
msgid "Plus de 24h"
msgstr "Plus de 24h"
msgid "Espace vert creux (EVC)"
msgstr "Espace vert creux (EVC)"
msgid "Massif drainant (OED)"
msgstr "Massif drainant (OED)"
msgid "Technique alternative (TA)"
msgstr "Technique alternative (TA)"
msgid "Technique Alternative / Bassin d'orage"
msgstr "Technique Alternative / Bassin d'orage"
msgid "Zone GIEP principale"
msgstr "Zone GIEP principale"
msgid "Synthèse des contrôles"
msgstr "Synthèse des contrôles"
msgid "Point d'entrée dans l'ouvrage"
msgstr "Point d'entrée dans l'ouvrage"
msgid "L'eau peut-elle facilement entrer dans l'EVC ?"
msgstr "L'eau peut-elle facilement entrer dans l'EVC ?"
msgid "L'eau peut-elle facilement entrer dans l'ouvrage ?"
msgstr "L'eau peut-elle facilement entrer dans l'ouvrage ?"
msgid "Des feuilles ou branches bloquent-elles l'eau ?"
msgstr "Des feuilles ou branches bloquent-elles l'eau ?"
msgid "Un amas de détritus bloque-t-il l'eau ?"
msgstr "Un amas de détritus bloque-t-il l'eau ?"
msgid "L'ouvrage dispose-t-il d'un empierrement ?"
msgstr "L'ouvrage dispose-t-il d'un empierrement ?"
msgid "Activer pour compléter l'état de l'empierrement"
msgstr "Activer pour compléter l'état de l'empierrement"
msgid "L'empierrement est-il en bon état ?"
msgstr "L'empierrement est-il en bon état ?"
msgid "Colmaté par les sédiments"
msgstr "Colmaté par les sédiments"
msgid "Emporté par l'eau"
msgstr "Emporté par l'eau"
msgid "Envahi d'adventices"
msgstr "Envahi d'adventices"
msgid "Espace vert"
msgstr "Espace vert"
msgid "Quel est l'état de la végétation ?"
msgstr "Quel est l'état de la végétation ?"
msgid "Bon"
msgstr "Bon"
msgid "Mauvais"
msgstr "Mauvais"
msgid "Présence de plantes mortes ou absentes :"
msgstr "Présence de plantes mortes ou absentes :"
msgid "+ de 10%"
msgstr "+ de 10%"
msgid "- de 10%"
msgstr "- de 10%"
msgid "Sur + de 10% de la surface"
msgstr "Sur + de 10% de la surface"
msgid "Sur - de 10% de la surface"
msgstr "Sur - de 10% de la surface"
msgid "Présence de chenaux d'érosion ?"
msgstr "Présence de chenaux d'érosion ?"
msgid "Présence de flaques d'eau persistantes ?"
msgstr "Présence de flaques d'eau persistantes ?"
msgid "Présence de sédiments dans l'ouvrage ?"
msgstr "Présence de sédiments dans l'ouvrage ?"
msgid "La croûte de sédimentation est-elle supérieure à 5 cm ?"
msgstr "La croûte de sédimentation est-elle supérieure à 5 cm ?"
msgid "Non (inférieure à 5 cm)"
msgstr "Non (inférieure à 5 cm)"
msgid "Oui (supérieure à 5 cm)"
msgstr "Oui (supérieure à 5 cm)"
msgid "Présence d'espèces exotiques envahissantes ?"
msgstr "Présence d'espèces exotiques envahissantes ?"
msgid "Une surverse est-elle présente ?"
msgstr "Une surverse est-elle présente ?"
msgid "Activer si le système dispose d'une évacuation de trop-plein"
msgstr "Activer si le système dispose d'une évacuation de trop-plein"
msgid "La grille de surverse est propre et dégagée ?"
msgstr "La grille de surverse est propre et dégagée ?"
msgid "La décantation est vide ou peu remplie ?"
msgstr "La décantation est vide ou peu remplie ?"
msgid "La surverse est-elle plus haute que le fond de l'ouvrage ?"
msgstr "La surverse est-elle plus haute que le fond de l'ouvrage ?"
msgid "État général du revêtement / chaussée"
msgstr "État général du revêtement / chaussée"
msgid "La surface au-dessus du massif drainant est en bon état ?"
msgstr "La surface au-dessus du massif drainant est en bon état ?"
msgid "Oui (Bon état)"
msgstr "Oui (Bon état)"
msgid "Non (Défaillances constatées)"
msgstr "Non (Défaillances constatées)"
msgid "Présence d'ornières"
msgstr "Présence d'ornières"
msgid "Présence de fissures"
msgstr "Présence de fissures"
msgid "Présence de trous"
msgstr "Présence de trous"
msgid "Une bouche d'injection est-elle présente ?"
msgstr "Une bouche d'injection est-elle présente ?"
msgid "Activer pour inspecter les conduits d'injection d'eau et de drainage"
msgstr "Activer pour inspecter les conduits d'injection d'eau et de drainage"
msgid "Les niveaux du revêtement conduisent bien l'eau vers la bouche ?"
msgstr "Les niveaux du revêtement conduisent bien l'eau vers la bouche ?"
msgid "La grille est propre et dégagée ?"
msgstr "La grille est propre et dégagée ?"
msgid "La décantation est-elle vide ou presque ?"
msgstr "La décantation est-elle vide ou presque ?"
msgid "La bouche d'injection dispose d'un filtre ?"
msgstr "La bouche d'injection dispose d'un filtre ?"
msgid "Le filtre est propre ?"
msgstr "Le filtre est propre ?"
msgid "Le filtre est en bon état ?"
msgstr "Le filtre est en bon état ?"
msgid "Le drain semble colmaté ?"
msgstr "Le drain semble colmaté ?"
msgid "Oui (colmaté)"
msgstr "Oui (colmaté)"
msgid "Type de structure & Revêtement OED"
msgstr "Type de structure & Revêtement OED"
msgid "Structure enherbée ou remplissage minéral ?"
msgstr "Structure enherbée ou remplissage minéral ?"
msgid "Dalles gazon, pavés à joints larges, dalles alvéolaires végétalisées/gravier, etc."
msgstr "Dalles gazon, pavés à joints larges, dalles alvéolaires végétalisées/gravier, etc."
msgid "De l'eau stagne-t-elle en surface ?"
msgstr "De l'eau stagne-t-elle en surface ?"
msgid "L'eau stagne plus de 2 heures ?"
msgstr "L'eau stagne plus de 2 heures ?"
msgid "Les joints sont-ils en bon état ?"
msgstr "Les joints sont-ils en bon état ?"
msgid "Présence importante de fines dans les joints ?"
msgstr "Présence importante de fines dans les joints ?"
msgid "Niveau de remplissage plus haut que la structure ?"
msgstr "Niveau de remplissage plus haut que la structure ?"
msgid "Modulaire poreux ou continu poreux ?"
msgstr "Modulaire poreux ou continu poreux ?"
msgid "Béton poreux, enrobés bitumineux poreux, graviers liés à la résine, EPDM, etc."
msgstr "Béton poreux, enrobés bitumineux poreux, graviers liés à la résine, EPDM, etc."
msgid "Présence de dépôts visibles (poussières, mousses, etc.) ?"
msgstr "Présence de dépôts visibles (poussières, mousses, etc.) ?"
msgid "Test de perméabilité nécessaire ?"
msgstr "Test de perméabilité nécessaire ?"
msgid "Matériaux non agrégés (graviers/mulch) ?"
msgstr "Matériaux non agrégés (graviers/mulch) ?"
msgid "Mulch végétal, graviers calcaires/non calcaires, terre battue, etc."
msgstr "Mulch végétal, graviers calcaires/non calcaires, terre battue, etc."
msgid "Le volume de matière est-il en suffisance ?"
msgstr "Le volume de matière est-il en suffisance ?"
msgid "Le géotextile est-il visible ?"
msgstr "Le géotextile est-il visible ?"
msgid "Les conduits et dispositifs amenant l'eau sont-ils fonctionnels ?"
msgstr "Les conduits et dispositifs amenant l'eau sont-ils fonctionnels ?"
msgid "Les grilles sont-elles propres et dégagées ?"
msgstr "Les grilles sont-elles propres et dégagées ?"
msgid "Présence de sédiments dans le bassin d'orage ?"
msgstr "Présence de sédiments dans le bassin d'orage ?"
msgid "L'ouvrage est-il rempli au 5e de sa capacité ?"
msgstr "L'ouvrage est-il rempli au 5e de sa capacité ?"
msgid "Présence de détritus dans l'ouvrage ?"
msgstr "Présence de détritus dans l'ouvrage ?"
msgid "Recommandations"
msgstr "Recommandations"
msgid "Aucune"
msgstr "Aucune"
msgid "Interventions préventives"
msgstr "Interventions préventives"
msgid "Entretien curatif / Réparation / Adaptation nécessaire"
msgstr "Entretien curatif / Réparation / Adaptation nécessaire"
msgid "Test de perméabilité"
msgstr "Test de perméabilité"
msgid "Inspection"
msgstr "Inspection"

View file

@ -158,8 +158,11 @@ class Inspection(models.Model):
modified = True modified = True
if hasattr(target_asset, 'inspection_status'): if hasattr(target_asset, 'inspection_status'):
target_asset.inspection_status = self.result_status try:
modified = True setattr(target_asset, 'inspection_status', self.result_status)
modified = True
except AttributeError:
pass
if hasattr(target_asset, 'validation_status'): if hasattr(target_asset, 'validation_status'):
target_asset.validation_status = 'validated' target_asset.validation_status = 'validated'

View file

@ -17,7 +17,7 @@ def get_inspection_partial_template(model_or_name):
else: else:
model_name = str(model_or_name).lower() model_name = str(model_or_name).lower()
if model_name in ('naturerwiz', 'naturerwiasset'): if model_name == 'naturerwiz':
model_name = 'naturerwiz' model_name = 'naturerwiz'
template_path = f"inspections/components/asset_fields/_{model_name}.html" template_path = f"inspections/components/asset_fields/_{model_name}.html"
@ -45,7 +45,7 @@ def get_inspection_detail_partial_template(insp_or_model_or_name):
else: else:
model_name = str(insp_or_model_or_name).lower() model_name = str(insp_or_model_or_name).lower()
if model_name in ('naturerwiz', 'naturerwiasset', 'giepinspection'): if model_name in ('naturerwiz', 'giepinspection'):
model_name = 'naturerwiz' model_name = 'naturerwiz'
elif model_name == 'parkingspotinspection': elif model_name == 'parkingspotinspection':
model_name = 'parkingspot' model_name = 'parkingspot'

View file

@ -1,4 +1,4 @@
{% load i18n static %} {% load i18n static inspections_tags %}
{% with leaf=insp.detailed_inspection %} {% with leaf=insp.detailed_inspection %}
{% if leaf %} {% if leaf %}
@ -10,21 +10,19 @@
<i class="bi bi-cloud-sun-fill me-2"></i>{% translate "Conditions météorologiques" %} <i class="bi bi-cloud-sun-fill me-2"></i>{% translate "Conditions météorologiques" %}
</h6> </h6>
<div class="row g-2"> <div class="row g-2">
{% if leaf.date_derniere_pluie and leaf.date_derniere_pluie != 'null' %}
<div class="col-12 col-md-6 d-flex align-items-center justify-content-between"> <div class="col-12 col-md-6 d-flex align-items-center justify-content-between">
<span class="small text-muted"><i class="bi bi-cloud-rain me-1 text-primary"></i>{% translate "Date de la dernière pluie" %} :</span> <span class="small text-muted"><i class="bi bi-cloud-rain me-1 text-primary"></i>{% translate "Date de la dernière pluie" %} :</span>
<span class="badge bg-info text-white px-2 py-1">{{ leaf.get_date_derniere_pluie_display }}</span> <span class="badge {% if leaf.date_derniere_pluie and leaf.date_derniere_pluie != 'null' %}bg-info text-white{% else %}bg-light text-muted border{% endif %} px-2 py-1">
</div> {% if leaf.date_derniere_pluie and leaf.date_derniere_pluie != 'null' %}{{ leaf.get_date_derniere_pluie_display }}{% else %}{% translate "Non renseignée" %}{% endif %}
{% endif %} </span>
</div>
{% if leaf.orage_moins_une_heure is not None %}
<div class="col-12 col-md-6 d-flex align-items-center justify-content-between"> <div class="col-12 col-md-6 d-flex align-items-center justify-content-between">
<span class="small text-muted"><i class="bi bi-lightning-charge me-1 text-danger"></i>{% translate "Orage récent (-1h)" %} :</span> <span class="small text-muted"><i class="bi bi-lightning-charge me-1 text-danger"></i>{% translate "Orage récent (-1h)" %} :</span>
<span class="badge {% if leaf.orage_moins_une_heure %}bg-danger{% else %}bg-success{% endif %} text-white px-2 py-1"> <span class="badge {% if leaf.orage_moins_une_heure is True %}bg-danger text-white{% elif leaf.orage_moins_une_heure is False %}bg-success text-white{% else %}bg-light text-muted border{% endif %} px-2 py-1">
{% if leaf.orage_moins_une_heure %}{% translate "Oui" %}{% else %}{% translate "Non" %}{% endif %} {% if leaf.orage_moins_une_heure is True %}{% translate "Oui" %}{% elif leaf.orage_moins_une_heure is False %}{% translate "Non" %}{% else %}{% translate "Non vérifié" %}{% endif %}
</span> </span>
</div> </div>
{% endif %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
@ -38,9 +36,11 @@
<div class="d-flex flex-column gap-3"> <div class="d-flex flex-column gap-3">
{% for ouv in leaf.data.ouvrages %} {% for ouv in leaf.data.ouvrages %}
{% with q=ouv.questions %}
<div class="card border shadow-sm rounded-3 giep-ouvrage-card overflow-hidden" <div class="card border shadow-sm rounded-3 giep-ouvrage-card overflow-hidden"
style="{% if ouv.device_type == 'evc' %}border-left: 5px solid #2e7d32;{% elif ouv.device_type == 'oed' %}border-left: 5px solid #0277bd;{% elif ouv.device_type == 'ta' %}border-left: 5px solid #00796b;{% else %}border-left: 5px solid #6c757d;{% endif %}"> style="{% if ouv.device_type == 'evc' %}border-left: 5px solid #2e7d32;{% elif ouv.device_type == 'oed' %}border-left: 5px solid #0277bd;{% elif ouv.device_type == 'ta' %}border-left: 5px solid #00796b;{% else %}border-left: 5px solid #6c757d;{% endif %}">
<!-- En-tête de l'ouvrage -->
<div class="card-header bg-white p-3 d-flex align-items-center justify-content-between flex-wrap gap-2"> <div class="card-header bg-white p-3 d-flex align-items-center justify-content-between flex-wrap gap-2">
<div class="d-flex align-items-center gap-2 flex-wrap"> <div class="d-flex align-items-center gap-2 flex-wrap">
<span class="fw-bold text-dark fs-6">{% translate "Ouvrage" %} #{{ forloop.counter }}</span> <span class="fw-bold text-dark fs-6">{% translate "Ouvrage" %} #{{ forloop.counter }}</span>
@ -61,7 +61,15 @@
<span class="badge bg-secondary px-2 py-1">{% translate "GÉNÉRIQUE" %}</span> <span class="badge bg-secondary px-2 py-1">{% translate "GÉNÉRIQUE" %}</span>
{% endif %} {% endif %}
{% if ouv.name %} {% if ouv.asset_code %}
<span class="badge bg-primary-subtle text-primary border border-primary-subtle px-2 py-1">
<i class="bi bi-droplet-fill me-1"></i>{{ ouv.asset_code }}
</span>
{% endif %}
{% if ouv.asset_name and ouv.asset_name != ouv.asset_code and ouv.asset_name != "Ouvrage #"|add:forloop.counter %}
<span class="badge bg-light text-dark border px-2 py-1">{{ ouv.asset_name }}</span>
{% elif ouv.name and ouv.name != ouv.asset_code and ouv.name != "Ouvrage #"|add:forloop.counter %}
<span class="badge bg-light text-dark border px-2 py-1">{{ ouv.name }}</span> <span class="badge bg-light text-dark border px-2 py-1">{{ ouv.name }}</span>
{% endif %} {% endif %}
</div> </div>
@ -69,14 +77,252 @@
<div class="card-body p-3 bg-light-subtle"> <div class="card-body p-3 bg-light-subtle">
<div class="row g-3"> <div class="row g-3">
<!-- Recommandations -->
<!-- ── 1. EVC (Espace Vert Creux) BLOCS ── -->
{% if ouv.device_type == 'evc' %}
<!-- Bloc EVC: Point d'entrée -->
<div class="col-12">
<div class="border rounded-3 p-3 bg-white border-success-subtle shadow-sm">
<h6 class="fw-bold mb-3 small text-success text-uppercase border-bottom pb-1">
<i class="bi bi-box-arrow-in-right me-1"></i>{% translate "Point d'entrée dans l'ouvrage" %}
</h6>
{% with val=q|get_q_val:'evc_pe_ok' %}
<div class="d-flex align-items-center justify-content-between p-2 mb-1.5 rounded border {% if val == 'null' %}bg-light opacity-75{% else %}bg-white{% endif %}">
<span class="small fw-medium {% if val == 'null' %}text-muted{% else %}text-dark{% endif %} me-2">{% translate "L'eau peut-elle facilement entrer dans l'EVC ?" %}</span>
<span class="badge {% if val == 'true' or val == True or val == 'oui' %}bg-success-subtle text-success border border-success-subtle{% elif val == 'false' or val == False or val == 'non' %}bg-danger-subtle text-danger border border-danger-subtle{% else %}bg-light text-muted border{% endif %} px-2 py-1">
{% if val == 'true' or val == True or val == 'oui' %}Oui{% elif val == 'false' or val == False or val == 'non' %}Non{% else %}Non vérifié{% endif %}
</span>
</div>
{% endwith %}
{% with val=q|get_q_val:'evc_pe_feuilles' %}
<div class="d-flex align-items-center justify-content-between p-2 mb-1.5 rounded border {% if val == 'null' %}bg-light opacity-75{% else %}bg-white{% endif %}">
<span class="small fw-medium {% if val == 'null' %}text-muted{% else %}text-dark{% endif %} me-2">{% translate "Des feuilles ou branches bloquent-elles l'eau ?" %}</span>
<span class="badge {% if val == 'true' or val == True or val == 'oui' %}bg-danger-subtle text-danger border border-danger-subtle{% elif val == 'false' or val == False or val == 'non' %}bg-success-subtle text-success border border-success-subtle{% else %}bg-light text-muted border{% endif %} px-2 py-1">
{% if val == 'true' or val == True or val == 'oui' %}Oui{% elif val == 'false' or val == False or val == 'non' %}Non{% else %}Non vérifié{% endif %}
</span>
</div>
{% endwith %}
{% with val=q|get_q_val:'evc_pe_detritus' %}
<div class="d-flex align-items-center justify-content-between p-2 mb-1.5 rounded border {% if val == 'null' %}bg-light opacity-75{% else %}bg-white{% endif %}">
<span class="small fw-medium {% if val == 'null' %}text-muted{% else %}text-dark{% endif %} me-2">{% translate "Un amas de détritus bloque-t-il l'eau ?" %}</span>
<span class="badge {% if val == 'true' or val == True or val == 'oui' %}bg-danger-subtle text-danger border border-danger-subtle{% elif val == 'false' or val == False or val == 'non' %}bg-success-subtle text-success border border-success-subtle{% else %}bg-light text-muted border{% endif %} px-2 py-1">
{% if val == 'true' or val == True or val == 'oui' %}Oui{% elif val == 'false' or val == False or val == 'non' %}Non{% else %}Non vérifié{% endif %}
</span>
</div>
{% endwith %}
{% with val=q|get_q_val:'evc_empierrement_present' %}
<div class="d-flex align-items-center justify-content-between p-2 mb-1.5 rounded border {% if val == 'null' %}bg-light opacity-75{% else %}bg-white{% endif %}">
<span class="small fw-medium {% if val == 'null' %}text-muted{% else %}text-dark{% endif %} me-2">{% translate "L'ouvrage dispose-t-il d'un empierrement ?" %}</span>
<span class="badge {% if val == 'true' or val == True or val == 'oui' %}bg-success-subtle text-success border border-success-subtle{% elif val == 'false' or val == False or val == 'non' %}bg-secondary-subtle text-secondary border{% endif %} px-2 py-1">
{% if val == 'true' or val == True or val == 'oui' %}Oui{% elif val == 'false' or val == False or val == 'non' %}Non{% else %}Non vérifié{% endif %}
</span>
</div>
{% endwith %}
</div>
</div>
<!-- Bloc EVC: Espace Vert -->
<div class="col-12">
<div class="border rounded-3 p-3 bg-white border-success-subtle shadow-sm">
<h6 class="fw-bold mb-3 small text-success text-uppercase border-bottom pb-1">
<i class="bi bi-flower1 me-1"></i>{% translate "Espace vert" %}
</h6>
{% with val=q|get_q_val:'evc_ev_veg_etat' %}
<div class="d-flex align-items-center justify-content-between p-2 mb-1.5 rounded border {% if val == 'null' %}bg-light opacity-75{% else %}bg-white{% endif %}">
<span class="small fw-medium {% if val == 'null' %}text-muted{% else %}text-dark{% endif %} me-2">{% translate "Quel est l'état de la végétation ?" %}</span>
<span class="badge {% if val == 'bon' %}bg-success-subtle text-success border border-success-subtle{% elif val == 'mauvais' %}bg-danger-subtle text-danger border border-danger-subtle{% else %}bg-light text-muted border{% endif %} px-2 py-1">
{{ val|format_giep_q_val }}
</span>
</div>
{% endwith %}
{% with val=q|get_q_val:'evc_ev_chenaux_erosion' %}
<div class="d-flex align-items-center justify-content-between p-2 mb-1.5 rounded border {% if val == 'null' %}bg-light opacity-75{% else %}bg-white{% endif %}">
<span class="small fw-medium {% if val == 'null' %}text-muted{% else %}text-dark{% endif %} me-2">{% translate "Présence de chenaux d'érosion ?" %}</span>
<span class="badge {% if val == 'true' or val == True or val == 'oui' %}bg-danger-subtle text-danger border border-danger-subtle{% elif val == 'false' or val == False or val == 'non' %}bg-success-subtle text-success border border-success-subtle{% else %}bg-light text-muted border{% endif %} px-2 py-1">
{% if val == 'true' or val == True or val == 'oui' %}Oui{% elif val == 'false' or val == False or val == 'non' %}Non{% else %}Non vérifié{% endif %}
</span>
</div>
{% endwith %}
{% with val=q|get_q_val:'evc_ev_flaques' %}
<div class="d-flex align-items-center justify-content-between p-2 mb-1.5 rounded border {% if val == 'null' %}bg-light opacity-75{% else %}bg-white{% endif %}">
<span class="small fw-medium {% if val == 'null' %}text-muted{% else %}text-dark{% endif %} me-2">{% translate "Présence de flaques d'eau persistantes ?" %}</span>
<span class="badge {% if val == 'true' or val == True or val == 'oui' %}bg-danger-subtle text-danger border border-danger-subtle{% elif val == 'false' or val == False or val == 'non' %}bg-success-subtle text-success border border-success-subtle{% else %}bg-light text-muted border{% endif %} px-2 py-1">
{% if val == 'true' or val == True or val == 'oui' %}Oui{% elif val == 'false' or val == False or val == 'non' %}Non{% else %}Non vérifié{% endif %}
</span>
</div>
{% endwith %}
{% with val=q|get_q_val:'evc_ev_sediments' %}
<div class="d-flex align-items-center justify-content-between p-2 mb-1.5 rounded border {% if val == 'null' %}bg-light opacity-75{% else %}bg-white{% endif %}">
<span class="small fw-medium {% if val == 'null' %}text-muted{% else %}text-dark{% endif %} me-2">{% translate "Présence de sédiments dans l'ouvrage ?" %}</span>
<span class="badge {% if val == 'true' or val == True or val == 'oui' %}bg-danger-subtle text-danger border border-danger-subtle{% elif val == 'false' or val == False or val == 'non' %}bg-success-subtle text-success border border-success-subtle{% else %}bg-light text-muted border{% endif %} px-2 py-1">
{% if val == 'true' or val == True or val == 'oui' %}Oui{% elif val == 'false' or val == False or val == 'non' %}Non{% else %}Non vérifié{% endif %}
</span>
</div>
{% endwith %}
{% with val=q|get_q_val:'evc_ev_envahissantes' %}
<div class="d-flex align-items-center justify-content-between p-2 mb-1.5 rounded border {% if val == 'null' %}bg-light opacity-75{% else %}bg-white{% endif %}">
<span class="small fw-medium {% if val == 'null' %}text-muted{% else %}text-dark{% endif %} me-2">{% translate "Présence d'espèces exotiques envahissantes ?" %}</span>
<span class="badge {% if val == 'true' or val == True or val == 'oui' %}bg-danger-subtle text-danger border border-danger-subtle{% elif val == 'false' or val == False or val == 'non' %}bg-success-subtle text-success border border-success-subtle{% else %}bg-light text-muted border{% endif %} px-2 py-1">
{% if val == 'true' or val == True or val == 'oui' %}Oui{% elif val == 'false' or val == False or val == 'non' %}Non{% else %}Non vérifié{% endif %}
</span>
</div>
{% endwith %}
</div>
</div>
<!-- Bloc EVC: Surverse / Trop-plein -->
<div class="col-12">
<div class="border rounded-3 p-3 bg-white border-success-subtle shadow-sm">
<h6 class="fw-bold mb-3 small text-success text-uppercase border-bottom pb-1">
<i class="bi bi-water me-1"></i>{% translate "Surverse / Trop-plein" %}
</h6>
{% with val=q|get_q_val:'evc_surverse_presente' %}
<div class="d-flex align-items-center justify-content-between p-2 mb-1.5 rounded border {% if val == 'null' %}bg-light opacity-75{% else %}bg-white{% endif %}">
<span class="small fw-medium {% if val == 'null' %}text-muted{% else %}text-dark{% endif %} me-2">{% translate "Une surverse est-elle présente ?" %}</span>
<span class="badge {% if val == 'true' or val == True or val == 'oui' %}bg-success-subtle text-success border border-success-subtle{% elif val == 'false' or val == False or val == 'non' %}bg-secondary-subtle text-secondary border{% endif %} px-2 py-1">
{% if val == 'true' or val == True or val == 'oui' %}Oui{% elif val == 'false' or val == False or val == 'non' %}Non{% else %}Non vérifié{% endif %}
</span>
</div>
{% endwith %}
{% with val=q|get_q_val:'evc_sv_grille_propre' %}
<div class="d-flex align-items-center justify-content-between p-2 mb-1.5 rounded border {% if val == 'null' %}bg-light opacity-75{% else %}bg-white{% endif %}">
<span class="small fw-medium {% if val == 'null' %}text-muted{% else %}text-dark{% endif %} me-2">{% translate "La grille de surverse est propre et dégagée ?" %}</span>
<span class="badge {% if val == 'true' or val == True or val == 'oui' %}bg-success-subtle text-success border border-success-subtle{% elif val == 'false' or val == False or val == 'non' %}bg-danger-subtle text-danger border border-danger-subtle{% else %}bg-light text-muted border{% endif %} px-2 py-1">
{% if val == 'true' or val == True or val == 'oui' %}Oui{% elif val == 'false' or val == False or val == 'non' %}Non{% else %}Non vérifié{% endif %}
</span>
</div>
{% endwith %}
</div>
</div>
{% endif %}
<!-- ── 2. OED (Massif Drainant) BLOCS ── -->
{% if ouv.device_type == 'oed' %}
<!-- Bloc OED: État du revêtement -->
<div class="col-12">
<div class="border rounded-3 p-3 bg-white border-info-subtle shadow-sm">
<h6 class="fw-bold mb-3 small text-info text-uppercase border-bottom pb-1">
<i class="bi bi-road-perspective me-1"></i>{% translate "État général du revêtement / chaussée" %}
</h6>
{% with val=q|get_q_val:'oed_revetement_ok' %}
<div class="d-flex align-items-center justify-content-between p-2 mb-1.5 rounded border {% if val == 'null' %}bg-light opacity-75{% else %}bg-white{% endif %}">
<span class="small fw-medium {% if val == 'null' %}text-muted{% else %}text-dark{% endif %} me-2">{% translate "La surface au-dessus du massif drainant est en bon état ?" %}</span>
<span class="badge {% if val == 'true' or val == True or val == 'oui' %}bg-success-subtle text-success border border-success-subtle{% elif val == 'false' or val == False or val == 'non' %}bg-danger-subtle text-danger border border-danger-subtle{% else %}bg-light text-muted border{% endif %} px-2 py-1">
{% if val == 'true' or val == True or val == 'oui' %}Oui{% elif val == 'false' or val == False or val == 'non' %}Non{% else %}Non vérifié{% endif %}
</span>
</div>
{% endwith %}
</div>
</div>
<!-- Bloc OED: Bouche d'injection -->
<div class="col-12">
<div class="border rounded-3 p-3 bg-white border-info-subtle shadow-sm">
<h6 class="fw-bold mb-3 small text-info text-uppercase border-bottom pb-1">
<i class="bi bi-funnel me-1"></i>{% translate "Bouche d'injection & Drainage" %}
</h6>
{% with val=q|get_q_val:'oed_injection_presente' %}
<div class="d-flex align-items-center justify-content-between p-2 mb-1.5 rounded border {% if val == 'null' %}bg-light opacity-75{% else %}bg-white{% endif %}">
<span class="small fw-medium {% if val == 'null' %}text-muted{% else %}text-dark{% endif %} me-2">{% translate "Une bouche d'injection est-elle présente ?" %}</span>
<span class="badge {% if val == 'true' or val == True or val == 'oui' %}bg-success-subtle text-success border border-success-subtle{% elif val == 'false' or val == False or val == 'non' %}bg-secondary-subtle text-secondary border{% endif %} px-2 py-1">
{% if val == 'true' or val == True or val == 'oui' %}Oui{% elif val == 'false' or val == False or val == 'non' %}Non{% else %}Non vérifié{% endif %}
</span>
</div>
{% endwith %}
{% with val=q|get_q_val:'oed_bi_grille_propre' %}
<div class="d-flex align-items-center justify-content-between p-2 mb-1.5 rounded border {% if val == 'null' %}bg-light opacity-75{% else %}bg-white{% endif %}">
<span class="small fw-medium {% if val == 'null' %}text-muted{% else %}text-dark{% endif %} me-2">{% translate "La grille est propre et dégagée ?" %}</span>
<span class="badge {% if val == 'true' or val == True or val == 'oui' %}bg-success-subtle text-success border border-success-subtle{% elif val == 'false' or val == False or val == 'non' %}bg-danger-subtle text-danger border border-danger-subtle{% else %}bg-light text-muted border{% endif %} px-2 py-1">
{% if val == 'true' or val == True or val == 'oui' %}Oui{% elif val == 'false' or val == False or val == 'non' %}Non{% else %}Non vérifié{% endif %}
</span>
</div>
{% endwith %}
</div>
</div>
{% endif %}
<!-- ── 3. TA (Technique Alternative) BLOCS ── -->
{% if ouv.device_type == 'ta' %}
<!-- Bloc TA: Points d'entrée -->
<div class="col-12">
<div class="border rounded-3 p-3 bg-white border-teal-subtle shadow-sm" style="border-color:#b2dfdb;">
<h6 class="fw-bold mb-3 small text-teal text-uppercase" style="color:#00796b;">
<i class="bi bi-box-arrow-in-right me-1"></i>{% translate "Points d'entrée dans l'ouvrage" %}
</h6>
{% with val=q|get_q_val:'ta_pe_ok' %}
<div class="d-flex align-items-center justify-content-between p-2 mb-1.5 rounded border {% if val == 'null' %}bg-light opacity-75{% else %}bg-white{% endif %}">
<span class="small fw-medium {% if val == 'null' %}text-muted{% else %}text-dark{% endif %} me-2">{% translate "L'eau peut-elle facilement entrer dans l'ouvrage ?" %}</span>
<span class="badge {% if val == 'true' or val == True or val == 'oui' %}bg-success-subtle text-success border border-success-subtle{% elif val == 'false' or val == False or val == 'non' %}bg-danger-subtle text-danger border border-danger-subtle{% else %}bg-light text-muted border{% endif %} px-2 py-1">
{% if val == 'true' or val == True or val == 'oui' %}Oui{% elif val == 'false' or val == False or val == 'non' %}Non{% else %}Non vérifié{% endif %}
</span>
</div>
{% endwith %}
{% with val=q|get_q_val:'ta_pe_conduits_ok' %}
<div class="d-flex align-items-center justify-content-between p-2 mb-1.5 rounded border {% if val == 'null' %}bg-light opacity-75{% else %}bg-white{% endif %}">
<span class="small fw-medium {% if val == 'null' %}text-muted{% else %}text-dark{% endif %} me-2">{% translate "Les conduits et dispositifs amenant l'eau sont-ils fonctionnels ?" %}</span>
<span class="badge {% if val == 'true' or val == True or val == 'oui' %}bg-success-subtle text-success border border-success-subtle{% elif val == 'false' or val == False or val == 'non' %}bg-danger-subtle text-danger border border-danger-subtle{% else %}bg-light text-muted border{% endif %} px-2 py-1">
{% if val == 'true' or val == True or val == 'oui' %}Oui{% elif val == 'false' or val == False or val == 'non' %}Non{% else %}Non vérifié{% endif %}
</span>
</div>
{% endwith %}
</div>
</div>
<!-- Bloc TA: Bassin d'orage -->
<div class="col-12">
<div class="border rounded-3 p-3 bg-white border-teal-subtle shadow-sm" style="border-color:#b2dfdb;">
<h6 class="fw-bold mb-3 small text-teal text-uppercase" style="color:#00796b;">
<i class="bi bi-cloud-snow me-1"></i>{% translate "Bassin d'orage" %}
</h6>
{% with val=q|get_q_val:'ta_bo_vegetation_etat' %}
<div class="d-flex align-items-center justify-content-between p-2 mb-1.5 rounded border {% if val == 'null' %}bg-light opacity-75{% else %}bg-white{% endif %}">
<span class="small fw-medium {% if val == 'null' %}text-muted{% else %}text-dark{% endif %} me-2">{% translate "Quel est l'état de la végétation ?" %}</span>
<span class="badge {% if val == 'bon' %}bg-success-subtle text-success border border-success-subtle{% elif val == 'mauvais' %}bg-danger-subtle text-danger border border-danger-subtle{% else %}bg-light text-muted border{% endif %} px-2 py-1">
{{ val|format_giep_q_val }}
</span>
</div>
{% endwith %}
{% with val=q|get_q_val:'ta_bo_sediments' %}
<div class="d-flex align-items-center justify-content-between p-2 mb-1.5 rounded border {% if val == 'null' %}bg-light opacity-75{% else %}bg-white{% endif %}">
<span class="small fw-medium {% if val == 'null' %}text-muted{% else %}text-dark{% endif %} me-2">{% translate "Présence de sédiments dans l'ouvrage ?" %}</span>
<span class="badge {% if val == 'true' or val == True or val == 'oui' %}bg-danger-subtle text-danger border border-danger-subtle{% elif val == 'false' or val == False or val == 'non' %}bg-success-subtle text-success border border-success-subtle{% else %}bg-light text-muted border{% endif %} px-2 py-1">
{% if val == 'true' or val == True or val == 'oui' %}Oui{% elif val == 'false' or val == False or val == 'non' %}Non{% else %}Non vérifié{% endif %}
</span>
</div>
{% endwith %}
</div>
</div>
{% endif %}
<!-- ── Recommandations ── -->
{% if ouv.actions %} {% if ouv.actions %}
<div class="col-12"> <div class="col-12">
<div class="border rounded p-3 bg-white shadow-sm"> <div class="border rounded-3 p-3 bg-white shadow-sm">
<p class="fw-bold mb-2 text-secondary small text-uppercase border-bottom pb-1"> <h6 class="fw-bold mb-2 text-secondary small text-uppercase border-bottom pb-1">
<i class="bi bi-bookmark-star me-1"></i>{% translate "Recommandations" %} <i class="bi bi-bookmark-star me-1"></i>{% translate "Recommandations" %}
</p> </h6>
<div class="d-flex flex-wrap gap-1.5"> <div class="d-flex flex-wrap gap-1.5 pt-1">
{% if ouv.actions.aucune %} {% if ouv.actions.aucune %}
<span class="badge bg-success-subtle text-success border border-success-subtle p-2"><i class="bi bi-check-circle me-1"></i>{% translate "Aucune" %}</span> <span class="badge bg-success-subtle text-success border border-success-subtle p-2"><i class="bi bi-check-circle me-1"></i>{% translate "Aucune" %}</span>
{% endif %} {% endif %}
@ -96,9 +342,11 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
{% endwith %}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View file

@ -63,6 +63,45 @@
</div> </div>
</div> </div>
<!-- Modal pour Sélection des Équipements GIEP sur Carte -->
<div class="modal fade" id="giepAssetMapModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="giepAssetMapModalLabel" aria-hidden="true" style="z-index: 1060;">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content rounded-4 shadow-lg border-0">
<div class="modal-header bg-primary text-white py-2.5 px-3 rounded-top-4">
<h6 class="modal-title fw-bold mb-0" id="giepAssetMapModalLabel">
<i class="bi bi-droplet-fill me-2"></i>{% translate "Sélectionner un équipement GIEP sur la carte" %}
</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
</div>
<div class="modal-body p-3">
<div class="row g-2 mb-3">
<div class="col-12 col-md-7">
<div class="input-group input-group-sm">
<span class="input-group-text bg-white"><i class="bi bi-search text-muted"></i></span>
<input type="text" id="giep_modal_search_input" class="form-control" placeholder="{% translate 'Filtrer par code (ex: BI-01...) ou par type...' %}">
</div>
</div>
<div class="col-12 col-md-5">
<select id="giep_modal_type_filter" class="form-select form-select-sm">
<option value="">{% translate "Tous les types d'équipements" %}</option>
<option value="evc">{% translate "Espace vert creux (EVC)" %}</option>
<option value="oed">{% translate "Massif drainant (OED)" %}</option>
<option value="ta">{% translate "Technique alternative (TA)" %}</option>
</select>
</div>
</div>
<!-- Conteneur Carte Agrandie -->
<div id="giep_modal_map_container" class="rounded-3 border overflow-hidden" style="height: 480px; max-height: 68vh; position: relative;">
<div id="giepModalMap" style="width: 100%; height: 100%;"></div>
</div>
</div>
<div class="modal-footer py-2 px-3 bg-light">
<button type="button" class="btn btn-secondary btn-sm rounded-pill" data-bs-dismiss="modal">{% translate "Fermer" %}</button>
</div>
</div>
</div>
</div>
<style> <style>
.device-select-btn { .device-select-btn {
border: 2px solid #dee2e6; border: 2px solid #dee2e6;
@ -114,11 +153,14 @@
<script> <script>
(function() { (function() {
function createEmptyOuvrage(idx, deviceType = "evc") { function createEmptyOuvrage(idx, deviceType = "evc", asset = null) {
return { return {
local_id: String(idx), local_id: String(idx),
device_type: deviceType, device_type: deviceType,
name: `Ouvrage #${idx + 1}`, name: asset ? (asset.code + (asset.structure_type ? ' (' + asset.structure_type + ')' : '')) : `Ouvrage #${idx + 1}`,
asset_id: asset ? asset.id : null,
asset_code: asset ? asset.code : null,
asset_name: asset ? (asset.structure_type || asset.name || '') : null,
questions: { questions: {
// EVC // EVC
evc_pe_ok: null, evc_pe_ok: null,
@ -190,12 +232,163 @@
}; };
} }
let giepOuvrages = [createEmptyOuvrage(0, "evc")]; // Formulaire initialement à VIDE (0 ouvrages)
let giepOuvrages = [];
let locationGiepAssets = [];
let activeOuvrageIdxForMap = null;
let modalMapInstance = null;
let modalMapMarkers = [];
const container = document.getElementById('giep_ouvrages_container'); const container = document.getElementById('giep_ouvrages_container');
const serializedInput = document.getElementById('giep_data_serialized'); const serializedInput = document.getElementById('giep_data_serialized');
const addBtn = document.getElementById('giep_add_ouvrage_btn'); const addBtn = document.getElementById('giep_add_ouvrage_btn');
function extendBoundsFromGeom(bounds, geom) {
if (!bounds || !geom || !geom.coordinates) return;
const type = geom.type;
if (type === 'Point') {
bounds.extend(geom.coordinates);
} else if (type === 'LineString' || type === 'MultiPoint') {
geom.coordinates.forEach(c => bounds.extend(c));
} else if (type === 'Polygon' || type === 'MultiLineString') {
geom.coordinates.forEach(ring => ring.forEach(c => bounds.extend(c)));
} else if (type === 'MultiPolygon') {
geom.coordinates.forEach(poly => poly.forEach(ring => ring.forEach(c => bounds.extend(c))));
}
}
function fetchLocationAssets() {
if (locationGiepAssets.length > 0) return Promise.resolve(locationGiepAssets);
let locationId = null;
const modalTrigger = document.querySelector('[data-object-id]');
if (modalTrigger) {
locationId = modalTrigger.getAttribute('data-object-id');
} else {
const urlMatch = window.location.pathname.match(/\/zirw\/(\d+)\//);
if (urlMatch) locationId = urlMatch[1];
}
if (!locationId) return Promise.resolve([]);
return fetch("{% url 'assets:water_assets_geojson' %}?location=" + locationId)
.then(r => r.json())
.then(data => {
if (data && data.features) {
locationGiepAssets = data.features.map(f => {
const props = f.properties || {};
let lon = props.lon;
let lat = props.lat;
if ((lon === null || lon === undefined || lat === null || lat === undefined) && f.geometry) {
if (f.geometry.type === 'Point' && Array.isArray(f.geometry.coordinates)) {
lon = f.geometry.coordinates[0];
lat = f.geometry.coordinates[1];
}
}
return {
id: f.id,
code: props.code || `Asset #${f.id}`,
name: props.name || props.structure_type || props.category || props.code || `Asset #${f.id}`,
structure_type: props.structure_type || '',
category: props.category || '',
lon: (lon !== null && lon !== undefined) ? parseFloat(lon) : null,
lat: (lat !== null && lat !== undefined) ? parseFloat(lat) : null,
geometry: f.geometry,
rawFeature: f
};
});
}
return locationGiepAssets;
})
.catch(err => {
console.warn("Erreur de chargement des équipements GIEP :", err);
return [];
});
}
function inferDeviceType(asset) {
if (!asset) return null;
const text = ((asset.structure_type || '') + ' ' + (asset.name || '') + ' ' + (asset.category || '')).toLowerCase();
// Avaloirs, Bouches d'injection, Regards, Grilles : équipements d'accès non associés à l'un des 3 types
if (text.includes('avaloir') || text.includes('injection') || text.includes('regard') || text.includes('grille')) {
return null;
}
// EVC (Espace Vert Creux, Noue, Jardin de pluie, Zone végétalisée creuse)
if (text.includes('espace vert') || text.includes('creux') || text.includes('evc') || text.includes('noue') || text.includes('gazon') || text.includes('jardin') || text.includes('végétalis')) {
return 'evc';
}
// TA (Technique Alternative, Bassin d'orage, Surverse, Trop-plein, Marre)
if (text.includes('bassin') || text.includes('orage') || text.includes('technique alternative') || text.includes('ta') || text.includes('surverse') || text.includes('trop plein') || text.includes('marre')) {
return 'ta';
}
// OED (Massif drainant, Chaussée réservoir, Dalles, Tranchée drainante, Revêtement poreux)
if (text.includes('massif') || text.includes('drainant') || text.includes('oed') || text.includes('chaussée') || text.includes('revêtement') || text.includes('dalle') || text.includes('tranchée')) {
return 'oed';
}
return null;
}
let currentMapPopup = null;
function showAssetPopup(asset, lngLat) {
if (!modalMapInstance) return;
if (currentMapPopup) {
currentMapPopup.remove();
currentMapPopup = null;
}
const devType = inferDeviceType(asset);
let badgeHtml = '';
if (devType === 'evc') badgeHtml = '<span class="badge bg-success-subtle text-success text-uppercase me-1">EVC</span>';
else if (devType === 'oed') badgeHtml = '<span class="badge bg-info-subtle text-info text-uppercase me-1">OED</span>';
else if (devType === 'ta') badgeHtml = '<span class="badge bg-teal-subtle text-teal text-uppercase me-1">TA</span>';
else badgeHtml = '<span class="badge bg-secondary-subtle text-secondary text-uppercase me-1">Équipement</span>';
const popupNode = document.createElement('div');
popupNode.className = 'p-1';
popupNode.innerHTML = `
<div class="d-flex align-items-center gap-1.5 mb-1">
<i class="bi bi-droplet-fill text-primary"></i>
<strong class="fw-bold text-dark fs-6">${asset.code}</strong>
</div>
<div class="small text-dark fw-semibold mb-1">${asset.name || asset.structure_type || ''}</div>
<div class="extra-small text-muted mb-2">
${badgeHtml}
<span>${asset.structure_type || ''}</span>
</div>
<button type="button" class="btn btn-primary btn-sm w-100 rounded-pill py-1.5 px-3 fw-semibold select-popup-btn">
<i class="bi bi-check-circle me-1"></i>{% filter escapejs %}{% translate "Sélectionner" %}{% endfilter %}
</button>
`;
popupNode.querySelector('.select-popup-btn').addEventListener('click', () => {
selectAssetForOuvrage(activeOuvrageIdxForMap, asset);
const modalEl = document.getElementById('giepAssetMapModal');
const modalInstance = bootstrap.Modal.getInstance(modalEl);
if (modalInstance) modalInstance.hide();
});
currentMapPopup = new maplibregl.Popup({ offset: 15, closeButton: true, maxWidth: '250px' })
.setLngLat(lngLat)
.setDOMContent(popupNode)
.addTo(modalMapInstance);
}
function selectAssetForOuvrage(idx, asset) {
if (typeof idx !== 'number' || !giepOuvrages[idx]) return;
giepOuvrages[idx].asset_id = asset.id;
giepOuvrages[idx].asset_code = asset.code;
giepOuvrages[idx].asset_name = asset.structure_type || asset.name || asset.code;
const inferred = inferDeviceType(asset);
if (inferred) {
giepOuvrages[idx].device_type = inferred;
}
renderOuvrages();
}
function updateSerializedGiepData() { function updateSerializedGiepData() {
const pluieVal = document.getElementById('giep_date_derniere_pluie')?.value || 'null'; const pluieVal = document.getElementById('giep_date_derniere_pluie')?.value || 'null';
const orageVal = document.getElementById('giep_orage_moins_une_heure')?.value || 'null'; const orageVal = document.getElementById('giep_orage_moins_une_heure')?.value || 'null';
@ -234,10 +427,274 @@
`; `;
} }
function populateModalAssetsList(filterText = '', filterType = '') {
// Obsolete list, filtering operates directly on map layers
}
function renderModalMapFeatures(isInitialFit = false) {
if (!modalMapInstance) return;
if (modalMapMarkers) {
modalMapMarkers.forEach(m => m.remove());
modalMapMarkers = [];
}
const filterText = (document.getElementById('giep_modal_search_input')?.value || '').toLowerCase().trim();
const filterType = document.getElementById('giep_modal_type_filter')?.value || '';
const filteredFeatures = [];
const bounds = (typeof maplibregl !== 'undefined') ? new maplibregl.LngLatBounds() : null;
let hasCoords = false;
locationGiepAssets.forEach(asset => {
const matchText = !filterText || asset.code.toLowerCase().includes(filterText) || (asset.structure_type && asset.structure_type.toLowerCase().includes(filterText)) || (asset.name && asset.name.toLowerCase().includes(filterText));
const devType = inferDeviceType(asset);
const matchType = !filterType || devType === filterType;
if (matchText && matchType && asset.rawFeature && asset.rawFeature.geometry && typeof maplibregl !== 'undefined') {
const geom = asset.rawFeature.geometry;
let color = '#0277bd'; // Bleu GIEP (naturerwiasset)
if (devType === 'evc') color = '#2e7d32'; // Vert EVC
if (devType === 'oed') color = '#0277bd'; // Bleu OED
if (devType === 'ta') color = '#00796b'; // Turquoise TA
const featureClone = {
type: 'Feature',
id: asset.id,
geometry: geom,
properties: {
...(asset.rawFeature.properties || {}),
id: asset.id,
code: asset.code,
name: asset.name,
structure_type: asset.structure_type,
device_type: devType,
color: color
}
};
filteredFeatures.push(featureClone);
if (isInitialFit) {
extendBoundsFromGeom(bounds, geom);
}
hasCoords = true;
// Marqueurs ronds pour les géométries de type Point
if (geom.type === 'Point' && Array.isArray(geom.coordinates)) {
const lon = geom.coordinates[0];
const lat = geom.coordinates[1];
if (!isNaN(lon) && !isNaN(lat)) {
const el = document.createElement('div');
el.className = 'giep-map-marker';
el.style.width = '28px';
el.style.height = '28px';
el.style.borderRadius = '50%';
el.style.backgroundColor = color;
el.style.border = '2.5px solid white';
el.style.boxShadow = '0 3px 8px rgba(0,0,0,0.4)';
el.style.cursor = 'pointer';
el.style.display = 'flex';
el.style.alignItems = 'center';
el.style.justifyContent = 'center';
el.innerHTML = '<i class="bi bi-droplet-fill text-white" style="font-size: 14px;"></i>';
el.title = `${asset.code} - ${asset.structure_type || asset.name}`;
const marker = new maplibregl.Marker({ element: el })
.setLngLat([lon, lat])
.addTo(modalMapInstance);
el.addEventListener('click', (e) => {
e.stopPropagation();
showAssetPopup(asset, [lon, lat]);
});
modalMapMarkers.push(marker);
}
}
}
});
const geojsonPayload = {
type: 'FeatureCollection',
features: filteredFeatures
};
const source = modalMapInstance.getSource('giep-assets-src');
if (source) {
source.setData(geojsonPayload);
} else {
modalMapInstance.addSource('giep-assets-src', {
type: 'geojson',
data: geojsonPayload
});
// 1. Couche de remplissage (Polygones) — EN-DESSOUS
modalMapInstance.addLayer({
id: 'giep-assets-fill',
type: 'fill',
source: 'giep-assets-src',
filter: ['match', ['geometry-type'], ['Polygon', 'MultiPolygon'], true, false],
paint: {
'fill-color': ['get', 'color'],
'fill-opacity': 0.45
}
});
// 2. Couche de lignes (Lignes et contours) — AU MILIEU
modalMapInstance.addLayer({
id: 'giep-assets-line',
type: 'line',
source: 'giep-assets-src',
filter: ['match', ['geometry-type'], ['LineString', 'MultiLineString', 'Polygon', 'MultiPolygon'], true, false],
paint: {
'line-color': ['get', 'color'],
'line-width': 3.5
}
});
// 3. Couche de cercles (Points) — AU-DESSUS
modalMapInstance.addLayer({
id: 'giep-assets-circle',
type: 'circle',
source: 'giep-assets-src',
filter: ['match', ['geometry-type'], ['Point', 'MultiPoint'], true, false],
paint: {
'circle-color': ['get', 'color'],
'circle-radius': 8,
'circle-stroke-width': 2.5,
'circle-stroke-color': '#ffffff'
}
});
// Un seul handler de clic sur la carte avec priorité stricte de géométrie (Points > Lignes > Polygones)
modalMapInstance.on('click', (e) => {
const layers = ['giep-assets-circle', 'giep-assets-line', 'giep-assets-fill'];
const features = modalMapInstance.queryRenderedFeatures(e.point, { layers: layers });
if (!features || features.length === 0) return;
const priorityOrder = { 'Point': 1, 'MultiPoint': 1, 'LineString': 2, 'MultiLineString': 2, 'Polygon': 3, 'MultiPolygon': 3 };
features.sort((a, b) => {
const typeA = a.geometry ? a.geometry.type : '';
const typeB = b.geometry ? b.geometry.type : '';
const pA = priorityOrder[typeA] || 99;
const pB = priorityOrder[typeB] || 99;
return pA - pB;
});
const topFeature = features[0];
const props = topFeature.properties;
const targetAsset = locationGiepAssets.find(a => String(a.id) === String(props.id));
if (targetAsset) {
showAssetPopup(targetAsset, e.lngLat);
}
});
['giep-assets-circle', 'giep-assets-line', 'giep-assets-fill'].forEach(layerId => {
modalMapInstance.on('mouseenter', layerId, () => {
modalMapInstance.getCanvas().style.cursor = 'pointer';
});
modalMapInstance.on('mouseleave', layerId, () => {
modalMapInstance.getCanvas().style.cursor = '';
});
});
}
// Seul le chargement initial modifie le zoom et cadrage de la carte
if (isInitialFit && hasCoords && bounds && !bounds.isEmpty()) {
modalMapInstance.fitBounds(bounds, { padding: 50, maxZoom: 18 });
}
}
function initOrUpdateModalMap() {
const containerEl = document.getElementById('giepModalMap');
if (!containerEl || typeof maplibregl === 'undefined') return;
const style = window.MAP_STYLE_LIGHT || 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json';
if (!modalMapInstance) {
modalMapInstance = new maplibregl.Map({
container: 'giepModalMap',
style: style,
center: [4.3517, 50.8503],
zoom: 15,
antialias: true
});
// Contrôles de navigation et de géolocalisation
modalMapInstance.addControl(new maplibregl.NavigationControl(), 'top-right');
const geolocate = new maplibregl.GeolocateControl({
positionOptions: { enableHighAccuracy: true },
trackUserLocation: true,
showUserLocation: true
});
modalMapInstance.addControl(geolocate, 'top-right');
modalMapInstance.on('load', () => {
renderModalMapFeatures(true);
geolocate.trigger();
});
} else {
setTimeout(() => {
modalMapInstance.resize();
renderModalMapFeatures(true);
}, 150);
}
// Tentative de géolocalisation de l'utilisateur si navigateur supporté
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(pos) => {
const uLon = pos.coords.longitude;
const uLat = pos.coords.latitude;
if (modalMapInstance) {
modalMapInstance.flyTo({ center: [uLon, uLat], zoom: 16 });
}
},
(err) => {
console.log("Géolocalisation utilisateur non disponible:", err);
},
{ enableHighAccuracy: true, timeout: 6000 }
);
}
}
function openMapModal(idx) {
activeOuvrageIdxForMap = idx;
fetchLocationAssets().then(assets => {
populateModalAssetsList();
const modalEl = document.getElementById('giepAssetMapModal');
let modalInstance = bootstrap.Modal.getInstance(modalEl);
if (!modalInstance) {
modalInstance = new bootstrap.Modal(modalEl);
}
modalInstance.show();
});
}
function renderOuvrages() { function renderOuvrages() {
if (!container) return; if (!container) return;
container.innerHTML = ''; container.innerHTML = '';
if (giepOuvrages.length === 0) {
container.innerHTML = `
<div class="text-center p-4 border border-dashed rounded-4 bg-light shadow-sm my-2" id="giep_empty_state">
<i class="bi bi-inbox fs-2 text-primary opacity-50 d-block mb-2"></i>
<h6 class="fw-bold text-secondary mb-1">{% filter escapejs %}{% translate "Aucun ouvrage dans cette inspection" %}{% endfilter %}</h6>
<p class="small text-muted mb-3">{% filter escapejs %}{% translate "Cliquez ci-dessous pour ajouter un ouvrage à inspecter." %}{% endfilter %}</p>
<button type="button" class="btn btn-success btn-sm px-3.5 py-2 rounded-pill fw-semibold shadow-sm" id="giep_add_first_ouvrage_btn">
<i class="bi bi-plus-circle me-1"></i>{% filter escapejs %}{% translate "Ajouter un ouvrage" %}{% endfilter %}
</button>
</div>
`;
document.getElementById('giep_add_first_ouvrage_btn')?.addEventListener('click', () => {
giepOuvrages.push(createEmptyOuvrage(0, "evc"));
renderOuvrages();
});
updateSerializedGiepData();
return;
}
giepOuvrages.forEach((ouv, idx) => { giepOuvrages.forEach((ouv, idx) => {
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'card border p-3 bg-light rounded-4 shadow-sm'; card.className = 'card border p-3 bg-light rounded-4 shadow-sm';
@ -513,10 +970,48 @@
`; `;
} }
const titleHeader = ouv.asset_code ? `${ouv.asset_code} — ${ouv.asset_name || 'Ouvrage #' + (idx + 1)}` : `Ouvrage #${idx + 1}`;
card.innerHTML = ` card.innerHTML = `
<div class="d-flex align-items-center justify-content-between mb-3 border-bottom pb-2"> <div class="d-flex align-items-center justify-content-between mb-3 border-bottom pb-2">
<span class="fw-bold text-dark">{% translate "Ouvrage" %} #${idx + 1}</span> <span class="fw-bold text-dark"><i class="bi bi-gear-fill me-1.5 text-primary"></i>${titleHeader}</span>
${idx > 0 ? `<button type="button" class="btn btn-outline-danger btn-sm py-0.5 px-2.5 rounded-pill remove-ouv-btn" data-idx="${idx}"><i class="bi bi-trash me-1"></i>{% translate "Supprimer" %}</button>` : ''} <button type="button" class="btn btn-outline-danger btn-sm py-0.5 px-2.5 rounded-pill remove-ouv-btn" data-idx="${idx}">
<i class="bi bi-trash me-1"></i>{% translate "Supprimer" %}
</button>
</div>
<!-- Liaison Équipement GIEP / Ouvrage -->
<div class="border rounded-3 p-3 mb-3 bg-white border-primary-subtle shadow-sm">
<div class="d-flex align-items-center justify-content-between mb-2">
<span class="extra-small fw-bold text-primary text-uppercase">
<i class="bi bi-link-45deg me-1"></i>{% translate "Équipement GIEP associé (Optionnel)" %}
</span>
${ouv.asset_id ? `<button type="button" class="btn btn-link text-danger p-0 extra-small text-decoration-none clear-asset-btn" data-idx="${idx}"><i class="bi bi-x-circle me-1"></i>{% translate "Délier" %}</button>` : ''}
</div>
${ouv.asset_id ? `
<div class="p-2.5 rounded-3 bg-primary-subtle bg-opacity-20 border border-primary-subtle d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-geo-alt-fill text-primary fs-5"></i>
<div>
<strong class="small text-primary d-block">${ouv.asset_code}</strong>
<span class="extra-small text-muted">${ouv.asset_name || ''}</span>
</div>
</div>
<span class="badge bg-primary text-white rounded-pill extra-small">{% translate "Lié" %}</span>
</div>
` : `
<div class="position-relative">
<div class="input-group input-group-sm">
<span class="input-group-text bg-white"><i class="bi bi-search text-muted"></i></span>
<input type="text" class="form-control asset-search-input" data-idx="${idx}" placeholder="{% filter escapejs %}{% translate "Rechercher par code (ex: BI-01...) ou par type..." %}{% endfilter %}" autocomplete="off">
<button type="button" class="btn btn-outline-primary open-map-modal-btn" data-idx="${idx}">
<i class="bi bi-geo-alt-fill me-1"></i>{% translate "Choisir sur la carte" %}
</button>
</div>
<div class="list-group position-absolute w-100 shadow-lg d-none asset-autocomplete-dropdown" data-idx="${idx}" style="z-index: 1050; max-height: 200px; overflow-y: auto; background: white;"></div>
</div>
`}
</div> </div>
<!-- Device Selector Symbology --> <!-- Device Selector Symbology -->
@ -574,13 +1069,72 @@
container.appendChild(card); container.appendChild(card);
}); });
// Event listeners pour la carte et l'autocomplétion
container.querySelectorAll('.open-map-modal-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const idx = parseInt(e.currentTarget.getAttribute('data-idx'));
openMapModal(idx);
});
});
container.querySelectorAll('.clear-asset-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const idx = parseInt(e.currentTarget.getAttribute('data-idx'));
if (giepOuvrages[idx]) {
giepOuvrages[idx].asset_id = null;
giepOuvrages[idx].asset_code = null;
giepOuvrages[idx].asset_name = null;
renderOuvrages();
}
});
});
container.querySelectorAll('.asset-search-input').forEach(input => {
input.addEventListener('input', (e) => {
const idx = parseInt(e.target.getAttribute('data-idx'));
const val = e.target.value.trim().toLowerCase();
const dropdown = container.querySelector(`.asset-autocomplete-dropdown[data-idx="${idx}"]`);
if (!dropdown) return;
if (!val) {
dropdown.classList.add('d-none');
dropdown.innerHTML = '';
return;
}
fetchLocationAssets().then(assets => {
const matches = assets.filter(a => a.code.toLowerCase().includes(val) || (a.structure_type && a.structure_type.toLowerCase().includes(val)) || (a.name && a.name.toLowerCase().includes(val))).slice(0, 8);
if (matches.length === 0) {
dropdown.classList.add('d-none');
dropdown.innerHTML = '';
return;
}
dropdown.innerHTML = matches.map(a => `
<button type="button" class="list-group-item list-group-item-action py-2 px-3 text-start autocomplete-item" data-id="${a.id}">
<div class="fw-bold small text-primary">${a.code}</div>
<div class="extra-small text-muted">${a.structure_type || a.name || ''}</div>
</button>
`).join('');
dropdown.classList.remove('d-none');
dropdown.querySelectorAll('.autocomplete-item').forEach(itemBtn => {
itemBtn.addEventListener('click', () => {
const assetId = itemBtn.getAttribute('data-id');
const targetAsset = assets.find(a => String(a.id) === String(assetId));
if (targetAsset) selectAssetForOuvrage(idx, targetAsset);
});
});
});
});
});
// Event listeners pour le sélecteur de dispositif // Event listeners pour le sélecteur de dispositif
container.querySelectorAll('.device-select-btn').forEach(btn => { container.querySelectorAll('.device-select-btn').forEach(btn => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
const target = e.currentTarget; const target = e.currentTarget;
const idx = parseInt(target.getAttribute('data-idx')); const idx = parseInt(target.getAttribute('data-idx'));
const dev = target.getAttribute('data-device'); const dev = target.getAttribute('data-device');
giepOuvrages[idx] = createEmptyOuvrage(idx, dev); giepOuvrages[idx].device_type = dev;
renderOuvrages(); renderOuvrages();
}); });
}); });
@ -635,6 +1189,28 @@
}); });
} }
// Modal filter & shown listeners
const modalEl = document.getElementById('giepAssetMapModal');
if (modalEl) {
modalEl.addEventListener('shown.bs.modal', () => {
initOrUpdateModalMap();
});
}
document.getElementById('giep_modal_search_input')?.addEventListener('input', (e) => {
const text = e.target.value;
const type = document.getElementById('giep_modal_type_filter')?.value || '';
populateModalAssetsList(text, type);
renderModalMapFeatures(false);
});
document.getElementById('giep_modal_type_filter')?.addEventListener('change', (e) => {
const type = e.target.value;
const text = document.getElementById('giep_modal_search_input')?.value || '';
populateModalAssetsList(text, type);
renderModalMapFeatures(false);
});
document.querySelectorAll('.giep-input').forEach(el => { document.querySelectorAll('.giep-input').forEach(el => {
el.addEventListener('change', updateSerializedGiepData); el.addEventListener('change', updateSerializedGiepData);
}); });

View file

@ -1,7 +1,7 @@
{% load i18n %} {% load i18n %}
<!-- Modale de Saisie Rapide d'Inspection --> <!-- Modale de Saisie Rapide d'Inspection -->
<div class="modal fade" id="quickInspectionModal" tabindex="-1" aria-labelledby="quickInspectionModalLabel" aria-hidden="true"> <div class="modal fade" id="quickInspectionModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="quickInspectionModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered"> <div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content shadow"> <div class="modal-content shadow">
<div class="modal-header bg-light"> <div class="modal-header bg-light">
@ -118,7 +118,7 @@
</div> </div>
<!-- Modal caméra Canvas (mode continu pour inspection) --> <!-- Modal caméra Canvas (mode continu pour inspection) -->
<div class="modal fade" id="inspCameraModal" tabindex="-1" aria-labelledby="inspCameraModalLabel" aria-hidden="true" style="z-index: 1060;"> <div class="modal fade" id="inspCameraModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="inspCameraModalLabel" aria-hidden="true" style="z-index: 1060;">
<div class="modal-dialog modal-fullscreen-sm-down d-flex m-0 h-100 mx-auto"> <div class="modal-dialog modal-fullscreen-sm-down d-flex m-0 h-100 mx-auto">
<div class="modal-content bg-dark text-white d-flex flex-column"> <div class="modal-content bg-dark text-white d-flex flex-column">
<div class="modal-header border-0 flex-shrink-0 py-2"> <div class="modal-header border-0 flex-shrink-0 py-2">
@ -150,7 +150,7 @@
</div> </div>
<!-- Modale de Choix d'Asset (Zone dense / Sélection multiple) --> <!-- Modale de Choix d'Asset (Zone dense / Sélection multiple) -->
<div class="modal fade" id="targetChooserModal" tabindex="-1" aria-labelledby="targetChooserModalLabel" aria-hidden="true"> <div class="modal fade" id="targetChooserModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="targetChooserModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content shadow"> <div class="modal-content shadow">
<div class="modal-header bg-light"> <div class="modal-header bg-light">

View file

@ -25,3 +25,98 @@ def result_btn_class(status_code):
@register.filter @register.filter
def result_icon(status_code): def result_icon(status_code):
return INSPECTION_RESULT_CONFIG.get(status_code, {}).get('icon', 'bi-question-circle') return INSPECTION_RESULT_CONFIG.get(status_code, {}).get('icon', 'bi-question-circle')
GIEP_QUESTION_LABELS = {
'evc_pe_ok': "L'eau peut-elle facilement entrer dans l'EVC ?",
'evc_pe_feuilles': "Des feuilles ou branches bloquent-elles l'eau ?",
'evc_pe_detritus': "Un amas de détritus bloque-t-il l'eau ?",
'evc_empierrement_present': "Un empierrement est-il présent à l'entrée ?",
'evc_empierrement_ok': "Quel est l'état de l'empierrement ?",
'evc_empierrement_colmate': "Colmaté par les sédiments",
'evc_empierrement_emporte': "Emporté par l'eau",
'evc_empierrement_envahi': "Envahi d'adventices",
'evc_ev_veg_etat': "Quel est l'état de la végétation ?",
'evc_ev_plantes_mortes': "Présence de plantes mortes ou absentes :",
'evc_ev_chenaux_erosion': "Présence de chenaux d'érosion ?",
'evc_ev_flaques': "Présence de flaques d'eau persistantes ?",
'evc_ev_sediments': "Présence de sédiments dans l'ouvrage ?",
'evc_ev_croute_sediments': "La croûte de sédimentation est-elle supérieure à 5 cm ?",
'evc_ev_envahissantes': "Présence d'espèces exotiques envahissantes ?",
'evc_surverse_presente': "Une surverse est-elle présente ?",
'evc_sv_grille_propre': "La grille de surverse est propre et dégagée ?",
'evc_sv_decantation_vide': "La décantation est vide ou peu remplie ?",
'evc_sv_haute': "La surverse est-elle plus haute que le fond de l'ouvrage ?",
'oed_revetement_ok': "La surface au-dessus du massif drainant est en bon état ?",
'oed_revetement_ornieres': "Présence d'ornières",
'oed_revetement_fissures': "Présence de fissures",
'oed_revetement_trous': "Présence de trous",
'oed_injection_presente': "Une bouche d'injection est-elle présente ?",
'oed_bi_niveaux_ok': "Les niveaux du revêtement conduisent bien l'eau vers la bouche ?",
'oed_bi_grille_propre': "La grille est propre et dégagée ?",
'oed_bi_decantation_vide': "La décantation est-elle vide ou presque ?",
'oed_bi_filtre_present': "La bouche d'injection dispose d'un filtre ?",
'oed_bi_filtre_propre': "Le filtre est propre ?",
'oed_bi_filtre_bon': "Le filtre est en bon état ?",
'oed_bi_drain_colmate': "Le drain semble colmaté ?",
'oed_rhm_presente': "Structure enherbée ou remplissage minéral ?",
'oed_rhm_stagne': "De l'eau stagne-t-elle en surface ?",
'oed_rhm_stagne_plus_2h': "L'eau stagne plus de 2 heures ?",
'oed_rhm_joints_ok': "Les joints sont-ils en bon état ?",
'oed_rhm_joints_fines': "Présence importante de fines dans les joints ?",
'oed_rhm_joints_remplissage': "Niveau de remplissage plus haut que la structure ?",
'oed_rp_presente': "Modulaire poreux ou continu poreux ?",
'oed_rp_stagne': "De l'eau stagne-t-elle en surface ?",
'oed_rp_stagne_plus_2h': "L'eau stagne plus de 2 heures ?",
'oed_rp_depots': "Présence de dépôts visibles (poussières, mousses, etc.) ?",
'oed_rp_test_permeabilite': "Test de perméabilité nécessaire ?",
'oed_rna_presente': "Matériaux non agrégés (graviers/mulch) ?",
'oed_rna_stagne': "De l'eau stagne-t-elle en surface ?",
'oed_rna_stagne_plus_2h': "L'eau stagne plus de 2 heures ?",
'oed_rna_matiere_ok': "Le volume de matière est-il en suffisance ?",
'oed_rna_geotextile_visible': "Le géotextile est-il visible ?",
'ta_pe_ok': "L'eau peut-elle facilement entrer dans l'ouvrage ?",
'ta_pe_conduits_ok': "Les conduits et dispositifs amenant l'eau sont-ils fonctionnels ?",
'ta_pe_grilles': "Les grilles sont-elles propres et dégagées ?",
'ta_pe_feuilles': "Des feuilles ou branches bloquent-elles l'eau ?",
'ta_pe_detritus': "Un amas de détritus bloque-t-il l'eau ?",
'ta_bo_vegetation_etat': "Quel est l'état de la végétation ?",
'ta_bo_plantes_mortes': "Présence de plantes mortes ou absentes :",
'ta_bo_chenaux': "Présence de chenaux d'érosion ?",
'ta_bo_flaques': "Présence de flaques d'eau persistantes ?",
'ta_bo_sediments': "Présence de sédiments dans le bassin d'orage ?",
'ta_bo_rempli_cinquieme': "L'ouvrage est-il rempli au 5e de sa capacité ?",
'ta_bo_detritus': "Présence de détritus dans l'ouvrage ?"
}
GIEP_VALUE_LABELS = {
'true': "Oui",
'false': "Non",
'null': "Non vérifié",
'bon': "Bon",
'mauvais': "Mauvais",
'plus_10': "+ de 10%",
'moins_10': "- de 10%",
}
@register.filter
def format_giep_q_label(key):
return GIEP_QUESTION_LABELS.get(str(key), str(key).replace('_', ' ').capitalize())
@register.filter
def format_giep_q_val(val):
if val is True:
return "Oui"
if val is False:
return "Non"
if val is None or val == 'null':
return "Non vérifié"
return GIEP_VALUE_LABELS.get(str(val).lower(), str(val))
@register.filter
def get_q_val(questions_dict, key):
if not isinstance(questions_dict, dict):
return 'null'
val = questions_dict.get(str(key))
if val is None or val == '' or val == 'null':
return 'null'
return val

View file

@ -22,6 +22,21 @@ class InspectionModelAndViewsTestCase(TestCase):
first_name='Jean', first_name='Jean',
last_name='Valjean' last_name='Valjean'
) )
from common.models import UserConfig, Role, UserThematics
admin_role, _ = Role.objects.get_or_create(name='admin')
config = UserConfig.objects.create(user=self.user)
config.roles.add(admin_role)
self.water_thematic, _ = Thematic.objects.get_or_create(
code='water',
defaults={'name_fr': 'Gestion de l\'Eau / GIEP'}
)
UserThematics.objects.create(
user_config=config,
thematic=self.water_thematic,
can_view_assets=True,
can_edit_assets=True
)
self.thematic, _ = Thematic.objects.get_or_create( self.thematic, _ = Thematic.objects.get_or_create(
code='parking', code='parking',
@ -142,7 +157,7 @@ class InspectionModelAndViewsTestCase(TestCase):
tmpl_rwiz = get_inspection_partial_template('naturerwiz') tmpl_rwiz = get_inspection_partial_template('naturerwiz')
tmpl_asset = get_inspection_partial_template('naturerwiasset') tmpl_asset = get_inspection_partial_template('naturerwiasset')
self.assertEqual(tmpl_rwiz, 'inspections/components/asset_fields/_naturerwiz.html') self.assertEqual(tmpl_rwiz, 'inspections/components/asset_fields/_naturerwiz.html')
self.assertEqual(tmpl_asset, 'inspections/components/asset_fields/_naturerwiz.html') self.assertIsNone(tmpl_asset)
def test_registry_detail_templates(self): def test_registry_detail_templates(self):
from .registry import get_inspection_detail_partial_template from .registry import get_inspection_detail_partial_template
@ -155,3 +170,27 @@ class InspectionModelAndViewsTestCase(TestCase):
self.assertEqual(tmpl_giep, 'inspections/components/asset_details/_naturerwiz.html') self.assertEqual(tmpl_giep, 'inspections/components/asset_details/_naturerwiz.html')
self.assertEqual(tmpl_parking, 'inspections/components/asset_details/_parkingspot.html') self.assertEqual(tmpl_parking, 'inspections/components/asset_details/_parkingspot.html')
def test_zirw_detail_view_renders_cleanly(self):
self.client.login(username='inspector_user', password='password123')
url = reverse('assets:zirw_detail', kwargs={'location_id': self.rwiz.id})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_spatial_location_resolution_for_rwi_asset(self):
from assets.templatetags.asset_tags import asset_location_link
from django.contrib.gis.geos import Polygon, MultiPolygon, Point
poly = Polygon(((0, 0), (0, 10), (10, 10), (10, 0), (0, 0)), srid=3812)
rwiz_poly = NatureRWIZ.objects.create(
code="RWIZ-TEST",
name_fr="Zone Test Polygons",
geom=MultiPolygon(poly, srid=3812)
)
rwi_asset = NatureRWIAsset.objects.create(
code="RWI-TEST-01",
geom=Point(5, 5, srid=3812)
)
ctx = asset_location_link({'request': None}, rwi_asset)
self.assertEqual(ctx['location'], rwiz_poly)

View file

@ -277,7 +277,7 @@ def create_inspection_api(request):
return JsonResponse({'success': False, 'error': _("Cible introuvable : %s") % str(e)}, status=404) return JsonResponse({'success': False, 'error': _("Cible introuvable : %s") % str(e)}, status=404)
is_parking_spot = (content_type.model == 'parkingspot') is_parking_spot = (content_type.model == 'parkingspot')
is_giep = (content_type.model in ('naturerwiz', 'naturerwiasset')) is_giep = (content_type.model == 'naturerwiz')
try: try:
if is_parking_spot: if is_parking_spot: