feat(inspections): restructurer le formulaire d'inspection GIEP par ouvrage
- Isoler la météo au niveau de la zone GIEP globale - Structurer chaque ouvrage : Conformité, Photos dédiées, Formulaire technique, Remarques - Lier les photos aux ouvrages via field_key et auto-consolider statut et remarques - Enrichir l'affichage de consultation et ajouter la suite de tests unitaires
This commit is contained in:
parent
5e9fced68f
commit
c6ef935e0b
11 changed files with 1111 additions and 312 deletions
|
|
@ -153,7 +153,7 @@
|
|||
{% endif %}
|
||||
</ul>
|
||||
|
||||
{% if insp.notes %}
|
||||
{% if insp.notes and not insp.is_giep %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small fw-semibold"><i class="bi bi-journal-text me-1"></i>{% translate "Remarques / Observations" %}:</label>
|
||||
<div class="p-2 bg-light rounded border text-secondary small">
|
||||
|
|
@ -162,7 +162,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if insp.documents.all %}
|
||||
{% if insp.documents.all and not insp.is_giep %}
|
||||
<div>
|
||||
<label class="form-label text-muted small fw-semibold mb-2"><i class="bi bi-camera me-1"></i>{% translate "Photos & Documents joints" %} ({{ insp.documents.count }}):</label>
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
|
|
|
|||
|
|
@ -245,6 +245,14 @@ class Inspection(models.Model):
|
|||
def is_scheduled(self):
|
||||
return self.status == 'scheduled'
|
||||
|
||||
@property
|
||||
def is_giep(self):
|
||||
return (
|
||||
(self.asset_content_type and self.asset_content_type.model in ('naturerwiz', 'giepinspection')) or
|
||||
hasattr(self, 'giepinspection') or
|
||||
isinstance(self, GiepInspection)
|
||||
)
|
||||
|
||||
@property
|
||||
def timeliness(self):
|
||||
"""
|
||||
|
|
@ -791,6 +799,149 @@ class GiepInspection(Inspection):
|
|||
verbose_name = _("Inspection GIEP / Eaux pluviales")
|
||||
verbose_name_plural = _("Inspections GIEP / Eaux pluviales")
|
||||
|
||||
@property
|
||||
def is_giep(self):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def compute_status_for_single_ouvrage(cls, ouv):
|
||||
"""
|
||||
Calcule le résultat ('damaged', 'non_compliant', 'compliant')
|
||||
sur base des réponses aux questions d'un ouvrage GIEP.
|
||||
Priorité : damaged > non_compliant > compliant.
|
||||
"""
|
||||
if not ouv or not isinstance(ouv, dict):
|
||||
return 'compliant'
|
||||
explicit_status = ouv.get('result_status')
|
||||
if explicit_status in ('damaged', 'non_compliant', 'compliant') and ouv.get('is_result_manual'):
|
||||
return explicit_status
|
||||
|
||||
device_type = (ouv.get('device_type') or 'evc').lower()
|
||||
q = ouv.get('questions') or {}
|
||||
if not isinstance(q, dict):
|
||||
return explicit_status if explicit_status in ('damaged', 'non_compliant', 'compliant') else 'compliant'
|
||||
|
||||
def is_val(key, *expected):
|
||||
v = q.get(key)
|
||||
if isinstance(v, bool):
|
||||
v_str = 'true' if v else 'false'
|
||||
elif v is None:
|
||||
v_str = 'null'
|
||||
else:
|
||||
v_str = str(v).strip().lower()
|
||||
return v_str in [str(e).strip().lower() for e in expected]
|
||||
|
||||
has_damaged = False
|
||||
has_non_compliant = False
|
||||
|
||||
if device_type == 'evc':
|
||||
# 1. Point d'entrée
|
||||
if is_val('evc_pe_ok', 'false', 'non'):
|
||||
has_non_compliant = True
|
||||
|
||||
# 2. Empierrement
|
||||
if is_val('evc_empierrement_present', True, 'true'):
|
||||
if is_val('evc_empierrement_ok', 'false', 'non'):
|
||||
has_non_compliant = True
|
||||
if is_val('evc_empierrement_emporte', True, 'true'):
|
||||
has_damaged = True
|
||||
|
||||
# 3. Végétation
|
||||
if is_val('evc_ev_veg_etat', 'mauvais'):
|
||||
has_non_compliant = True
|
||||
if is_val('evc_ev_plantes_mortes', 'plus_10', '+ de 10%'):
|
||||
has_damaged = True
|
||||
|
||||
# 4. Chenaux d'érosion
|
||||
if is_val('evc_ev_chenaux_erosion', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
|
||||
# 5. Flaques d'eau
|
||||
if is_val('evc_ev_flaques', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
|
||||
# 6. Sédiments
|
||||
if is_val('evc_ev_sediments', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
if is_val('evc_ev_croute_sediments', 'true', 'oui', True):
|
||||
has_damaged = True
|
||||
|
||||
# 7. Espèces envahissantes
|
||||
if is_val('evc_ev_envahissantes', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
|
||||
# 8. Surverse
|
||||
if is_val('evc_surverse_presente', True, 'true'):
|
||||
if is_val('evc_sv_grille_propre', 'false', 'non'):
|
||||
has_non_compliant = True
|
||||
if is_val('evc_sv_decantation_vide', 'false', 'non'):
|
||||
has_non_compliant = True
|
||||
if is_val('evc_sv_haute', 'false', 'non'):
|
||||
has_damaged = True
|
||||
|
||||
elif device_type == 'oed':
|
||||
# 1. Revêtement général
|
||||
if is_val('oed_revetement_ok', 'false', 'non'):
|
||||
has_non_compliant = True
|
||||
|
||||
# 2. Bouche d'injection
|
||||
if is_val('oed_injection_presente', True, 'true'):
|
||||
if is_val('oed_bi_niveaux_ok', 'false', 'non'):
|
||||
has_damaged = True
|
||||
if is_val('oed_bi_grille_propre', 'false', 'non'):
|
||||
has_non_compliant = True
|
||||
if is_val('oed_bi_decantation_vide', 'false', 'non'):
|
||||
has_damaged = True
|
||||
if is_val('oed_bi_drain_colmate', 'true', 'oui', True):
|
||||
has_damaged = True
|
||||
|
||||
# 3. Structure enherbée / minérale
|
||||
if is_val('oed_rhm_presente', True, 'true'):
|
||||
if is_val('oed_rhm_stagne', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
if is_val('oed_rhm_joints_ok', 'false', 'non'):
|
||||
has_non_compliant = True
|
||||
if is_val('oed_rhm_joints_fines', 'true', 'oui', True):
|
||||
has_damaged = True
|
||||
if is_val('oed_rhm_joints_remplissage', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
|
||||
# 4. Modulaire poreux / continu poreux
|
||||
if is_val('oed_rp_presente', True, 'true'):
|
||||
if is_val('oed_rp_stagne', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
if is_val('oed_rp_depots', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
|
||||
elif device_type == 'ta':
|
||||
# 1. Point d'entrée
|
||||
if is_val('ta_pe_ok', 'false', 'non'):
|
||||
has_non_compliant = True
|
||||
if is_val('ta_pe_conduits_ok', 'false', 'non'):
|
||||
has_damaged = True
|
||||
if is_val('ta_pe_decantation_vide', 'false', 'non'):
|
||||
has_non_compliant = True
|
||||
|
||||
# 2. Bassin d'orage
|
||||
if is_val('ta_bo_vegetation_etat', 'mauvais'):
|
||||
has_non_compliant = True
|
||||
if is_val('ta_bo_plantes_mortes', 'plus_10', '+ de 10%'):
|
||||
has_damaged = True
|
||||
if is_val('ta_bo_chenaux', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
if is_val('ta_sediments', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
if is_val('ta_sediments_rempli', 'true', 'oui', True):
|
||||
has_damaged = True
|
||||
if is_val('ta_bo_envahissantes', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
|
||||
if has_damaged:
|
||||
return 'damaged'
|
||||
if has_non_compliant:
|
||||
return 'non_compliant'
|
||||
return explicit_status if explicit_status in ('damaged', 'non_compliant', 'compliant') else 'compliant'
|
||||
|
||||
@classmethod
|
||||
def compute_status_from_data(cls, data):
|
||||
"""
|
||||
|
|
@ -810,122 +961,11 @@ class GiepInspection(Inspection):
|
|||
for ouv in ouvrages:
|
||||
if not isinstance(ouv, dict):
|
||||
continue
|
||||
device_type = (ouv.get('device_type') or 'evc').lower()
|
||||
q = ouv.get('questions') or {}
|
||||
if not isinstance(q, dict):
|
||||
continue
|
||||
|
||||
def is_val(key, *expected):
|
||||
v = q.get(key)
|
||||
if isinstance(v, bool):
|
||||
v_str = 'true' if v else 'false'
|
||||
elif v is None:
|
||||
v_str = 'null'
|
||||
else:
|
||||
v_str = str(v).strip().lower()
|
||||
return v_str in [str(e).strip().lower() for e in expected]
|
||||
|
||||
if device_type == 'evc':
|
||||
# 1. Point d'entrée
|
||||
if is_val('evc_pe_ok', 'false', 'non'):
|
||||
has_non_compliant = True
|
||||
|
||||
# 2. Empierrement
|
||||
if is_val('evc_empierrement_present', True, 'true'):
|
||||
if is_val('evc_empierrement_ok', 'false', 'non'):
|
||||
has_non_compliant = True
|
||||
if is_val('evc_empierrement_emporte', True, 'true'):
|
||||
has_damaged = True
|
||||
|
||||
# 3. Végétation
|
||||
if is_val('evc_ev_veg_etat', 'mauvais'):
|
||||
has_non_compliant = True
|
||||
if is_val('evc_ev_plantes_mortes', 'plus_10', '+ de 10%'):
|
||||
has_damaged = True
|
||||
|
||||
# 4. Chenaux d'érosion
|
||||
if is_val('evc_ev_chenaux_erosion', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
|
||||
# 5. Flaques d'eau
|
||||
if is_val('evc_ev_flaques', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
|
||||
# 6. Sédiments
|
||||
if is_val('evc_ev_sediments', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
if is_val('evc_ev_croute_sediments', 'true', 'oui', True):
|
||||
has_damaged = True
|
||||
|
||||
# 7. Espèces envahissantes
|
||||
if is_val('evc_ev_envahissantes', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
|
||||
# 8. Surverse
|
||||
if is_val('evc_surverse_presente', True, 'true'):
|
||||
if is_val('evc_sv_grille_propre', 'false', 'non'):
|
||||
has_non_compliant = True
|
||||
if is_val('evc_sv_decantation_vide', 'false', 'non'):
|
||||
has_non_compliant = True
|
||||
if is_val('evc_sv_haute', 'false', 'non'):
|
||||
has_damaged = True
|
||||
|
||||
elif device_type == 'oed':
|
||||
# 1. Revêtement général
|
||||
if is_val('oed_revetement_ok', 'false', 'non'):
|
||||
has_non_compliant = True
|
||||
|
||||
# 2. Bouche d'injection
|
||||
if is_val('oed_injection_presente', True, 'true'):
|
||||
if is_val('oed_bi_niveaux_ok', 'false', 'non'):
|
||||
has_damaged = True
|
||||
if is_val('oed_bi_grille_propre', 'false', 'non'):
|
||||
has_non_compliant = True
|
||||
if is_val('oed_bi_decantation_vide', 'false', 'non'):
|
||||
has_damaged = True
|
||||
if is_val('oed_bi_drain_colmate', 'true', 'oui', True):
|
||||
has_damaged = True
|
||||
|
||||
# 3. Structure enherbée / minérale
|
||||
if is_val('oed_rhm_presente', True, 'true'):
|
||||
if is_val('oed_rhm_stagne', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
if is_val('oed_rhm_joints_ok', 'false', 'non'):
|
||||
has_non_compliant = True
|
||||
if is_val('oed_rhm_joints_fines', 'true', 'oui', True):
|
||||
has_damaged = True
|
||||
if is_val('oed_rhm_joints_remplissage', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
|
||||
# 4. Modulaire poreux / continu poreux
|
||||
if is_val('oed_rp_presente', True, 'true'):
|
||||
if is_val('oed_rp_stagne', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
if is_val('oed_rp_depots', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
|
||||
elif device_type == 'ta':
|
||||
# 1. Point d'entrée
|
||||
if is_val('ta_pe_ok', 'false', 'non'):
|
||||
has_non_compliant = True
|
||||
if is_val('ta_pe_conduits_ok', 'false', 'non'):
|
||||
has_damaged = True
|
||||
if is_val('ta_pe_decantation_vide', 'false', 'non'):
|
||||
has_non_compliant = True
|
||||
|
||||
# 2. Bassin d'orage
|
||||
if is_val('ta_bo_vegetation_etat', 'mauvais'):
|
||||
has_non_compliant = True
|
||||
if is_val('ta_bo_plantes_mortes', 'plus_10', '+ de 10%'):
|
||||
has_damaged = True
|
||||
if is_val('ta_bo_chenaux', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
if is_val('ta_sediments', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
if is_val('ta_sediments_rempli', 'true', 'oui', True):
|
||||
has_damaged = True
|
||||
if is_val('ta_bo_envahissantes', 'true', 'oui', True):
|
||||
has_non_compliant = True
|
||||
st = cls.compute_status_for_single_ouvrage(ouv)
|
||||
if st == 'damaged':
|
||||
has_damaged = True
|
||||
elif st == 'non_compliant':
|
||||
has_non_compliant = True
|
||||
|
||||
if has_damaged:
|
||||
return 'damaged'
|
||||
|
|
@ -934,6 +974,7 @@ class GiepInspection(Inspection):
|
|||
return 'compliant'
|
||||
|
||||
|
||||
|
||||
class SignPanelInspection(Inspection):
|
||||
"""
|
||||
Spécialisation de l'inspection pour les panneaux de signalisation routière.
|
||||
|
|
|
|||
|
|
@ -73,11 +73,45 @@
|
|||
<span class="badge bg-light text-dark border px-2 py-1">{{ ouv.name }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 1. Conformité badge -->
|
||||
<div class="d-flex align-items-center gap-1.5 ms-auto">
|
||||
<span class="badge {{ ouv|ouvrage_result_badge }} px-2.5 py-1.5 fs-6 shadow-sm">
|
||||
<i class="bi {{ ouv|ouvrage_result_icon }} me-1"></i>{{ ouv|ouvrage_result_label }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-3 bg-light-subtle">
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- 2. Photos dédiées à cet ouvrage -->
|
||||
{% with ouv_docs=insp.documents.all|filter_ouvrage_docs:ouv %}
|
||||
{% if ouv_docs %}
|
||||
<div class="col-12">
|
||||
<div class="border rounded-3 p-3 bg-white shadow-sm border-success-subtle">
|
||||
<h6 class="fw-bold mb-2 text-success small text-uppercase border-bottom pb-1">
|
||||
<i class="bi bi-camera me-1"></i>{% translate "Photos de l'ouvrage" %} ({{ ouv_docs|length }})
|
||||
</h6>
|
||||
<div class="d-flex flex-wrap gap-2 pt-1">
|
||||
{% for doc in ouv_docs %}
|
||||
<a href="{{ doc.file_url }}" target="_blank" class="border rounded overflow-hidden shadow-sm d-inline-block position-relative" style="width: 100px; height: 80px;" title="{{ doc.filename }}">
|
||||
{% if doc.is_image %}
|
||||
<img src="{{ doc.file_url }}" class="w-100 h-100" style="object-fit: cover;" alt="{{ doc.filename }}">
|
||||
{% else %}
|
||||
<div class="d-flex flex-column align-items-center justify-content-center w-100 h-100 bg-light text-muted p-1">
|
||||
<i class="bi bi-file-earmark-text fs-3 text-secondary"></i>
|
||||
<span class="extra-small text-truncate" style="max-width: 90px;">{{ doc.filename }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- ── 1. EVC (Espace Vert Creux) BLOCS ── -->
|
||||
{% if ouv.device_type == 'evc' %}
|
||||
<!-- Bloc EVC: Point d'entrée -->
|
||||
|
|
@ -357,6 +391,17 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!-- 4. Remarques sur cet ouvrage -->
|
||||
{% if ouv.notes %}
|
||||
<div class="col-12">
|
||||
<div class="border rounded-3 p-3 bg-white shadow-sm border-secondary-subtle">
|
||||
<h6 class="fw-bold mb-1.5 text-secondary small text-uppercase border-bottom pb-1">
|
||||
<i class="bi bi-chat-left-text me-1 text-primary"></i>{% translate "Remarques sur cet ouvrage" %}
|
||||
</h6>
|
||||
<p class="small text-dark mb-0" style="white-space: pre-wrap;">{{ ouv.notes }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -366,6 +411,31 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Photos générales / zone GIEP -->
|
||||
{% with gen_docs=insp.documents.all|filter_general_docs %}
|
||||
{% if gen_docs %}
|
||||
<div class="card border border-secondary-subtle bg-white p-3 mb-3 shadow-sm rounded-3">
|
||||
<h6 class="fw-bold mb-2 text-secondary small text-uppercase border-bottom pb-1">
|
||||
<i class="bi bi-images me-1 text-primary"></i>{% translate "Photos générales de la zone" %} ({{ gen_docs|length }})
|
||||
</h6>
|
||||
<div class="d-flex flex-wrap gap-2 pt-1">
|
||||
{% for doc in gen_docs %}
|
||||
<a href="{{ doc.file_url }}" target="_blank" class="border rounded overflow-hidden shadow-sm d-inline-block position-relative" style="width: 100px; height: 80px;" title="{{ doc.filename }}">
|
||||
{% if doc.is_image %}
|
||||
<img src="{{ doc.file_url }}" class="w-100 h-100" style="object-fit: cover;" alt="{{ doc.filename }}">
|
||||
{% else %}
|
||||
<div class="d-flex flex-column align-items-center justify-content-center w-100 h-100 bg-light text-muted p-1">
|
||||
<i class="bi bi-file-earmark-text fs-3 text-secondary"></i>
|
||||
<span class="extra-small text-truncate" style="max-width: 90px;">{{ doc.filename }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
<!-- Hidden Field pour la sérialisation JSON -->
|
||||
<input type="hidden" name="giep_data" id="giep_data_serialized" value="{}">
|
||||
<div id="giep_hidden_file_inputs" class="d-none"></div>
|
||||
|
||||
<!-- Bandeau de statut automatique GIEP -->
|
||||
<div id="giep_status_auto_banner" class="giep-status-auto-banner alert alert-info border border-info-subtle rounded-3 p-2.5 mb-3 shadow-sm d-none"></div>
|
||||
|
|
@ -178,12 +179,17 @@
|
|||
|
||||
function createEmptyOuvrage(idx, deviceType = "evc", asset = null) {
|
||||
return {
|
||||
local_id: String(idx),
|
||||
local_id: 'ouv_' + Date.now() + '_' + Math.random().toString(36).substr(2, 4),
|
||||
device_type: deviceType,
|
||||
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,
|
||||
result_status: 'compliant',
|
||||
is_result_manual: false,
|
||||
notes: '',
|
||||
newFiles: [],
|
||||
existingDocs: [],
|
||||
questions: {
|
||||
// EVC
|
||||
evc_pe_ok: null,
|
||||
|
|
@ -510,20 +516,105 @@
|
|||
renderOuvrages();
|
||||
}
|
||||
|
||||
function syncHiddenFileInputs() {
|
||||
const hiddenContainer = rootModal.querySelector('#giep_hidden_file_inputs') || document.getElementById('giep_hidden_file_inputs');
|
||||
if (!hiddenContainer) return;
|
||||
hiddenContainer.innerHTML = '';
|
||||
|
||||
giepOuvrages.forEach((ouv, idx) => {
|
||||
const localId = ouv.local_id || String(idx);
|
||||
(ouv.newFiles || []).forEach(file => {
|
||||
try {
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(file);
|
||||
const fileInp = document.createElement('input');
|
||||
fileInp.type = 'file';
|
||||
fileInp.name = 'photos';
|
||||
fileInp.files = dt.files;
|
||||
hiddenContainer.appendChild(fileInp);
|
||||
|
||||
const keyInp = document.createElement('input');
|
||||
keyInp.type = 'hidden';
|
||||
keyInp.name = 'field_keys';
|
||||
keyInp.value = 'ouvrage_' + localId;
|
||||
hiddenContainer.appendChild(keyInp);
|
||||
} catch (err) {
|
||||
console.warn('DataTransfer error:', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toggleParentGlobalSections(isGiep) {
|
||||
const sectionsToToggle = [
|
||||
rootModal.querySelector('#quickInspResultSection'),
|
||||
rootModal.querySelector('#editInspResultSection'),
|
||||
rootModal.querySelector('#quickInspPhotosSection'),
|
||||
rootModal.querySelector('#editInspPhotosSection'),
|
||||
rootModal.querySelector('#quickInspNotesSection'),
|
||||
rootModal.querySelector('#editInspNotesSection')
|
||||
];
|
||||
sectionsToToggle.forEach(sec => {
|
||||
if (sec) {
|
||||
if (isGiep) sec.classList.add('d-none');
|
||||
else sec.classList.remove('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
const editExistingDocs = rootModal.querySelector('#editInspectionExistingDocsContainer');
|
||||
if (editExistingDocs && isGiep) {
|
||||
editExistingDocs.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
function updateSerializedGiepData() {
|
||||
const dateRainEl = rootModal.querySelector('#giep_date_derniere_pluie, [name="date_derniere_pluie"]');
|
||||
const stormEl = rootModal.querySelector('#giep_orage_moins_une_heure, [name="orage_moins_une_heure"]');
|
||||
const pluieVal = dateRainEl?.value || 'null';
|
||||
const orageVal = stormEl?.value || 'null';
|
||||
|
||||
// Auto-calcul pour chaque ouvrage si non manuel
|
||||
giepOuvrages.forEach(ouv => {
|
||||
if (!ouv.is_result_manual) {
|
||||
ouv.result_status = evaluateSingleOuvrageStatus(ouv);
|
||||
}
|
||||
});
|
||||
|
||||
// Consolidation des remarques par ouvrage dans le champ notes global de la modale
|
||||
const notesEl = rootModal.querySelector('textarea[name="notes"]');
|
||||
if (notesEl) {
|
||||
const consolidatedNotes = giepOuvrages
|
||||
.map((o, i) => {
|
||||
const title = o.asset_code || `Ouvrage #${i + 1}`;
|
||||
return o.notes && o.notes.trim() ? `[${title}] : ${o.notes.trim()}` : '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n\n');
|
||||
notesEl.value = consolidatedNotes;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
ouvrages: giepOuvrages,
|
||||
ouvrages: giepOuvrages.map((o, i) => ({
|
||||
local_id: o.local_id || String(i),
|
||||
device_type: o.device_type,
|
||||
name: o.name,
|
||||
asset_id: o.asset_id,
|
||||
asset_code: o.asset_code,
|
||||
asset_name: o.asset_name,
|
||||
result_status: o.result_status || 'compliant',
|
||||
is_result_manual: Boolean(o.is_result_manual),
|
||||
notes: o.notes || '',
|
||||
questions: o.questions || {},
|
||||
actions: o.actions || {}
|
||||
})),
|
||||
date_derniere_pluie: pluieVal,
|
||||
orage_moins_une_heure: orageVal === 'true' ? true : (orageVal === 'false' ? false : null)
|
||||
};
|
||||
if (serializedInput) {
|
||||
serializedInput.value = JSON.stringify(payload);
|
||||
}
|
||||
|
||||
syncHiddenFileInputs();
|
||||
}
|
||||
|
||||
// Liste des clés de questions où "Oui" représente une anomalie / dysfonctionnement
|
||||
|
|
@ -623,6 +714,196 @@
|
|||
return false;
|
||||
}
|
||||
|
||||
function evaluateSingleOuvrageStatus(ouv) {
|
||||
if (!ouv) return 'compliant';
|
||||
if (ouv.is_result_manual && ouv.result_status) {
|
||||
return ouv.result_status;
|
||||
}
|
||||
|
||||
const deviceType = (ouv.device_type || 'evc').toLowerCase();
|
||||
const q = ouv.questions || {};
|
||||
|
||||
const isVal = (key, ...expected) => {
|
||||
const v = q[key];
|
||||
if (v === undefined || v === null) return expected.includes('null');
|
||||
const vStr = String(v).trim().toLowerCase();
|
||||
return expected.map(e => String(e).trim().toLowerCase()).includes(vStr);
|
||||
};
|
||||
|
||||
let hasDamaged = false;
|
||||
let hasNonCompliant = false;
|
||||
|
||||
if (deviceType === 'evc') {
|
||||
// 1. Point d'entrée
|
||||
if (isVal('evc_pe_ok', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
|
||||
// 2. Empierrement
|
||||
if (isVal('evc_empierrement_present', true, 'true')) {
|
||||
if (isVal('evc_empierrement_ok', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
if (isVal('evc_empierrement_emporte', true, 'true')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Végétation
|
||||
if (isVal('evc_ev_veg_etat', 'mauvais')) {
|
||||
hasNonCompliant = true;
|
||||
if (isVal('evc_ev_plantes_mortes', 'plus_10', '+ de 10%')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Chenaux d'érosion
|
||||
if (isVal('evc_ev_chenaux_erosion', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
|
||||
// 5. Flaques
|
||||
if (isVal('evc_ev_flaques', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
|
||||
// 6. Sédiments
|
||||
if (isVal('evc_ev_sediments', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
if (isVal('evc_ev_croute_sediments', true, 'true', 'oui')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Espèces envahissantes
|
||||
if (isVal('evc_ev_envahissantes', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
|
||||
// 8. Surverse
|
||||
if (isVal('evc_surverse_presente', true, 'true')) {
|
||||
if (isVal('evc_sv_grille_propre', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
if (isVal('evc_sv_decantation_vide', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
if (isVal('evc_sv_haute', 'false', 'non')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (deviceType === 'oed') {
|
||||
// 1. Revêtement
|
||||
if (isVal('oed_revetement_ok', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
if (isVal('oed_revetement_ornieres', true, 'true') || isVal('oed_revetement_fissures', true, 'true') || isVal('oed_revetement_trous', true, 'true')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Bouche d'injection
|
||||
if (isVal('oed_injection_presente', true, 'true')) {
|
||||
if (isVal('oed_bi_niveaux_ok', 'false', 'non')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
if (isVal('oed_bi_grille_propre', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
if (isVal('oed_bi_decantation_vide', 'false', 'non')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
if (isVal('oed_bi_drain_colmate', true, 'true', 'oui')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Structure enherbée / minérale
|
||||
if (isVal('oed_rhm_presente', true, 'true')) {
|
||||
if (isVal('oed_rhm_stagne', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
if (isVal('oed_rhm_stagne_plus_2h', true, 'true', 'oui')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
if (isVal('oed_rhm_joints_ok', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
if (isVal('oed_rhm_joints_fines', true, 'true', 'oui')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
if (isVal('oed_rhm_joints_remplissage', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Modulaire poreux / continu poreux
|
||||
if (isVal('oed_rp_presente', true, 'true')) {
|
||||
if (isVal('oed_rp_stagne', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
if (isVal('oed_rp_stagne_plus_2h', true, 'true', 'oui')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
if (isVal('oed_rp_depots', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Matériaux non agrégés
|
||||
if (isVal('oed_rna_presente', true, 'true')) {
|
||||
if (isVal('oed_rna_stagne', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
if (isVal('oed_rna_stagne_plus_2h', true, 'true', 'oui')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
if (isVal('oed_rna_matiere_ok', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
if (isVal('oed_rna_geotextile_visible', true, 'true', 'oui')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (deviceType === 'ta') {
|
||||
// 1. Point d'entrée
|
||||
if (isVal('ta_pe_ok', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
if (isVal('ta_pe_conduits_ok', 'false', 'non')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
if (isVal('ta_pe_decantation_vide', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
|
||||
// 2. Bassin d'orage
|
||||
if (isVal('ta_bo_vegetation_etat', 'mauvais')) {
|
||||
hasNonCompliant = true;
|
||||
if (isVal('ta_bo_plantes_mortes', 'plus_10', '+ de 10%')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
if (isVal('ta_bo_chenaux', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
if (isVal('ta_sediments', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
if (isVal('ta_sediments_rempli', true, 'true', 'oui')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
if (isVal('ta_bo_envahissantes', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasDamaged) return 'damaged';
|
||||
if (hasNonCompliant) return 'non_compliant';
|
||||
return 'compliant';
|
||||
}
|
||||
|
||||
function evaluateGiepInspectionStatus(ouvrages) {
|
||||
if (!Array.isArray(ouvrages) || ouvrages.length === 0) {
|
||||
return null;
|
||||
|
|
@ -636,156 +917,9 @@
|
|||
|
||||
for (const ouv of ouvrages) {
|
||||
if (!ouv) continue;
|
||||
const deviceType = (ouv.device_type || 'evc').toLowerCase();
|
||||
const q = ouv.questions || {};
|
||||
|
||||
const isVal = (key, ...expected) => {
|
||||
const v = q[key];
|
||||
if (v === undefined || v === null) return expected.includes('null');
|
||||
const vStr = String(v).trim().toLowerCase();
|
||||
return expected.map(e => String(e).trim().toLowerCase()).includes(vStr);
|
||||
};
|
||||
|
||||
if (deviceType === 'evc') {
|
||||
// 1. Point d'entrée
|
||||
if (isVal('evc_pe_ok', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
|
||||
// 2. Empierrement
|
||||
if (isVal('evc_empierrement_present', true, 'true')) {
|
||||
if (isVal('evc_empierrement_ok', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
if (isVal('evc_empierrement_emporte', true, 'true')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Végétation
|
||||
if (isVal('evc_ev_veg_etat', 'mauvais')) {
|
||||
hasNonCompliant = true;
|
||||
if (isVal('evc_ev_plantes_mortes', 'plus_10', '+ de 10%')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Chenaux d'érosion
|
||||
if (isVal('evc_ev_chenaux_erosion', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
|
||||
// 5. Flaques
|
||||
if (isVal('evc_ev_flaques', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
|
||||
// 6. Sédiments
|
||||
if (isVal('evc_ev_sediments', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
if (isVal('evc_ev_croute_sediments', true, 'true', 'oui')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Espèces envahissantes
|
||||
if (isVal('evc_ev_envahissantes', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
|
||||
// 8. Surverse
|
||||
if (isVal('evc_surverse_presente', true, 'true')) {
|
||||
if (isVal('evc_sv_grille_propre', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
if (isVal('evc_sv_decantation_vide', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
if (isVal('evc_sv_haute', 'false', 'non')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (deviceType === 'oed') {
|
||||
// 1. Revêtement
|
||||
if (isVal('oed_revetement_ok', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
|
||||
// 2. Bouche d'injection
|
||||
if (isVal('oed_injection_presente', true, 'true')) {
|
||||
if (isVal('oed_bi_niveaux_ok', 'false', 'non')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
if (isVal('oed_bi_grille_propre', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
if (isVal('oed_bi_decantation_vide', 'false', 'non')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
if (isVal('oed_bi_drain_colmate', true, 'true', 'oui')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Structure enherbée / minérale
|
||||
if (isVal('oed_rhm_presente', true, 'true')) {
|
||||
if (isVal('oed_rhm_stagne', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
if (isVal('oed_rhm_joints_ok', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
if (isVal('oed_rhm_joints_fines', true, 'true', 'oui')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
if (isVal('oed_rhm_joints_remplissage', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Modulaire poreux / continu poreux
|
||||
if (isVal('oed_rp_presente', true, 'true')) {
|
||||
if (isVal('oed_rp_stagne', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
if (isVal('oed_rp_depots', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (deviceType === 'ta') {
|
||||
// 1. Point d'entrée
|
||||
if (isVal('ta_pe_ok', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
if (isVal('ta_pe_conduits_ok', 'false', 'non')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
if (isVal('ta_pe_decantation_vide', 'false', 'non')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
|
||||
// 2. Bassin d'orage
|
||||
if (isVal('ta_bo_vegetation_etat', 'mauvais')) {
|
||||
hasNonCompliant = true;
|
||||
if (isVal('ta_bo_plantes_mortes', 'plus_10', '+ de 10%')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
if (isVal('ta_bo_chenaux', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
if (isVal('ta_sediments', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
if (isVal('ta_sediments_rempli', true, 'true', 'oui')) {
|
||||
hasDamaged = true;
|
||||
}
|
||||
}
|
||||
if (isVal('ta_bo_envahissantes', true, 'true', 'oui')) {
|
||||
hasNonCompliant = true;
|
||||
}
|
||||
}
|
||||
const st = evaluateSingleOuvrageStatus(ouv);
|
||||
if (st === 'damaged') hasDamaged = true;
|
||||
else if (st === 'non_compliant') hasNonCompliant = true;
|
||||
}
|
||||
|
||||
if (hasDamaged) return 'damaged';
|
||||
|
|
@ -1405,19 +1539,74 @@
|
|||
`;
|
||||
}
|
||||
|
||||
const titleHeader = ouv.asset_code ? `${ouv.asset_code} — ${ouv.asset_name || 'Ouvrage #' + (idx + 1)}` : `Ouvrage #${idx + 1}`;
|
||||
const titleHeader = ouv.asset_code ? `${ouv.asset_code} — ${ouv.asset_name || 'Ouvrage #' + (idx + 1)}` : (ouv.name || `Ouvrage #${idx + 1}`);
|
||||
const currentStatus = ouv.is_result_manual ? (ouv.result_status || 'compliant') : evaluateSingleOuvrageStatus(ouv);
|
||||
ouv.result_status = currentStatus;
|
||||
|
||||
// Photos previews HTML
|
||||
const existingDocs = ouv.existingDocs || [];
|
||||
const newFiles = ouv.newFiles || [];
|
||||
const totalOuvPhotos = existingDocs.length + newFiles.length;
|
||||
|
||||
let photosPreviewsHtml = '';
|
||||
existingDocs.forEach(doc => {
|
||||
let docPreview = '';
|
||||
if (doc.is_image) {
|
||||
docPreview = `<img src="${doc.url}" class="rounded w-100 h-100" style="object-fit: cover;" alt="${doc.filename || ''}">`;
|
||||
} else {
|
||||
docPreview = `<div class="d-flex flex-column align-items-center justify-content-center w-100 h-100 bg-light text-muted p-1"><i class="bi bi-file-earmark-text fs-4"></i><span class="extra-small text-truncate" style="max-width: 65px;">${doc.filename || ''}</span></div>`;
|
||||
}
|
||||
photosPreviewsHtml += `
|
||||
<div class="position-relative border rounded p-0.5 bg-white shadow-sm" style="width: 75px; height: 65px;">
|
||||
<a href="${doc.url}" target="_blank" class="d-block w-100 h-100">${docPreview}</a>
|
||||
<button type="button" class="btn btn-danger btn-sm p-0 position-absolute top-0 end-0 rounded-circle delete-existing-doc-btn"
|
||||
style="width: 20px; height: 20px; line-height: 1; transform: translate(30%, -30%);"
|
||||
data-idx="${idx}" data-doc-id="${doc.id}" title="{% translate 'Supprimer ce document' %}">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
newFiles.forEach((file, fileIdx) => {
|
||||
const isImg = file.type && file.type.startsWith('image/');
|
||||
const fileUrl = isImg ? URL.createObjectURL(file) : '';
|
||||
let filePreview = '';
|
||||
if (isImg) {
|
||||
filePreview = `<img src="${fileUrl}" class="rounded w-100 h-100" style="object-fit: cover;" alt="${file.name}">`;
|
||||
} else {
|
||||
filePreview = `<div class="d-flex flex-column align-items-center justify-content-center w-100 h-100 bg-light text-muted p-1"><i class="bi bi-file-earmark fs-4"></i><span class="extra-small text-truncate" style="max-width: 65px;">${file.name}</span></div>`;
|
||||
}
|
||||
photosPreviewsHtml += `
|
||||
<div class="position-relative border rounded p-0.5 bg-white shadow-sm" style="width: 75px; height: 65px;">
|
||||
<div class="w-100 h-100">${filePreview}</div>
|
||||
<button type="button" class="btn btn-danger btn-sm p-0 position-absolute top-0 end-0 rounded-circle remove-new-file-btn"
|
||||
style="width: 20px; height: 20px; line-height: 1; transform: translate(30%, -30%);"
|
||||
data-idx="${idx}" data-file-idx="${fileIdx}" title="{% translate 'Supprimer cette photo' %}">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
if (totalOuvPhotos === 0) {
|
||||
photosPreviewsHtml = '<div class="extra-small text-muted fst-italic py-1">{% translate "Aucune photo pour cet ouvrage" %}</div>';
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 border-bottom pb-2">
|
||||
<span class="fw-bold text-dark"><i class="bi bi-gear-fill me-1.5 text-primary"></i>${titleHeader}</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge bg-secondary-subtle text-secondary px-2 py-1 fs-6">#${idx + 1}</span>
|
||||
<span class="fw-bold text-dark fs-6">${titleHeader}</span>
|
||||
</div>
|
||||
<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">
|
||||
<div class="border rounded-3 p-2.5 mb-3 bg-white border-primary-subtle shadow-sm">
|
||||
<div class="d-flex align-items-center justify-content-between mb-1.5">
|
||||
<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>
|
||||
|
|
@ -1471,35 +1660,113 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Questions Block -->
|
||||
${dynamicQuestionsHtml}
|
||||
|
||||
<!-- Recommandations -->
|
||||
<div class="border-top pt-2">
|
||||
<label class="form-label extra-small text-muted fw-bold mb-2 d-block"><i class="bi bi-bookmark-star me-1"></i>{% translate "Recommandations" %}</label>
|
||||
<div class="d-flex flex-column gap-1.5">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input action-check" type="checkbox" data-idx="${idx}" data-act="aucune" id="act_auc_${idx}" ${act.aucune ? 'checked' : ''}>
|
||||
<label class="form-check-label extra-small" for="act_auc_${idx}">{% translate "Aucune" %}</label>
|
||||
<!-- 1. Conformité de l'ouvrage -->
|
||||
<div class="border rounded-3 p-3 mb-3 bg-white shadow-sm border-info-subtle">
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<label class="form-label small fw-bold text-dark mb-0">
|
||||
<i class="bi bi-shield-check me-1 text-primary"></i>{% translate "1. Conformité de l'ouvrage" %}
|
||||
</label>
|
||||
${ouv.is_result_manual ? `
|
||||
<button type="button" class="btn btn-link text-decoration-none text-muted p-0 extra-small reset-ouv-status-btn" data-idx="${idx}">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>{% translate "Rétablir auto" %}
|
||||
</button>
|
||||
` : `
|
||||
<span class="badge bg-secondary-subtle text-secondary border extra-small">
|
||||
<i class="bi bi-magic me-1"></i>{% translate "Calcul auto" %}
|
||||
</span>
|
||||
`}
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-4">
|
||||
<button type="button" class="btn w-100 py-2 d-flex flex-column align-items-center justify-content-center gap-1 ouv-status-btn ${currentStatus === 'compliant' ? 'btn-success text-white shadow-sm' : 'btn-outline-success'}" data-idx="${idx}" data-status="compliant">
|
||||
<i class="bi bi-check-circle-fill fs-5"></i>
|
||||
<span class="extra-small fw-bold">{% translate "Conforme" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input action-check" type="checkbox" data-idx="${idx}" data-act="interventions_preventives" id="act_prev_${idx}" ${act.interventions_preventives || act.renforcement_preventif ? 'checked' : ''}>
|
||||
<label class="form-check-label extra-small" for="act_prev_${idx}">{% translate "Renforcement de l’entretien préventif" %}</label>
|
||||
<div class="col-4">
|
||||
<button type="button" class="btn w-100 py-2 d-flex flex-column align-items-center justify-content-center gap-1 ouv-status-btn ${currentStatus === 'non_compliant' ? 'btn-warning text-dark shadow-sm' : 'btn-outline-warning'}" data-idx="${idx}" data-status="non_compliant">
|
||||
<i class="bi bi-exclamation-triangle-fill fs-5"></i>
|
||||
<span class="extra-small fw-bold">{% translate "Non conforme" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input action-check" type="checkbox" data-idx="${idx}" data-act="entretien_curatif" id="act_cur_${idx}" ${act.entretien_curatif ? 'checked' : ''}>
|
||||
<label class="form-check-label extra-small" for="act_cur_${idx}">{% translate "Entretien curatif / Réparation / Adaptation nécessaire" %}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input action-check" type="checkbox" data-idx="${idx}" data-act="test_permeabilite" id="act_perm_${idx}" ${act.test_permeabilite ? 'checked' : ''}>
|
||||
<label class="form-check-label extra-small" for="act_perm_${idx}">{% translate "Test de perméabilité" %}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input action-check" type="checkbox" data-idx="${idx}" data-act="inspection" id="act_insp_${idx}" ${act.inspection || act.inspection_camera ? 'checked' : ''}>
|
||||
<label class="form-check-label extra-small" for="act_insp_${idx}">{% translate "Inspection caméra" %}</label>
|
||||
<div class="col-4">
|
||||
<button type="button" class="btn w-100 py-2 d-flex flex-column align-items-center justify-content-center gap-1 ouv-status-btn ${currentStatus === 'damaged' ? 'btn-danger text-white shadow-sm' : 'btn-outline-danger'}" data-idx="${idx}" data-status="damaged">
|
||||
<i class="bi bi-x-octagon-fill fs-5"></i>
|
||||
<span class="extra-small fw-bold">{% translate "Endommagé" %}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Photos de l'ouvrage -->
|
||||
<div class="border rounded-3 p-3 mb-3 bg-white shadow-sm border-success-subtle" style="background-color: #fafffc;">
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<label class="form-label small fw-bold text-dark mb-0">
|
||||
<i class="bi bi-camera me-1 text-success"></i>{% translate "2. Photos de l'ouvrage" %}
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle rounded-pill ms-1">${totalOuvPhotos}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<button type="button" class="btn btn-outline-success btn-sm flex-fill py-1.5 rounded-pill ouv-camera-btn" data-idx="${idx}">
|
||||
<i class="bi bi-camera-fill me-1"></i>{% translate "Prendre une photo" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm flex-fill py-1.5 rounded-pill ouv-gallery-btn" data-idx="${idx}">
|
||||
<i class="bi bi-folder2-open me-1"></i>{% translate "Fichiers / Galerie" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input type="file" accept="image/*" capture="environment" class="d-none ouv-camera-input" data-idx="${idx}">
|
||||
<input type="file" accept="image/*,.pdf,.doc,.docx,.xls,.xlsx" multiple class="d-none ouv-gallery-input" data-idx="${idx}">
|
||||
|
||||
<!-- Previews list -->
|
||||
<div class="d-flex flex-wrap gap-2 ouv-photos-preview-list pt-1" data-idx="${idx}">
|
||||
${photosPreviewsHtml}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Formulaire d'inspection -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold text-dark mb-2 d-block">
|
||||
<i class="bi bi-ui-checks me-1 text-primary"></i>{% translate "3. Formulaire d'inspection" %}
|
||||
</label>
|
||||
${dynamicQuestionsHtml}
|
||||
|
||||
<!-- Recommandations -->
|
||||
<div class="border rounded-3 p-3 bg-white shadow-sm mt-3">
|
||||
<label class="form-label extra-small text-muted fw-bold mb-2 d-block"><i class="bi bi-bookmark-star me-1"></i>{% translate "Recommandations" %}</label>
|
||||
<div class="d-flex flex-column gap-1.5">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input action-check" type="checkbox" data-idx="${idx}" data-act="aucune" id="act_auc_${idx}" ${act.aucune ? 'checked' : ''}>
|
||||
<label class="form-check-label extra-small" for="act_auc_${idx}">{% translate "Aucune" %}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input action-check" type="checkbox" data-idx="${idx}" data-act="interventions_preventives" id="act_prev_${idx}" ${act.interventions_preventives || act.renforcement_preventif ? 'checked' : ''}>
|
||||
<label class="form-check-label extra-small" for="act_prev_${idx}">{% translate "Renforcement de l’entretien préventif" %}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input action-check" type="checkbox" data-idx="${idx}" data-act="entretien_curatif" id="act_cur_${idx}" ${act.entretien_curatif ? 'checked' : ''}>
|
||||
<label class="form-check-label extra-small" for="act_cur_${idx}">{% translate "Entretien curatif / Réparation / Adaptation nécessaire" %}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input action-check" type="checkbox" data-idx="${idx}" data-act="test_permeabilite" id="act_perm_${idx}" ${act.test_permeabilite ? 'checked' : ''}>
|
||||
<label class="form-check-label extra-small" for="act_perm_${idx}">{% translate "Test de perméabilité" %}</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input action-check" type="checkbox" data-idx="${idx}" data-act="inspection" id="act_insp_${idx}" ${act.inspection || act.inspection_camera ? 'checked' : ''}>
|
||||
<label class="form-check-label extra-small" for="act_insp_${idx}">{% translate "Inspection caméra" %}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. Remarques sur cet ouvrage -->
|
||||
<div class="border rounded-3 p-3 bg-white shadow-sm border-secondary-subtle">
|
||||
<label class="form-label small fw-bold text-dark mb-1 d-block">
|
||||
<i class="bi bi-chat-left-text me-1 text-secondary"></i>{% translate "4. Remarques sur cet ouvrage" %}
|
||||
</label>
|
||||
<textarea class="form-control form-control-sm rounded-3 ouv-notes-input" data-idx="${idx}" rows="2" placeholder="{% filter escapejs %}{% translate "Observations ou remarques spécifiques à cet ouvrage..." %}{% endfilter %}">${ouv.notes || ''}</textarea>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(card);
|
||||
});
|
||||
|
|
@ -1574,6 +1841,100 @@
|
|||
});
|
||||
});
|
||||
|
||||
// Event listeners pour la conformité de l'ouvrage
|
||||
container.querySelectorAll('.ouv-status-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const idx = parseInt(e.currentTarget.getAttribute('data-idx'));
|
||||
const st = e.currentTarget.getAttribute('data-status');
|
||||
if (!giepOuvrages[idx]) return;
|
||||
giepOuvrages[idx].result_status = st;
|
||||
giepOuvrages[idx].is_result_manual = true;
|
||||
updateSerializedGiepData();
|
||||
syncGiepCalculatedStatus();
|
||||
renderOuvrages();
|
||||
});
|
||||
});
|
||||
|
||||
container.querySelectorAll('.reset-ouv-status-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const idx = parseInt(e.currentTarget.getAttribute('data-idx'));
|
||||
if (!giepOuvrages[idx]) return;
|
||||
giepOuvrages[idx].is_result_manual = false;
|
||||
giepOuvrages[idx].result_status = evaluateSingleOuvrageStatus(giepOuvrages[idx]);
|
||||
updateSerializedGiepData();
|
||||
syncGiepCalculatedStatus();
|
||||
renderOuvrages();
|
||||
});
|
||||
});
|
||||
|
||||
// Event listeners pour les photos de l'ouvrage
|
||||
container.querySelectorAll('.ouv-camera-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const idx = parseInt(e.currentTarget.getAttribute('data-idx'));
|
||||
const inp = container.querySelector(`.ouv-camera-input[data-idx="${idx}"]`);
|
||||
if (inp) inp.click();
|
||||
});
|
||||
});
|
||||
|
||||
container.querySelectorAll('.ouv-gallery-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const idx = parseInt(e.currentTarget.getAttribute('data-idx'));
|
||||
const inp = container.querySelector(`.ouv-gallery-input[data-idx="${idx}"]`);
|
||||
if (inp) inp.click();
|
||||
});
|
||||
});
|
||||
|
||||
container.querySelectorAll('.ouv-camera-input, .ouv-gallery-input').forEach(inp => {
|
||||
inp.addEventListener('change', (e) => {
|
||||
const idx = parseInt(e.target.getAttribute('data-idx'));
|
||||
if (!giepOuvrages[idx]) return;
|
||||
if (!giepOuvrages[idx].newFiles) giepOuvrages[idx].newFiles = [];
|
||||
Array.from(e.target.files).forEach(f => {
|
||||
giepOuvrages[idx].newFiles.push(f);
|
||||
});
|
||||
updateSerializedGiepData();
|
||||
renderOuvrages();
|
||||
});
|
||||
});
|
||||
|
||||
container.querySelectorAll('.remove-new-file-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const idx = parseInt(e.currentTarget.getAttribute('data-idx'));
|
||||
const fileIdx = parseInt(e.currentTarget.getAttribute('data-file-idx'));
|
||||
if (giepOuvrages[idx] && giepOuvrages[idx].newFiles) {
|
||||
giepOuvrages[idx].newFiles.splice(fileIdx, 1);
|
||||
updateSerializedGiepData();
|
||||
renderOuvrages();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
container.querySelectorAll('.delete-existing-doc-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const idx = parseInt(e.currentTarget.getAttribute('data-idx'));
|
||||
const docId = parseInt(e.currentTarget.getAttribute('data-doc-id'));
|
||||
if (confirm("{% filter escapejs %}{% translate "Supprimer définitivement ce document ?" %}{% endfilter %}")) {
|
||||
if (typeof window.deleteInspectionDoc === 'function') {
|
||||
window.deleteInspectionDoc(docId);
|
||||
}
|
||||
if (giepOuvrages[idx] && giepOuvrages[idx].existingDocs) {
|
||||
giepOuvrages[idx].existingDocs = giepOuvrages[idx].existingDocs.filter(d => d.id !== docId);
|
||||
renderOuvrages();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Event listeners pour les remarques sur cet ouvrage
|
||||
container.querySelectorAll('.ouv-notes-input').forEach(textarea => {
|
||||
textarea.addEventListener('input', (e) => {
|
||||
const idx = parseInt(e.target.getAttribute('data-idx'));
|
||||
if (!giepOuvrages[idx]) return;
|
||||
giepOuvrages[idx].notes = e.target.value;
|
||||
updateSerializedGiepData();
|
||||
});
|
||||
});
|
||||
|
||||
// Event listeners pour les boutons segmentés (Tactiles Oui/Non)
|
||||
container.querySelectorAll('.btn-seg').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
|
|
@ -1614,6 +1975,7 @@
|
|||
});
|
||||
});
|
||||
|
||||
toggleParentGlobalSections(true);
|
||||
updateSerializedGiepData();
|
||||
syncGiepCalculatedStatus();
|
||||
}
|
||||
|
|
@ -1674,6 +2036,18 @@
|
|||
giepOuvrages = [];
|
||||
}
|
||||
|
||||
// Compléter chaque ouvrage avec local_id, result_status, notes, existingDocs
|
||||
giepOuvrages.forEach((ouv, idx) => {
|
||||
if (!ouv.local_id) ouv.local_id = String(idx);
|
||||
if (!ouv.notes) ouv.notes = '';
|
||||
ouv.newFiles = [];
|
||||
ouv.existingDocs = (data.documents || []).filter(d => (d.field_key === 'ouvrage_' + ouv.local_id || d.field_key === 'ouvrage_' + idx));
|
||||
if (ouv.result_status === undefined) {
|
||||
ouv.result_status = evaluateSingleOuvrageStatus(ouv);
|
||||
}
|
||||
ouv.is_result_manual = Boolean(ouv.is_result_manual);
|
||||
});
|
||||
|
||||
// Si le statut existant diffère du statut calculé automatiquement, marquer comme surchargé
|
||||
const autoStatus = evaluateGiepInspectionStatus(giepOuvrages);
|
||||
if (data.result_status && autoStatus && data.result_status !== autoStatus) {
|
||||
|
|
@ -1684,15 +2058,20 @@
|
|||
isGiepStatusManuallyOverridden = false;
|
||||
}
|
||||
|
||||
toggleParentGlobalSections(true);
|
||||
renderOuvrages();
|
||||
updateSerializedGiepData();
|
||||
};
|
||||
|
||||
if (rootModal) {
|
||||
rootModal.setGiepFormData = window.setGiepFormData;
|
||||
rootModal.addEventListener('hidden.bs.modal', () => {
|
||||
toggleParentGlobalSections(false);
|
||||
});
|
||||
}
|
||||
|
||||
initStatusRadioListeners();
|
||||
toggleParentGlobalSections(true);
|
||||
renderOuvrages();
|
||||
})();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
<div id="editInspectionSuccessAlert" class="alert alert-success d-none mb-3"></div>
|
||||
|
||||
<!-- Résultat de contrôle -->
|
||||
<div class="mb-3">
|
||||
<div class="mb-3" id="editInspResultSection">
|
||||
<label class="form-label fw-semibold d-block">{% translate "Résultat de l'inspection" %} <span class="text-danger">*</span></label>
|
||||
<div class="row g-2">
|
||||
{% for choice in INSPECTION_RESULT_CHOICES_CONFIG %}
|
||||
|
|
@ -280,7 +280,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Remarques -->
|
||||
<div class="mb-3">
|
||||
<div class="mb-3" id="editInspNotesSection">
|
||||
<label class="form-label fw-semibold mb-1">{% translate "Remarques / Remarques du constat" %}</label>
|
||||
<textarea name="notes" id="edit_insp_notes" class="form-control" rows="3" placeholder="{% translate 'Saisir vos remarques ou détails...' %}"></textarea>
|
||||
</div>
|
||||
|
|
@ -306,7 +306,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Section Ajout de Photos/Documents (Mode continu & Fichiers) -->
|
||||
<div class="card border border-success-subtle mb-3" style="background-color: #e8f6f3;">
|
||||
<div class="card border border-success-subtle mb-3" id="editInspPhotosSection" style="background-color: #e8f6f3;">
|
||||
<div class="card-header bg-transparent border-0 pb-0 pt-2 px-3">
|
||||
<span class="fw-bold text-dark small"><i class="bi bi-camera me-1 text-teal"></i>{% translate "Photos & Fichiers complémentaires" %}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
<div id="quickInspectionAlert" class="alert alert-danger d-none mb-3"></div>
|
||||
|
||||
<!-- Résultat de contrôle avec boutons colorés -->
|
||||
<div class="mb-3">
|
||||
<div class="mb-3" id="quickInspResultSection">
|
||||
<label class="form-label fw-semibold d-block">{% translate "Résultat de l'inspection" %} <span class="text-danger">*</span></label>
|
||||
<div class="d-flex flex-column gap-2" role="group" aria-label="{% translate 'Résultat' %}">
|
||||
<div class="row g-2">
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Section Photos (Mode continu & Fichiers) -->
|
||||
<div class="card border border-success-subtle mb-3" style="background-color: #e8f6f3;">
|
||||
<div class="card border border-success-subtle mb-3" style="background-color: #e8f6f3;" id="quickInspPhotosSection">
|
||||
<div class="card-header bg-transparent border-0 pb-0 pt-2 px-3">
|
||||
<span class="fw-bold text-dark small"><i class="bi bi-camera me-1 text-teal"></i>{% translate "Photos & Fichiers" %}</span>
|
||||
</div>
|
||||
|
|
@ -100,7 +100,7 @@
|
|||
{% endif %}
|
||||
|
||||
<!-- Remarques -->
|
||||
<div class="mb-3">
|
||||
<div class="mb-3" id="quickInspNotesSection">
|
||||
<label class="form-label fw-semibold mb-1">{% translate "Remarques / Remarques du constat" %}</label>
|
||||
<textarea name="notes" id="insp_notes" class="form-control" rows="2" placeholder="{% translate 'Saisir vos remarques ou détails...' %}"></textarea>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@
|
|||
{% endif %}
|
||||
|
||||
|
||||
{% if insp.notes %}
|
||||
{% if insp.notes and not insp.is_giep %}
|
||||
<div class="card bg-light border-0 p-3 mb-3">
|
||||
<h6 class="fw-bold text-secondary mb-2"><i class="bi bi-journal-text me-2"></i>{% translate "Remarques" %}</h6>
|
||||
<div class="text-dark small">{{ insp.notes }}</div>
|
||||
|
|
@ -277,7 +277,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if insp.documents.exists %}
|
||||
{% if insp.documents.exists and not insp.is_giep %}
|
||||
<div class="card bg-light border-0 p-3 mb-3">
|
||||
<h6 class="fw-bold text-secondary mb-2">
|
||||
<i class="bi bi-images me-2 text-primary"></i>{% translate "Photos & Documents joints" %} ({{ insp.documents.count }})
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if insp.notes %}
|
||||
{% if insp.notes and not insp.is_giep %}
|
||||
<div class="card bg-light border-0 p-3 mb-3">
|
||||
<h6 class="fw-bold text-secondary mb-2"><i class="bi bi-journal-text me-2"></i>{% translate "Remarques" %}</h6>
|
||||
<div class="text-dark small">{{ insp.notes }}</div>
|
||||
|
|
@ -363,7 +363,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if insp.documents.exists %}
|
||||
{% if insp.documents.exists and not insp.is_giep %}
|
||||
<div class="card bg-light border-0 p-3 mb-3">
|
||||
<h6 class="fw-bold text-secondary mb-2">
|
||||
<i class="bi bi-images me-2 text-primary"></i>{% translate "Photos & Documents joints" %} ({{ insp.documents.count }})
|
||||
|
|
|
|||
|
|
@ -130,3 +130,63 @@ def get_q_val(questions_dict, key):
|
|||
if val is not None and val != '' and val != 'null':
|
||||
return val
|
||||
return 'null'
|
||||
|
||||
@register.filter
|
||||
def filter_ouvrage_docs(documents, ouv):
|
||||
"""
|
||||
Filtre les documents/photos d'une inspection pour un ouvrage donné.
|
||||
Correspondance sur field_key == 'ouvrage_{local_id}' ou 'ouvrage_{idx}' ou 'asset_{asset_id}'.
|
||||
"""
|
||||
if not documents:
|
||||
return []
|
||||
target_keys = set()
|
||||
if isinstance(ouv, dict):
|
||||
local_id = ouv.get('local_id')
|
||||
if local_id is not None:
|
||||
target_keys.add(f"ouvrage_{local_id}")
|
||||
target_keys.add(str(local_id))
|
||||
ouv_id = ouv.get('id')
|
||||
if ouv_id is not None:
|
||||
target_keys.add(f"ouvrage_{ouv_id}")
|
||||
target_keys.add(str(ouv_id))
|
||||
asset_id = ouv.get('asset_id')
|
||||
if asset_id is not None:
|
||||
target_keys.add(f"asset_{asset_id}")
|
||||
elif isinstance(ouv, (int, str)):
|
||||
target_keys.add(f"ouvrage_{ouv}")
|
||||
target_keys.add(str(ouv))
|
||||
|
||||
matching = []
|
||||
docs_list = documents.all() if hasattr(documents, 'all') else list(documents)
|
||||
for doc in docs_list:
|
||||
f_key = getattr(doc, 'field_key', None) or (doc.get('field_key') if isinstance(doc, dict) else '')
|
||||
if f_key and str(f_key).strip() in target_keys:
|
||||
matching.append(doc)
|
||||
return matching
|
||||
|
||||
@register.filter
|
||||
def filter_general_docs(documents, ouvrages=None):
|
||||
"""
|
||||
Retourne les documents/photos qui ne sont rattachés à aucun ouvrage spécifique (field_key vide ou non lié).
|
||||
"""
|
||||
if not documents:
|
||||
return []
|
||||
docs_list = documents.all() if hasattr(documents, 'all') else list(documents)
|
||||
general = []
|
||||
for doc in docs_list:
|
||||
f_key = (getattr(doc, 'field_key', None) or (doc.get('field_key') if isinstance(doc, dict) else '') or '').strip()
|
||||
if not f_key or not (f_key.startswith('ouvrage_') or f_key.startswith('asset_')):
|
||||
general.append(doc)
|
||||
return general
|
||||
|
||||
@register.filter
|
||||
def ouvrage_result_badge(status_code):
|
||||
return INSPECTION_RESULT_CONFIG.get(status_code, {}).get('badge_class', 'bg-secondary')
|
||||
|
||||
@register.filter
|
||||
def ouvrage_result_label(status_code):
|
||||
return INSPECTION_RESULT_CONFIG.get(status_code, {}).get('label', status_code or 'Non déterminé')
|
||||
|
||||
@register.filter
|
||||
def ouvrage_result_icon(status_code):
|
||||
return INSPECTION_RESULT_CONFIG.get(status_code, {}).get('icon', 'bi-question-circle')
|
||||
|
|
|
|||
|
|
@ -2154,4 +2154,234 @@ class PostInterventionDurabilityInspectionTestCase(TestCase):
|
|||
self.assertTrue(base_insp.is_hotbox_related)
|
||||
|
||||
|
||||
class GiepInspectionRestructuringTestCase(TestCase):
|
||||
def setUp(self):
|
||||
from django.db import connection
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("CREATE EXTENSION IF NOT EXISTS unaccent;")
|
||||
cursor.execute("""
|
||||
CREATE OR REPLACE FUNCTION public.unaccent_immutable(text)
|
||||
RETURNS text
|
||||
LANGUAGE sql
|
||||
IMMUTABLE PARALLEL SAFE
|
||||
AS $function$
|
||||
SELECT public.unaccent('public.unaccent', $1)
|
||||
$function$;
|
||||
CREATE OR REPLACE FUNCTION public.unaccent_immutable(character varying)
|
||||
RETURNS text
|
||||
LANGUAGE sql
|
||||
IMMUTABLE PARALLEL SAFE
|
||||
AS $function$
|
||||
SELECT public.unaccent('public.unaccent', $1)
|
||||
$function$;
|
||||
""")
|
||||
self.client = Client()
|
||||
self.user = User.objects.create_user(
|
||||
username='giep_inspector',
|
||||
password='password123',
|
||||
first_name='Giep',
|
||||
last_name='Tester'
|
||||
)
|
||||
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.rwiz = NatureRWIZ.objects.create(name_fr="Zone GIEP Parc")
|
||||
self.rwiz_ct = ContentType.objects.get_for_model(NatureRWIZ)
|
||||
self.client.login(username='giep_inspector', password='password123')
|
||||
|
||||
def test_compute_status_for_single_ouvrage(self):
|
||||
# 1. Manual status takes precedence if valid
|
||||
ouv_manual_damaged = {'result_status': 'damaged', 'is_result_manual': True}
|
||||
self.assertEqual(GiepInspection.compute_status_for_single_ouvrage(ouv_manual_damaged), 'damaged')
|
||||
|
||||
ouv_manual_nc = {'result_status': 'non_compliant'}
|
||||
self.assertEqual(GiepInspection.compute_status_for_single_ouvrage(ouv_manual_nc), 'non_compliant')
|
||||
|
||||
ouv_manual_compliant = {'result_status': 'compliant'}
|
||||
self.assertEqual(GiepInspection.compute_status_for_single_ouvrage(ouv_manual_compliant), 'compliant')
|
||||
|
||||
# 2. Auto status computation for EVC
|
||||
# Bad vegetation -> non_compliant
|
||||
ouv_evc_nc = {
|
||||
'device_type': 'evc',
|
||||
'questions': {'evc_ev_veg_etat': 'mauvais'}
|
||||
}
|
||||
self.assertEqual(GiepInspection.compute_status_for_single_ouvrage(ouv_evc_nc), 'non_compliant')
|
||||
|
||||
# Crust sediment > 5cm -> damaged
|
||||
ouv_evc_damaged = {
|
||||
'device_type': 'evc',
|
||||
'questions': {'evc_ev_sediments': True, 'evc_ev_croute_sediments': True}
|
||||
}
|
||||
self.assertEqual(GiepInspection.compute_status_for_single_ouvrage(ouv_evc_damaged), 'damaged')
|
||||
|
||||
# Good state -> compliant
|
||||
ouv_evc_good = {
|
||||
'device_type': 'evc',
|
||||
'questions': {'evc_ev_veg_etat': 'bon', 'evc_ev_sediments': False}
|
||||
}
|
||||
self.assertEqual(GiepInspection.compute_status_for_single_ouvrage(ouv_evc_good), 'compliant')
|
||||
|
||||
def test_compute_status_from_data_consolidation(self):
|
||||
# If any ouvrage is damaged -> overall damaged
|
||||
data_with_damaged = {
|
||||
'ouvrages': [
|
||||
{'result_status': 'compliant'},
|
||||
{'result_status': 'damaged'},
|
||||
]
|
||||
}
|
||||
self.assertEqual(GiepInspection.compute_status_from_data(data_with_damaged), 'damaged')
|
||||
|
||||
# If any is non_compliant and none damaged -> non_compliant
|
||||
data_with_nc = {
|
||||
'ouvrages': [
|
||||
{'result_status': 'compliant'},
|
||||
{'result_status': 'non_compliant'},
|
||||
]
|
||||
}
|
||||
self.assertEqual(GiepInspection.compute_status_from_data(data_with_nc), 'non_compliant')
|
||||
|
||||
# If all compliant -> compliant
|
||||
data_all_compliant = {
|
||||
'ouvrages': [
|
||||
{'result_status': 'compliant'},
|
||||
{'result_status': 'compliant'},
|
||||
]
|
||||
}
|
||||
self.assertEqual(GiepInspection.compute_status_from_data(data_all_compliant), 'compliant')
|
||||
|
||||
def test_templatetags_filters(self):
|
||||
from inspections.models import INSPECTION_RESULT_CONFIG
|
||||
from inspections.templatetags.inspections_tags import (
|
||||
filter_ouvrage_docs,
|
||||
filter_general_docs,
|
||||
ouvrage_result_badge,
|
||||
ouvrage_result_label,
|
||||
ouvrage_result_icon,
|
||||
)
|
||||
|
||||
insp = Inspection.objects.create(
|
||||
asset_content_type=self.rwiz_ct,
|
||||
asset_object_id=self.rwiz.id,
|
||||
inspector=self.user,
|
||||
result_status='compliant'
|
||||
)
|
||||
doc1 = InspectionDocument.objects.create(
|
||||
inspection=insp,
|
||||
field_key='ouvrage_1',
|
||||
file='test1.jpg'
|
||||
)
|
||||
doc2 = InspectionDocument.objects.create(
|
||||
inspection=insp,
|
||||
field_key='ouvrage_ouv_abc',
|
||||
file='test2.jpg'
|
||||
)
|
||||
doc_gen = InspectionDocument.objects.create(
|
||||
inspection=insp,
|
||||
field_key='',
|
||||
file='general.jpg'
|
||||
)
|
||||
|
||||
all_docs = [doc1, doc2, doc_gen]
|
||||
|
||||
# Test filter_ouvrage_docs with dict ouvrage
|
||||
ouv1 = {'id': 1, 'local_id': 'local_1'}
|
||||
res1 = filter_ouvrage_docs(all_docs, ouv1)
|
||||
self.assertEqual(len(res1), 1)
|
||||
self.assertEqual(res1[0].id, doc1.id)
|
||||
|
||||
ouv2 = {'id': 99, 'local_id': 'ouv_abc'}
|
||||
res2 = filter_ouvrage_docs(all_docs, ouv2)
|
||||
self.assertEqual(len(res2), 1)
|
||||
self.assertEqual(res2[0].id, doc2.id)
|
||||
|
||||
# Test filter_general_docs
|
||||
gen_res = filter_general_docs(all_docs)
|
||||
self.assertEqual(len(gen_res), 1)
|
||||
self.assertEqual(gen_res[0].id, doc_gen.id)
|
||||
|
||||
# Badges and labels
|
||||
self.assertIn('bg-success', ouvrage_result_badge('compliant'))
|
||||
self.assertIn('bg-danger', ouvrage_result_badge('damaged'))
|
||||
self.assertIn('bg-warning', ouvrage_result_badge('non_compliant'))
|
||||
self.assertEqual(str(ouvrage_result_label('compliant')), str(INSPECTION_RESULT_CONFIG['compliant']['label']))
|
||||
self.assertEqual(str(ouvrage_result_label('damaged')), str(INSPECTION_RESULT_CONFIG['damaged']['label']))
|
||||
self.assertEqual(str(ouvrage_result_label('non_compliant')), str(INSPECTION_RESULT_CONFIG['non_compliant']['label']))
|
||||
|
||||
def test_create_inspection_api_with_giep_data_and_photos(self):
|
||||
import json
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
|
||||
giep_dict = {
|
||||
'meteo': {'pluie_recente': True, 'sol_sec': False},
|
||||
'ouvrages': [
|
||||
{
|
||||
'local_id': 'ouv_1',
|
||||
'name': 'Ouvrage 1',
|
||||
'device_type': 'evc',
|
||||
'result_status': 'damaged',
|
||||
'is_result_manual': True,
|
||||
'notes': 'Bordure affaissée',
|
||||
'questions': {}
|
||||
},
|
||||
{
|
||||
'local_id': 'ouv_2',
|
||||
'name': 'Ouvrage 2',
|
||||
'device_type': 'oed',
|
||||
'result_status': 'compliant',
|
||||
'is_result_manual': True,
|
||||
'notes': 'Tout est propre',
|
||||
'questions': {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
photo_ouv1 = SimpleUploadedFile("ouv1.jpg", b"fake_jpeg_ouv1", content_type="image/jpeg")
|
||||
photo_gen = SimpleUploadedFile("zone.jpg", b"fake_jpeg_zone", content_type="image/jpeg")
|
||||
|
||||
url = reverse('inspections:create_inspection_api')
|
||||
post_data = {
|
||||
'asset_content_type_id': self.rwiz_ct.id,
|
||||
'asset_object_id': self.rwiz.id,
|
||||
'nature': 'periodic',
|
||||
'status': 'completed',
|
||||
'giep_data': json.dumps(giep_dict),
|
||||
'field_keys': ['ouvrage_ouv_1', 'general'],
|
||||
'photos': [photo_ouv1, photo_gen]
|
||||
}
|
||||
|
||||
response = self.client.post(url, post_data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
resp_json = response.json()
|
||||
self.assertTrue(resp_json['success'])
|
||||
|
||||
insp = Inspection.objects.get(id=resp_json['inspection_id'])
|
||||
self.assertTrue(insp.is_giep)
|
||||
# Consolidated status should be damaged because ouv_1 is damaged
|
||||
self.assertEqual(insp.result_status, 'damaged')
|
||||
# Notes should be auto-consolidated from ouv_1 and ouv_2
|
||||
self.assertIn('Ouvrage 1: Bordure affaissée', insp.notes)
|
||||
self.assertIn('Ouvrage 2: Tout est propre', insp.notes)
|
||||
|
||||
# Check attached documents
|
||||
self.assertEqual(insp.documents.count(), 2)
|
||||
doc_keys = list(insp.documents.values_list('field_key', flat=True))
|
||||
self.assertIn('ouvrage_ouv_1', doc_keys)
|
||||
self.assertIn('general', doc_keys)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -552,11 +552,9 @@ def create_inspection_api(request):
|
|||
"""
|
||||
API endpoint pour l'enregistrement d'une nouvelle inspection (avec prise en compte optionnelle de mission_intervention et checked_intervention).
|
||||
"""
|
||||
content_type_id = request.POST.get('content_type_id')
|
||||
object_id = request.POST.get('object_id')
|
||||
content_type_id = request.POST.get('content_type_id') or request.POST.get('asset_content_type_id')
|
||||
object_id = request.POST.get('object_id') or request.POST.get('asset_object_id')
|
||||
result_status = request.POST.get('result_status')
|
||||
if not result_status:
|
||||
return JsonResponse({'success': False, 'error': _("Veuillez sélectionner le résultat de l'inspection.")}, status=400)
|
||||
nature = request.POST.get('nature', 'visual')
|
||||
inspector_role = request.POST.get('inspector_role', 'observer')
|
||||
notes = request.POST.get('notes', '').strip()
|
||||
|
|
@ -578,6 +576,9 @@ def create_inspection_api(request):
|
|||
|
||||
is_parking_spot = (content_type.model == 'parkingspot')
|
||||
is_giep = (content_type.model == 'naturerwiz')
|
||||
|
||||
if not is_giep and not result_status:
|
||||
return JsonResponse({'success': False, 'error': _("Veuillez sélectionner le résultat de l'inspection.")}, status=400)
|
||||
is_signpanel = (content_type.model == 'signpanel')
|
||||
is_hotbox = (
|
||||
request.POST.get('is_hotbox') == 'true' or
|
||||
|
|
@ -665,6 +666,21 @@ def create_inspection_api(request):
|
|||
except Exception:
|
||||
parsed_giep_data = {}
|
||||
|
||||
if not result_status or result_status == 'pending':
|
||||
result_status = GiepInspection.compute_status_from_data(parsed_giep_data)
|
||||
if not result_status:
|
||||
result_status = 'compliant'
|
||||
|
||||
if not notes and parsed_giep_data and isinstance(parsed_giep_data.get('ouvrages'), list):
|
||||
collected_notes = []
|
||||
for idx_o, ouv in enumerate(parsed_giep_data['ouvrages']):
|
||||
o_notes = (ouv.get('notes') or '').strip()
|
||||
if o_notes:
|
||||
o_name = ouv.get('asset_code') or ouv.get('name') or f"Ouvrage #{idx_o + 1}"
|
||||
collected_notes.append(f"{o_name}: {o_notes}")
|
||||
if collected_notes:
|
||||
notes = "\n".join(collected_notes)
|
||||
|
||||
inspection = GiepInspection(
|
||||
asset_content_type=content_type,
|
||||
asset_object_id=target_obj.pk,
|
||||
|
|
@ -876,6 +892,7 @@ def get_inspection_detail_api(request, inspection_id):
|
|||
'filename': doc.filename,
|
||||
'url': doc.file_url,
|
||||
'is_image': doc.is_image,
|
||||
'field_key': doc.field_key or '',
|
||||
})
|
||||
|
||||
from django.template.loader import render_to_string
|
||||
|
|
@ -1099,6 +1116,8 @@ def update_inspection_api(request, inspection_id):
|
|||
inspection.data = json.loads(request.POST['giep_data'])
|
||||
except Exception:
|
||||
pass
|
||||
if 'result_status' not in request.POST or not request.POST.get('result_status'):
|
||||
inspection.result_status = GiepInspection.compute_status_from_data(inspection.data)
|
||||
elif isinstance(inspection, SignPanelInspection):
|
||||
from assets.models import SignPanelType
|
||||
if 'verified_signpanel_type_id' in request.POST:
|
||||
|
|
|
|||
Loading…
Reference in a new issue