feat: add inspection management system including models, views, API endpoints, and front-end modal components
This commit is contained in:
parent
64e93d7ae4
commit
5417ca32b0
8 changed files with 226 additions and 8 deletions
|
|
@ -1397,6 +1397,19 @@
|
||||||
document.getElementById('insp_object_id').value = objId;
|
document.getElementById('insp_object_id').value = objId;
|
||||||
|
|
||||||
let modelName = props.model_name || props.location_type || '';
|
let modelName = props.model_name || props.location_type || '';
|
||||||
|
const rawModel = String(props.model_name || props.location_type || '').toLowerCase();
|
||||||
|
const targetCode = String(props.code || props.target_display || '').toUpperCase();
|
||||||
|
const isZoneRwiz = (
|
||||||
|
rawModel === 'naturerwiz' ||
|
||||||
|
props.location_type === 'NatureRWIZ' ||
|
||||||
|
targetCode.startsWith('RWIZ') ||
|
||||||
|
Boolean(props.is_location && (props.thematic === 'water' || props.thematic === 'giep' || rawModel === 'naturerwiz'))
|
||||||
|
) && (props.model_name !== 'naturerwiasset' && props.asset_type !== 'naturerwiasset' && props.is_location !== false);
|
||||||
|
|
||||||
|
if (typeof window.toggleMissingResultChoice === 'function') {
|
||||||
|
window.toggleMissingResultChoice(isZoneRwiz);
|
||||||
|
}
|
||||||
|
|
||||||
if (modelName === 'NatureRWIZ' || modelName === 'NatureRWIAsset' || modelName === 'naturerwiasset' || props.thematic === 'water' || props.location_type === 'NatureRWIZ') {
|
if (modelName === 'NatureRWIZ' || modelName === 'NatureRWIAsset' || modelName === 'naturerwiasset' || props.thematic === 'water' || props.location_type === 'NatureRWIZ') {
|
||||||
modelName = 'naturerwiz';
|
modelName = 'naturerwiz';
|
||||||
window.currentGiepLocationId = props.location_id || (props.is_location ? objId : null) || objId;
|
window.currentGiepLocationId = props.location_id || (props.is_location ? objId : null) || objId;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#quickInspectionModal"
|
data-bs-target="#quickInspectionModal"
|
||||||
data-content-type-id="{{ asset_ct_id }}"
|
data-content-type-id="{{ asset_ct_id }}"
|
||||||
|
data-model-name="{{ asset_ct_model }}"
|
||||||
data-object-id="{{ asset_pk }}"
|
data-object-id="{{ asset_pk }}"
|
||||||
data-target-code="{{ asset.code|default:asset.pk }}"
|
data-target-code="{{ asset.code|default:asset.pk }}"
|
||||||
data-target-name="{{ asset.name_fr|default:asset.name_nl|default:asset }}"
|
data-target-name="{{ asset.name_fr|default:asset.name_nl|default:asset }}"
|
||||||
|
|
@ -206,6 +207,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
if (modalTypeName) modalTypeName.textContent = name;
|
if (modalTypeName) modalTypeName.textContent = name;
|
||||||
if (modalTypeBadge) modalTypeBadge.textContent = type;
|
if (modalTypeBadge) modalTypeBadge.textContent = type;
|
||||||
if (modalStatusBadge) modalStatusBadge.textContent = status;
|
if (modalStatusBadge) modalStatusBadge.textContent = status;
|
||||||
|
|
||||||
|
const modelName = (this.getAttribute('data-model-name') || '').toLowerCase();
|
||||||
|
const codeUpper = (code || '').toUpperCase();
|
||||||
|
const isRwiz = (modelName === 'naturerwiz' || codeUpper.startsWith('RWIZ'));
|
||||||
|
if (typeof window.toggleMissingResultChoice === 'function') {
|
||||||
|
window.toggleMissingResultChoice(isRwiz);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,9 +64,9 @@
|
||||||
<span>{% translate "Endommagé" %}</span>
|
<span>{% translate "Endommagé" %}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="col-4" id="res_container_missing">
|
||||||
<input type="radio" class="btn-check" name="result_status" id="res_missing" value="missing">
|
<input type="radio" class="btn-check" name="result_status" id="res_missing" value="missing">
|
||||||
<label class="btn btn-outline-dark w-100 py-2 d-flex align-items-center justify-content-center gap-1 small" for="res_missing">
|
<label class="btn btn-outline-dark w-100 py-2 d-flex align-items-center justify-content-center gap-1 small" for="res_missing" id="res_label_missing">
|
||||||
<i class="bi bi-dash-circle-fill"></i>
|
<i class="bi bi-dash-circle-fill"></i>
|
||||||
<span>{% translate "Absent" %}</span>
|
<span>{% translate "Absent" %}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,14 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
|
if (typeof window.toggleMissingResultChoice === 'function') {
|
||||||
|
const container = document.getElementById('giepSpecificFields');
|
||||||
|
const isAssetSpecific = container && container.dataset.assetId && container.dataset.assetId !== '';
|
||||||
|
if (!isAssetSpecific) {
|
||||||
|
window.toggleMissingResultChoice(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createEmptyOuvrage(idx, deviceType = "evc", asset = null) {
|
function createEmptyOuvrage(idx, deviceType = "evc", asset = null) {
|
||||||
return {
|
return {
|
||||||
local_id: String(idx),
|
local_id: String(idx),
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,9 @@
|
||||||
<label class="form-label fw-semibold d-block">{% translate "Résultat de l'inspection" %} <span class="text-danger">*</span></label>
|
<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">
|
<div class="row g-2">
|
||||||
{% for choice in INSPECTION_RESULT_CHOICES_CONFIG %}
|
{% for choice in INSPECTION_RESULT_CHOICES_CONFIG %}
|
||||||
<div class="col-6 mb-1">
|
<div class="col-6 mb-1" id="edit_res_container_{{ choice.code }}">
|
||||||
<input type="radio" class="btn-check" name="result_status" id="edit_res_{{ choice.code }}" value="{{ choice.code }}">
|
<input type="radio" class="btn-check" name="result_status" id="edit_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="edit_res_{{ choice.code }}">
|
<label class="btn {{ choice.btn_class }} w-100 py-2 d-flex align-items-center justify-content-center gap-1" for="edit_res_{{ choice.code }}" id="edit_res_label_{{ choice.code }}">
|
||||||
<i class="bi {{ choice.icon }}"></i>
|
<i class="bi {{ choice.icon }}"></i>
|
||||||
<span>{{ choice.label }}</span>
|
<span>{{ choice.label }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -453,6 +453,45 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
initEditInspectionPhotoHandler();
|
initEditInspectionPhotoHandler();
|
||||||
|
|
||||||
|
window.toggleEditMissingResultChoice = function(isDisabled, tooltipText) {
|
||||||
|
const radio = document.getElementById('edit_res_missing');
|
||||||
|
const label = document.getElementById('edit_res_label_missing') || document.querySelector('label[for="edit_res_missing"]');
|
||||||
|
const container = document.getElementById('edit_res_container_missing') || (radio ? radio.closest('.col-6') : null);
|
||||||
|
const defaultMsg = "{% filter escapejs %}{% translate 'Non applicable pour une zone (RWIZ)' %}{% endfilter %}";
|
||||||
|
const msg = tooltipText || defaultMsg;
|
||||||
|
|
||||||
|
if (radio) {
|
||||||
|
radio.disabled = Boolean(isDisabled);
|
||||||
|
if (isDisabled && radio.checked) {
|
||||||
|
const compliantRadio = document.getElementById('edit_res_compliant');
|
||||||
|
if (compliantRadio) {
|
||||||
|
compliantRadio.checked = true;
|
||||||
|
compliantRadio.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (label) {
|
||||||
|
if (isDisabled) {
|
||||||
|
label.classList.add('disabled');
|
||||||
|
label.style.pointerEvents = 'none';
|
||||||
|
label.style.opacity = '0.35';
|
||||||
|
} else {
|
||||||
|
label.classList.remove('disabled');
|
||||||
|
label.style.pointerEvents = '';
|
||||||
|
label.style.opacity = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (container) {
|
||||||
|
if (isDisabled) {
|
||||||
|
container.style.cursor = 'not-allowed';
|
||||||
|
container.setAttribute('title', msg);
|
||||||
|
} else {
|
||||||
|
container.style.cursor = '';
|
||||||
|
container.removeAttribute('title');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
window.openEditInspectionModal = function(inspectionId) {
|
window.openEditInspectionModal = function(inspectionId) {
|
||||||
if (!inspectionId) return;
|
if (!inspectionId) return;
|
||||||
|
|
||||||
|
|
@ -498,9 +537,20 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
document.getElementById('edit_insp_dates_info').textContent = datesText;
|
document.getElementById('edit_insp_dates_info').textContent = datesText;
|
||||||
document.getElementById('edit_insp_current_status').textContent = data.result_status || '';
|
document.getElementById('edit_insp_current_status').textContent = data.result_status || '';
|
||||||
|
|
||||||
|
if (typeof window.toggleEditMissingResultChoice === 'function') {
|
||||||
|
window.toggleEditMissingResultChoice(Boolean(data.is_zone_rwiz));
|
||||||
|
}
|
||||||
|
|
||||||
// Pre-check result_status radio
|
// Pre-check result_status radio
|
||||||
const rad = document.getElementById('edit_res_' + data.result_status);
|
const rad = document.getElementById('edit_res_' + data.result_status);
|
||||||
if (rad) rad.checked = true;
|
if (rad) {
|
||||||
|
if (data.is_zone_rwiz && data.result_status === 'missing') {
|
||||||
|
const compRad = document.getElementById('edit_res_compliant');
|
||||||
|
if (compRad) compRad.checked = true;
|
||||||
|
} else {
|
||||||
|
rad.checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (document.getElementById('edit_insp_nature')) {
|
if (document.getElementById('edit_insp_nature')) {
|
||||||
document.getElementById('edit_insp_nature').value = data.nature || 'visual';
|
document.getElementById('edit_insp_nature').value = data.nature || 'visual';
|
||||||
|
|
@ -683,5 +733,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const editModalEl = document.getElementById('editInspectionModal');
|
||||||
|
if (editModalEl) {
|
||||||
|
editModalEl.addEventListener('hidden.bs.modal', function() {
|
||||||
|
if (typeof window.toggleEditMissingResultChoice === 'function') {
|
||||||
|
window.toggleEditMissingResultChoice(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -43,9 +43,9 @@
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
{% for choice in INSPECTION_RESULT_CHOICES_CONFIG %}
|
{% for choice in INSPECTION_RESULT_CHOICES_CONFIG %}
|
||||||
{% if choice.code != 'cancelled' %}
|
{% if choice.code != 'cancelled' %}
|
||||||
<div class="col-6 mb-1">
|
<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 }}" {% if forloop.first %}checked{% endif %}>
|
||||||
<label class="btn {{ choice.btn_class }} w-100 py-2 d-flex align-items-center justify-content-center gap-1" for="res_{{ 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>
|
<i class="bi {{ choice.icon }}"></i>
|
||||||
<span>{{ choice.label }}</span>
|
<span>{{ choice.label }}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -168,3 +168,55 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.toggleMissingResultChoice = function(isDisabled, tooltipText) {
|
||||||
|
const radio = document.getElementById('res_missing');
|
||||||
|
const label = document.getElementById('res_label_missing') || document.querySelector('label[for="res_missing"]');
|
||||||
|
const container = document.getElementById('res_container_missing') || (radio ? radio.closest('.col-6, .col-4') : null);
|
||||||
|
const defaultMsg = "{% filter escapejs %}{% translate 'Non applicable pour une zone (RWIZ)' %}{% endfilter %}";
|
||||||
|
const msg = tooltipText || defaultMsg;
|
||||||
|
|
||||||
|
if (radio) {
|
||||||
|
radio.disabled = Boolean(isDisabled);
|
||||||
|
if (isDisabled && radio.checked) {
|
||||||
|
const compliantRadio = document.getElementById('res_compliant');
|
||||||
|
if (compliantRadio) {
|
||||||
|
compliantRadio.checked = true;
|
||||||
|
compliantRadio.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (label) {
|
||||||
|
if (isDisabled) {
|
||||||
|
label.classList.add('disabled');
|
||||||
|
label.style.pointerEvents = 'none';
|
||||||
|
label.style.opacity = '0.35';
|
||||||
|
} else {
|
||||||
|
label.classList.remove('disabled');
|
||||||
|
label.style.pointerEvents = '';
|
||||||
|
label.style.opacity = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (container) {
|
||||||
|
if (isDisabled) {
|
||||||
|
container.style.cursor = 'not-allowed';
|
||||||
|
container.setAttribute('title', msg);
|
||||||
|
} else {
|
||||||
|
container.style.cursor = '';
|
||||||
|
container.removeAttribute('title');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const modalEl = document.getElementById('quickInspectionModal');
|
||||||
|
if (modalEl) {
|
||||||
|
modalEl.addEventListener('hidden.bs.modal', function() {
|
||||||
|
if (typeof window.toggleMissingResultChoice === 'function') {
|
||||||
|
window.toggleMissingResultChoice(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,62 @@ class InspectionModelAndViewsTestCase(TestCase):
|
||||||
self.assertFalse(insp.orage_moins_une_heure)
|
self.assertFalse(insp.orage_moins_une_heure)
|
||||||
self.assertEqual(len(insp.data.get('ouvrages', [])), 1)
|
self.assertEqual(len(insp.data.get('ouvrages', [])), 1)
|
||||||
|
|
||||||
|
def test_create_giep_inspection_api_rejects_missing(self):
|
||||||
|
self.client.login(username='inspector_user', password='password123')
|
||||||
|
url = reverse('inspections:create_inspection_api')
|
||||||
|
response = self.client.post(url, {
|
||||||
|
'content_type_id': self.rwiz_ct.id,
|
||||||
|
'object_id': self.rwiz.id,
|
||||||
|
'result_status': 'missing',
|
||||||
|
'nature': 'visual',
|
||||||
|
'inspector_role': 'observer',
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
data = response.json()
|
||||||
|
self.assertFalse(data['success'])
|
||||||
|
self.assertIn("Absent / Retiré", data['error'])
|
||||||
|
|
||||||
|
def test_update_giep_inspection_api_rejects_missing(self):
|
||||||
|
self.client.login(username='inspector_user', password='password123')
|
||||||
|
insp = GiepInspection.objects.create(
|
||||||
|
asset_content_type=self.rwiz_ct,
|
||||||
|
asset_object_id=self.rwiz.id,
|
||||||
|
inspector=self.user,
|
||||||
|
result_status='compliant'
|
||||||
|
)
|
||||||
|
url = reverse('inspections:update_inspection_api', kwargs={'inspection_id': insp.id})
|
||||||
|
response = self.client.post(url, {
|
||||||
|
'result_status': 'missing'
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
data = response.json()
|
||||||
|
self.assertFalse(data['success'])
|
||||||
|
self.assertIn("Absent / Retiré", data['error'])
|
||||||
|
|
||||||
|
def test_get_inspection_detail_api_returns_is_zone_rwiz(self):
|
||||||
|
self.client.login(username='inspector_user', password='password123')
|
||||||
|
rwiz_insp = GiepInspection.objects.create(
|
||||||
|
asset_content_type=self.rwiz_ct,
|
||||||
|
asset_object_id=self.rwiz.id,
|
||||||
|
inspector=self.user,
|
||||||
|
result_status='compliant'
|
||||||
|
)
|
||||||
|
url_rwiz = reverse('inspections:get_inspection_detail_api', kwargs={'inspection_id': rwiz_insp.id})
|
||||||
|
resp_rwiz = self.client.get(url_rwiz)
|
||||||
|
self.assertEqual(resp_rwiz.status_code, 200)
|
||||||
|
self.assertTrue(resp_rwiz.json()['is_zone_rwiz'])
|
||||||
|
|
||||||
|
spot_insp = ParkingSpotInspection.objects.create(
|
||||||
|
asset_content_type=self.spot_ct,
|
||||||
|
asset_object_id=self.spot.id,
|
||||||
|
inspector=self.user,
|
||||||
|
result_status='compliant'
|
||||||
|
)
|
||||||
|
url_spot = reverse('inspections:get_inspection_detail_api', kwargs={'inspection_id': spot_insp.id})
|
||||||
|
resp_spot = self.client.get(url_spot)
|
||||||
|
self.assertEqual(resp_spot.status_code, 200)
|
||||||
|
self.assertFalse(resp_spot.json()['is_zone_rwiz'])
|
||||||
|
|
||||||
def test_registry_giep_template(self):
|
def test_registry_giep_template(self):
|
||||||
tmpl_rwiz = get_inspection_partial_template('naturerwiz')
|
tmpl_rwiz = get_inspection_partial_template('naturerwiz')
|
||||||
tmpl_asset = get_inspection_partial_template('naturerwiasset')
|
tmpl_asset = get_inspection_partial_template('naturerwiasset')
|
||||||
|
|
|
||||||
|
|
@ -506,6 +506,12 @@ def create_inspection_api(request):
|
||||||
is_giep = (content_type.model == 'naturerwiz')
|
is_giep = (content_type.model == 'naturerwiz')
|
||||||
is_signpanel = (content_type.model == 'signpanel')
|
is_signpanel = (content_type.model == 'signpanel')
|
||||||
|
|
||||||
|
if is_giep and result_status == 'missing':
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': _("Le résultat 'Absent / Retiré' n'est pas applicable à une zone GIEP (RWIZ).")
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if is_parking_spot:
|
if is_parking_spot:
|
||||||
inspection = ParkingSpotInspection(
|
inspection = ParkingSpotInspection(
|
||||||
|
|
@ -748,6 +754,11 @@ def get_inspection_detail_api(request, inspection_id):
|
||||||
except Exception:
|
except Exception:
|
||||||
rendered_detail_html = ''
|
rendered_detail_html = ''
|
||||||
|
|
||||||
|
is_zone_rwiz = bool(
|
||||||
|
(leaf.asset_content_type and leaf.asset_content_type.model == 'naturerwiz') or
|
||||||
|
(asset and asset.__class__.__name__ == 'NatureRWIZ')
|
||||||
|
)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'success': True,
|
'success': True,
|
||||||
'id': leaf.id,
|
'id': leaf.id,
|
||||||
|
|
@ -759,6 +770,7 @@ def get_inspection_detail_api(request, inspection_id):
|
||||||
'thematic_name': thematic_name,
|
'thematic_name': thematic_name,
|
||||||
'thematic_icon': thematic_icon,
|
'thematic_icon': thematic_icon,
|
||||||
'asset_type_name': leaf.asset_content_type.name if leaf.asset_content_type else '',
|
'asset_type_name': leaf.asset_content_type.name if leaf.asset_content_type else '',
|
||||||
|
'is_zone_rwiz': is_zone_rwiz,
|
||||||
'result_status': leaf.result_status,
|
'result_status': leaf.result_status,
|
||||||
'result_status_display': str(leaf.get_result_status_display()),
|
'result_status_display': str(leaf.get_result_status_display()),
|
||||||
'status_badge_class': leaf.status_badge_class,
|
'status_badge_class': leaf.status_badge_class,
|
||||||
|
|
@ -833,7 +845,17 @@ def update_inspection_api(request, inspection_id):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if 'result_status' in request.POST:
|
if 'result_status' in request.POST:
|
||||||
inspection.result_status = request.POST['result_status']
|
new_status = request.POST['result_status']
|
||||||
|
is_zone_rwiz = (
|
||||||
|
isinstance(inspection, GiepInspection) or
|
||||||
|
(inspection.asset_content_type and inspection.asset_content_type.model == 'naturerwiz')
|
||||||
|
)
|
||||||
|
if is_zone_rwiz and new_status == 'missing':
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': _("Le résultat 'Absent / Retiré' n'est pas applicable à une zone GIEP (RWIZ).")
|
||||||
|
}, status=400)
|
||||||
|
inspection.result_status = new_status
|
||||||
if 'nature' in request.POST:
|
if 'nature' in request.POST:
|
||||||
inspection.nature = request.POST['nature']
|
inspection.nature = request.POST['nature']
|
||||||
if 'inspector_role' in request.POST:
|
if 'inspector_role' in request.POST:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue