feat: make inspection result status mandatory and improve GIEP form scoping

This commit is contained in:
kdeterme 2026-09-14 14:49:34 +02:00
parent fe535683a1
commit bfebdcf3dd
7 changed files with 176 additions and 87 deletions

View file

@ -1438,6 +1438,11 @@
.catch(err => console.error('Error loading inspection fields partial:', err));
}
// Réinitialisation des boutons de résultat de contrôle (aucun présélectionné par défaut)
modalEl.querySelectorAll('input[type="radio"][name="result_status"]').forEach(r => {
r.checked = false;
});
// Réinitialisation de l'accumulateur de photos/fichiers
accumulatedInspectionFiles = [];
window.accumulatedInspectionFiles = accumulatedInspectionFiles;
@ -2142,6 +2147,16 @@
const formData = new FormData(form);
if (!formData.get('result_status')) {
const missingStatusMsg = window.gettext ? window.gettext("Veuillez sélectionner le résultat de l'inspection.") : "Veuillez sélectionner le résultat de l'inspection.";
alertDiv.textContent = missingStatusMsg;
alertDiv.classList.remove('d-none');
submitBtn.disabled = false;
if (spinner) spinner.classList.add('d-none');
if (icon) icon.classList.remove('d-none');
return;
}
// Ajout des fichiers accumulés en mode continu
accumulatedInspectionFiles.forEach(file => {
formData.append('photos', file);

View file

@ -42,7 +42,7 @@
<div class="d-flex flex-column gap-2" role="group" aria-label="{% translate 'Résultat' %}">
<div class="row g-2">
<div class="col-6">
<input type="radio" class="btn-check" name="result_status" id="res_compliant" value="compliant" checked>
<input type="radio" class="btn-check" name="result_status" id="res_compliant" value="compliant">
<label class="btn btn-outline-success w-100 py-2 d-flex align-items-center justify-content-center gap-1" for="res_compliant">
<i class="bi bi-check-circle-fill"></i>
<span>{% translate "Conforme" %}</span>

View file

@ -1,7 +1,7 @@
{% load i18n static %}
<!-- Component Formulaire d'Inspection GIEP Natif Mobile-First (Recommandations avec 5 choix) -->
<div id="giepSpecificFields" class="border rounded-4 p-3 mb-3 bg-white shadow-sm" data-location-id="{% if asset.location_id %}{{ asset.location_id }}{% elif asset.id %}{{ asset.id }}{% elif location.id %}{{ location.id }}{% endif %}" data-asset-id="{% if asset.id %}{{ asset.id }}{% endif %}">
<div id="giepSpecificFields" class="giep-specific-fields border rounded-4 p-3 mb-3 bg-white shadow-sm" data-location-id="{% if asset.location_id %}{{ asset.location_id }}{% elif asset.id %}{{ asset.id }}{% elif location.id %}{{ location.id }}{% endif %}" data-asset-id="{% if asset.id %}{{ asset.id }}{% endif %}">
<!-- En-tête -->
<div class="d-flex align-items-center justify-content-between mb-3 border-bottom pb-2">
@ -16,7 +16,7 @@
<input type="hidden" name="giep_data" id="giep_data_serialized" value="{}">
<!-- Bandeau de statut automatique GIEP -->
<div id="giep_status_auto_banner" class="alert alert-info border border-info-subtle rounded-3 p-2.5 mb-3 shadow-sm d-none"></div>
<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>
<!-- 1. Conditions météorologiques -->
<div class="card border-primary-subtle bg-light-subtle mb-3 rounded-4 shadow-sm">
@ -56,18 +56,18 @@
<h6 class="fw-bold mb-0 small text-uppercase text-secondary">
<i class="bi bi-list-check me-2"></i>{% translate "Ouvrages inspectés" %}
</h6>
<button type="button" class="btn btn-outline-success btn-sm py-1.5 px-3 fw-semibold rounded-pill shadow-sm" id="giep_add_ouvrage_btn">
<button type="button" class="btn btn-outline-success btn-sm py-1.5 px-3 fw-semibold rounded-pill shadow-sm giep-add-ouvrage-btn" id="giep_add_ouvrage_btn">
<i class="bi bi-plus-circle me-1"></i>{% translate "Ajouter un ouvrage" %}
</button>
</div>
<!-- Dynamic Container for Ouvrages -->
<div id="giep_ouvrages_container" class="d-flex flex-column gap-3"></div>
<div id="giep_ouvrages_container" class="giep-ouvrages-container d-flex flex-column gap-3"></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: 1090;">
<div class="modal fade giep-asset-map-modal" id="giepAssetMapModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="giepAssetMapModalLabel" aria-hidden="true" style="z-index: 1090;">
<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">
@ -81,11 +81,11 @@
<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 ou type d'ouvrage..." %}">
<input type="text" id="giep_modal_search_input" class="form-control giep-modal-search-input" placeholder="{% translate "Filtrer par code ou type d'ouvrage..." %}">
</div>
</div>
<div class="col-12 col-md-5">
<select id="giep_modal_type_filter" class="form-select form-select-sm">
<select id="giep_modal_type_filter" class="form-select form-select-sm giep-modal-type-filter">
<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>
@ -156,9 +156,21 @@
<script>
(function() {
const currentScript = document.currentScript;
const rootModal = (currentScript && currentScript.closest('.modal')) ||
(currentScript && currentScript.closest('form')) ||
document.querySelector('#quickInspectionModal.show, #editInspectionModal.show') ||
document.getElementById('quickInspectionModal') ||
document;
const giepFields = rootModal.querySelector('.giep-specific-fields') || rootModal.querySelector('#giepSpecificFields');
const container = rootModal.querySelector('.giep-ouvrages-container') || rootModal.querySelector('#giep_ouvrages_container');
const serializedInput = rootModal.querySelector('input[name="giep_data"]');
const addBtn = rootModal.querySelector('.giep-add-ouvrage-btn') || rootModal.querySelector('#giep_add_ouvrage_btn');
const banner = rootModal.querySelector('.giep-status-auto-banner') || rootModal.querySelector('#giep_status_auto_banner');
if (typeof window.toggleMissingResultChoice === 'function') {
const container = document.getElementById('giepSpecificFields');
const isAssetSpecific = container && container.dataset.assetId && container.dataset.assetId !== '';
const isAssetSpecific = giepFields && giepFields.dataset.assetId && giepFields.dataset.assetId !== '';
if (!isAssetSpecific) {
window.toggleMissingResultChoice(true);
}
@ -241,13 +253,8 @@
let modalMapInstance = null;
let modalMapMarkers = [];
const container = document.getElementById('giep_ouvrages_container');
const serializedInput = document.getElementById('giep_data_serialized');
const addBtn = document.getElementById('giep_add_ouvrage_btn');
function resolveCurrentLocationId() {
// 1. Check data-location-id on container
const giepFields = document.getElementById('giepSpecificFields');
if (giepFields && giepFields.getAttribute('data-location-id')) {
return giepFields.getAttribute('data-location-id');
}
@ -256,17 +263,17 @@
return window.currentGiepLocationId;
}
// 3. Check quick inspection modal insp_object_id
const inspObjIdEl = document.getElementById('insp_object_id');
const inspObjIdEl = rootModal.querySelector('#insp_object_id, [name="object_id"]');
if (inspObjIdEl && inspObjIdEl.value) {
return inspObjIdEl.value;
}
// 4. Check edit inspection modal edit_insp_object_id
const editInspObjIdEl = document.getElementById('edit_insp_object_id');
const editInspObjIdEl = rootModal.querySelector('#edit_insp_object_id');
if (editInspObjIdEl && editInspObjIdEl.value) {
return editInspObjIdEl.value;
}
// 5. Check trigger with data-object-id
const modalTrigger = document.querySelector('[data-object-id]');
const modalTrigger = rootModal.querySelector('[data-object-id]') || document.querySelector('[data-object-id]');
if (modalTrigger && modalTrigger.getAttribute('data-object-id')) {
return modalTrigger.getAttribute('data-object-id');
}
@ -504,8 +511,10 @@
}
function updateSerializedGiepData() {
const pluieVal = document.getElementById('giep_date_derniere_pluie')?.value || 'null';
const orageVal = document.getElementById('giep_orage_moins_une_heure')?.value || 'null';
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';
const payload = {
ouvrages: giepOuvrages,
@ -597,12 +606,29 @@
function getStatusLabel(status) {
if (status === 'damaged') return '{% filter escapejs %}{% translate "Endommagé" %}{% endfilter %}';
if (status === 'non_compliant') return '{% filter escapejs %}{% translate "Non conforme" %}{% endfilter %}';
return '{% filter escapejs %}{% translate "Conforme" %}{% endfilter %}';
if (status === 'compliant') return '{% filter escapejs %}{% translate "Conforme" %}{% endfilter %}';
return '{% filter escapejs %}{% translate "Non déterminé" %}{% endfilter %}';
}
function hasAnyGiepAnswer(ouvrages) {
if (!Array.isArray(ouvrages) || ouvrages.length === 0) return false;
for (const ouv of ouvrages) {
if (!ouv || !ouv.questions) continue;
for (const [k, v] of Object.entries(ouv.questions)) {
if (v !== null && v !== undefined && v !== 'null' && v !== '' && v !== false) {
return true;
}
}
}
return false;
}
function evaluateGiepInspectionStatus(ouvrages) {
if (!Array.isArray(ouvrages) || ouvrages.length === 0) {
return 'compliant';
return null;
}
if (!hasAnyGiepAnswer(ouvrages)) {
return null;
}
let hasDamaged = false;
@ -768,60 +794,67 @@
}
function getAllStatusRadios() {
return document.querySelectorAll('input[type="radio"][name="result_status"]');
return rootModal.querySelectorAll('input[type="radio"][name="result_status"]');
}
function getActiveStatusRadio(status) {
const editRad = document.getElementById('edit_res_' + status);
if (editRad && editRad.closest('.modal.show, form')) return editRad;
const createRad = document.getElementById('res_' + status);
if (createRad && createRad.closest('.modal.show, form')) return createRad;
return createRad || editRad || null;
function getStatusRadio(status) {
return rootModal.querySelector('input[type="radio"][name="result_status"][value="' + status + '"]');
}
function syncGiepCalculatedStatus() {
initStatusRadioListeners();
const calculated = evaluateGiepInspectionStatus(giepOuvrages);
const banner = document.getElementById('giep_status_auto_banner');
if (!banner) return;
if (!isGiepStatusManuallyOverridden) {
const rad = getActiveStatusRadio(calculated);
if (rad && !rad.checked) {
rad.checked = true;
rad.dispatchEvent(new Event('change', { bubbles: true }));
if (calculated) {
const rad = getStatusRadio(calculated);
if (rad && !rad.checked) {
rad.checked = true;
rad.dispatchEvent(new Event('change', { bubbles: true }));
}
}
}
if (banner) {
banner.classList.remove('d-none');
const calcLabel = getStatusLabel(calculated);
let badgeClass = 'bg-success';
banner.classList.remove('d-none');
if (isGiepStatusManuallyOverridden) {
let checkedVal = '';
getAllStatusRadios().forEach(r => { if (r.checked) checkedVal = r.value; });
const currentLabel = getStatusLabel(checkedVal);
const calcLabel = calculated ? getStatusLabel(calculated) : '{% filter escapejs %}{% translate "Non déterminé" %}{% endfilter %}';
let badgeClass = 'bg-secondary';
if (calculated === 'damaged') badgeClass = 'bg-danger';
else if (calculated === 'non_compliant') badgeClass = 'bg-warning text-dark';
else if (calculated === 'compliant') badgeClass = 'bg-success';
if (isGiepStatusManuallyOverridden) {
let checkedVal = '';
getAllStatusRadios().forEach(r => { if (r.checked) checkedVal = r.value; });
const currentLabel = getStatusLabel(checkedVal);
banner.className = 'alert alert-warning border border-warning-subtle rounded-3 p-2.5 mb-3 shadow-sm d-flex align-items-center justify-content-between flex-wrap gap-2 giep-status-auto-banner';
banner.innerHTML = `
<div class="small">
<i class="bi bi-pencil-square me-1"></i>
<strong>{% filter escapejs %}{% translate "Statut modifié manuellement" %}{% endfilter %} :</strong>
<span class="badge bg-secondary me-1">${currentLabel}</span>
<span class="text-muted">({% filter escapejs %}{% translate "Calculé" %}{% endfilter %} : <span class="badge ${badgeClass}">${calcLabel}</span>)</span>
</div>
<button type="button" class="btn btn-sm btn-outline-dark py-0 px-2 fw-semibold btn-reset-giep-auto-status" id="btn_reset_giep_auto_status">
<i class="bi bi-arrow-counterclockwise me-1"></i>{% filter escapejs %}{% translate "Rétablir le calcul auto" %}{% endfilter %}
</button>
`;
banner.querySelector('.btn-reset-giep-auto-status')?.addEventListener('click', () => {
isGiepStatusManuallyOverridden = false;
if (!calculated) {
getAllStatusRadios().forEach(r => { r.checked = false; });
}
syncGiepCalculatedStatus();
});
} else {
if (calculated) {
const calcLabel = getStatusLabel(calculated);
let badgeClass = 'bg-success';
if (calculated === 'damaged') badgeClass = 'bg-danger';
else if (calculated === 'non_compliant') badgeClass = 'bg-warning text-dark';
banner.className = 'alert alert-warning border border-warning-subtle rounded-3 p-2.5 mb-3 shadow-sm d-flex align-items-center justify-content-between flex-wrap gap-2';
banner.innerHTML = `
<div class="small">
<i class="bi bi-pencil-square me-1"></i>
<strong>{% filter escapejs %}{% translate "Statut modifié manuellement" %}{% endfilter %} :</strong>
<span class="badge bg-secondary me-1">${currentLabel}</span>
<span class="text-muted">({% filter escapejs %}{% translate "Calculé" %}{% endfilter %} : <span class="badge ${badgeClass}">${calcLabel}</span>)</span>
</div>
<button type="button" class="btn btn-sm btn-outline-dark py-0 px-2 fw-semibold" id="btn_reset_giep_auto_status">
<i class="bi bi-arrow-counterclockwise me-1"></i>{% filter escapejs %}{% translate "Rétablir le calcul auto" %}{% endfilter %}
</button>
`;
document.getElementById('btn_reset_giep_auto_status')?.addEventListener('click', () => {
isGiepStatusManuallyOverridden = false;
syncGiepCalculatedStatus();
});
} else {
banner.className = 'alert alert-light border border-info-subtle bg-info-subtle text-info-emphasis rounded-3 p-2.5 mb-3 shadow-sm d-flex align-items-center justify-content-between flex-wrap gap-2';
banner.className = 'alert alert-light border border-info-subtle bg-info-subtle text-info-emphasis rounded-3 p-2.5 mb-3 shadow-sm d-flex align-items-center justify-content-between flex-wrap gap-2 giep-status-auto-banner';
banner.innerHTML = `
<div class="small">
<i class="bi bi-magic me-1 text-primary"></i>
@ -829,24 +862,37 @@
<span class="badge ${badgeClass} ms-1">${calcLabel}</span>
</div>
`;
} else {
banner.className = 'alert alert-light border border-secondary-subtle bg-light text-secondary rounded-3 p-2.5 mb-3 shadow-sm d-flex align-items-center justify-content-between flex-wrap gap-2 giep-status-auto-banner';
banner.innerHTML = `
<div class="small">
<i class="bi bi-magic me-1 text-secondary"></i>
<strong>{% filter escapejs %}{% translate "Statut calculé automatiquement" %}{% endfilter %} :</strong>
<span class="badge bg-secondary ms-1">{% filter escapejs %}{% translate "En attente des réponses" %}{% endfilter %}</span>
</div>
`;
}
}
}
function initStatusRadioListeners() {
getAllStatusRadios().forEach(radio => {
if (radio.dataset.giepListenerAttached) return;
radio.dataset.giepListenerAttached = 'true';
const onRadioClick = (e) => {
const radio = e.currentTarget;
const calculated = evaluateGiepInspectionStatus(giepOuvrages);
if (radio.value !== calculated) {
isGiepStatusManuallyOverridden = true;
} else {
isGiepStatusManuallyOverridden = false;
}
syncGiepCalculatedStatus();
};
radio.addEventListener('click', () => {
const calculated = evaluateGiepInspectionStatus(giepOuvrages);
if (radio.value !== calculated) {
isGiepStatusManuallyOverridden = true;
} else {
isGiepStatusManuallyOverridden = false;
}
syncGiepCalculatedStatus();
});
getAllStatusRadios().forEach(radio => {
if (radio._giepRadioClickHandler) {
radio.removeEventListener('click', radio._giepRadioClickHandler);
}
radio._giepRadioClickHandler = onRadioClick;
radio.addEventListener('click', onRadioClick);
});
}
@ -1033,7 +1079,7 @@
}
function initOrUpdateModalMap() {
const containerEl = document.getElementById('giepModalMap');
const containerEl = rootModal.querySelector('#giepModalMap') || 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';
@ -1071,7 +1117,7 @@
function openMapModal(idx) {
activeOuvrageIdxForMap = idx;
fetchLocationAssets().then(assets => {
const modalEl = document.getElementById('giepAssetMapModal');
const modalEl = rootModal.querySelector('.giep-asset-map-modal') || document.getElementById('giepAssetMapModal');
let modalInstance = bootstrap.Modal.getInstance(modalEl);
if (!modalInstance) {
modalInstance = new bootstrap.Modal(modalEl);
@ -1095,7 +1141,7 @@
</button>
</div>
`;
document.getElementById('giep_add_first_ouvrage_btn')?.addEventListener('click', () => {
container.querySelector('#giep_add_first_ouvrage_btn')?.addEventListener('click', () => {
giepOuvrages.push(createEmptyOuvrage(0, "evc"));
renderOuvrages();
});
@ -1580,28 +1626,31 @@
}
// Modal filter & shown listeners
const modalEl = document.getElementById('giepAssetMapModal');
const modalEl = rootModal.querySelector('.giep-asset-map-modal') || document.getElementById('giepAssetMapModal');
if (modalEl) {
modalEl.addEventListener('shown.bs.modal', () => {
initOrUpdateModalMap();
});
}
document.getElementById('giep_modal_search_input')?.addEventListener('input', (e) => {
const searchInput = rootModal.querySelector('#giep_modal_search_input, .giep-modal-search-input');
const typeFilter = rootModal.querySelector('#giep_modal_type_filter, .giep-modal-type-filter');
searchInput?.addEventListener('input', (e) => {
const text = e.target.value;
const type = document.getElementById('giep_modal_type_filter')?.value || '';
const type = typeFilter?.value || '';
populateModalAssetsList(text, type);
renderModalMapFeatures(false);
});
document.getElementById('giep_modal_type_filter')?.addEventListener('change', (e) => {
typeFilter?.addEventListener('change', (e) => {
const type = e.target.value;
const text = document.getElementById('giep_modal_search_input')?.value || '';
const text = searchInput?.value || '';
populateModalAssetsList(text, type);
renderModalMapFeatures(false);
});
document.querySelectorAll('.giep-input').forEach(el => {
rootModal.querySelectorAll('.giep-input').forEach(el => {
el.addEventListener('change', updateSerializedGiepData);
});
@ -1611,11 +1660,11 @@
window.currentGiepLocationId = data.location_id;
locationGiepAssets = [];
}
const dateRain = document.getElementById('giep_date_derniere_pluie');
const dateRain = rootModal.querySelector('#giep_date_derniere_pluie, [name="date_derniere_pluie"]');
if (dateRain) {
dateRain.value = (data.date_derniere_pluie !== undefined && data.date_derniere_pluie !== null) ? String(data.date_derniere_pluie) : 'null';
}
const storm = document.getElementById('giep_orage_moins_une_heure');
const storm = rootModal.querySelector('#giep_orage_moins_une_heure, [name="orage_moins_une_heure"]');
if (storm) {
storm.value = (data.orage_moins_une_heure !== undefined && data.orage_moins_une_heure !== null) ? String(data.orage_moins_une_heure) : 'null';
}
@ -1627,7 +1676,9 @@
// Si le statut existant diffère du statut calculé automatiquement, marquer comme surchargé
const autoStatus = evaluateGiepInspectionStatus(giepOuvrages);
if (data.result_status && data.result_status !== autoStatus) {
if (data.result_status && autoStatus && data.result_status !== autoStatus) {
isGiepStatusManuallyOverridden = true;
} else if (data.result_status && !autoStatus) {
isGiepStatusManuallyOverridden = true;
} else {
isGiepStatusManuallyOverridden = false;
@ -1637,6 +1688,10 @@
updateSerializedGiepData();
};
if (rootModal) {
rootModal.setGiepFormData = window.setGiepFormData;
}
initStatusRadioListeners();
renderOuvrages();
})();

View file

@ -594,7 +594,10 @@ document.addEventListener('DOMContentLoaded', function() {
const giepFieldsContainer = document.getElementById('editGiepSpecificFieldsContainer');
if (data.is_giep) {
if (giepFieldsContainer) giepFieldsContainer.classList.remove('d-none');
if (typeof window.setGiepFormData === 'function') {
const editModal = document.getElementById('editInspectionModal');
if (editModal && typeof editModal.setGiepFormData === 'function') {
editModal.setGiepFormData(data);
} else if (typeof window.setGiepFormData === 'function') {
window.setGiepFormData(data);
}
} else {

View file

@ -44,7 +44,7 @@
{% for choice in INSPECTION_RESULT_CHOICES_CONFIG %}
{% if choice.code != 'cancelled' %}
<div class="col-6 mb-1" id="res_container_{{ choice.code }}">
<input type="radio" class="btn-check" name="result_status" id="res_{{ choice.code }}" value="{{ choice.code }}" {% if forloop.first %}checked{% endif %}>
<input type="radio" class="btn-check" name="result_status" id="res_{{ choice.code }}" value="{{ choice.code }}">
<label class="btn {{ choice.btn_class }} w-100 py-2 d-flex align-items-center justify-content-center gap-1" for="res_{{ choice.code }}" id="res_label_{{ choice.code }}">
<i class="bi {{ choice.icon }}"></i>
<span>{{ choice.label }}</span>

View file

@ -204,6 +204,20 @@ class InspectionModelAndViewsTestCase(TestCase):
self.assertFalse(data['success'])
self.assertIn("Absent / Retiré", data['error'])
def test_create_inspection_api_requires_result_status(self):
self.client.login(username='inspector_user', password='password123')
url = reverse('inspections:create_inspection_api')
response = self.client.post(url, {
'content_type_id': self.spot_ct.id,
'object_id': self.spot.id,
'nature': 'visual',
'inspector_role': 'provider',
})
self.assertEqual(response.status_code, 400)
data = response.json()
self.assertFalse(data['success'])
self.assertIn("Veuillez sélectionner le résultat", data['error'])
def test_update_giep_inspection_api_rejects_missing(self):
self.client.login(username='inspector_user', password='password123')
insp = GiepInspection.objects.create(

View file

@ -482,7 +482,9 @@ def create_inspection_api(request):
"""
content_type_id = request.POST.get('content_type_id')
object_id = request.POST.get('object_id')
result_status = request.POST.get('result_status', 'compliant')
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()