refactor: improve inspection document UI with file support, lazy loading, and translation fixes.

This commit is contained in:
kdeterme 2026-09-09 10:40:09 +02:00
parent 1c7e369fe3
commit d9f1970ca6
11 changed files with 126 additions and 15 deletions

View file

@ -3658,8 +3658,8 @@ msgid "Actions"
msgstr "Action" msgstr "Action"
#: assets/templates/assets/buildings/publicbuilding_detail.html:347 #: assets/templates/assets/buildings/publicbuilding_detail.html:347
msgid "Détail de l\\" msgid "Détail de l'asset"
msgstr "Detail of " msgstr "Asset detail"
#: assets/templates/assets/buildings/publicbuilding_detail.html:354 #: assets/templates/assets/buildings/publicbuilding_detail.html:354
msgid "Aucun asset ou équipement technique enregistré." msgid "Aucun asset ou équipement technique enregistré."
@ -4377,7 +4377,7 @@ msgstr "No inspection or verification recorded for this equipment."
#: assets/templates/assets/components/asset_inspections.html:89 #: assets/templates/assets/components/asset_inspections.html:89
msgid "Détail de l'inspection" msgid "Détail de l'inspection"
msgstr "Detail of " msgstr "Inspection detail"
#: assets/templates/assets/components/asset_inspections.html:98 #: assets/templates/assets/components/asset_inspections.html:98
msgid "Inspection annulée" msgid "Inspection annulée"

View file

@ -3663,8 +3663,8 @@ msgid "Actions"
msgstr "Actions" msgstr "Actions"
#: assets/templates/assets/buildings/publicbuilding_detail.html:347 #: assets/templates/assets/buildings/publicbuilding_detail.html:347
msgid "Détail de l\\" msgid "Détail de l'asset"
msgstr "Détail de l\\" msgstr "Détail de l'asset"
#: assets/templates/assets/buildings/publicbuilding_detail.html:354 #: assets/templates/assets/buildings/publicbuilding_detail.html:354
msgid "Aucun asset ou équipement technique enregistré." msgid "Aucun asset ou équipement technique enregistré."
@ -4383,7 +4383,7 @@ msgstr "Aucune inspection ni vérification enregistrée pour cet équipement."
#: assets/templates/assets/components/asset_inspections.html:89 #: assets/templates/assets/components/asset_inspections.html:89
msgid "Détail de l'inspection" msgid "Détail de l'inspection"
msgstr "Détail de l\\" msgstr "Détail de l'inspection"
#: assets/templates/assets/components/asset_inspections.html:98 #: assets/templates/assets/components/asset_inspections.html:98
msgid "Inspection annulée" msgid "Inspection annulée"

View file

@ -3657,8 +3657,8 @@ msgid "Actions"
msgstr "Actie" msgstr "Actie"
#: assets/templates/assets/buildings/publicbuilding_detail.html:347 #: assets/templates/assets/buildings/publicbuilding_detail.html:347
msgid "Détail de l\\" msgid "Détail de l'asset"
msgstr "Detail van " msgstr "Assetdetail"
#: assets/templates/assets/buildings/publicbuilding_detail.html:354 #: assets/templates/assets/buildings/publicbuilding_detail.html:354
msgid "Aucun asset ou équipement technique enregistré." msgid "Aucun asset ou équipement technique enregistré."
@ -4378,7 +4378,7 @@ msgstr "Geen wijzigingen geregistreerd voor dit asset."
#: assets/templates/assets/components/asset_inspections.html:89 #: assets/templates/assets/components/asset_inspections.html:89
msgid "Détail de l'inspection" msgid "Détail de l'inspection"
msgstr "Detail van " msgstr "Inspectiedetail"
#: assets/templates/assets/components/asset_inspections.html:98 #: assets/templates/assets/components/asset_inspections.html:98
msgid "Inspection annulée" msgid "Inspection annulée"

View file

@ -344,7 +344,7 @@
</td> </td>
<td>{{ eq.next_control_date|date:"d/m/Y"|default:"-" }}</td> <td>{{ eq.next_control_date|date:"d/m/Y"|default:"-" }}</td>
<td class="text-end pe-3"> <td class="text-end pe-3">
<a href="{{ eq.get_absolute_url }}" class="btn btn-sm btn-outline-info py-0 px-2 small text-nowrap" title="{% translate 'Détail de l\'asset' %}"> <a href="{{ eq.get_absolute_url }}" class="btn btn-sm btn-outline-info py-0 px-2 small text-nowrap" title="{% translate \"Détail de l'asset\" %}">
<i class="bi bi-eye me-1"></i>{% translate "Détail" %} <i class="bi bi-eye me-1"></i>{% translate "Détail" %}
</a> </a>
</td> </td>

View file

@ -145,11 +145,18 @@
{% if insp.documents.all %} {% if insp.documents.all %}
<div> <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> <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"> <div class="d-flex flex-wrap gap-2 align-items-center">
{% for doc in insp.documents.all %} {% for doc in insp.documents.all %}
<a href="{{ doc.file_url }}" target="_blank" class="d-inline-block border rounded p-1 bg-light"> {% if doc.is_image %}
<img data-src="{{ doc.file_url }}" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3C/svg%3E" loading="lazy" alt="Photo" class="rounded" style="width: 75px; height: 75px; object-fit: cover;"> <a href="{{ doc.file_url }}" target="_blank" class="d-inline-block border rounded p-1 bg-light" title="{{ doc.filename }}">
</a> <img data-src="{{ doc.file_url }}" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3C/svg%3E" loading="lazy" alt="{{ doc.filename }}" class="rounded" style="width: 75px; height: 75px; object-fit: cover;">
</a>
{% else %}
<a href="{{ doc.file_url }}" target="_blank" class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center gap-1" title="{{ doc.filename }}">
<i class="bi bi-file-earmark-arrow-down text-primary"></i>
<span class="text-truncate" style="max-width: 160px;">{{ doc.filename }}</span>
</a>
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -260,7 +267,8 @@ document.addEventListener('DOMContentLoaded', function() {
window.location.reload(); window.location.reload();
} else { } else {
if (alertBox) { if (alertBox) {
alertBox.textContent = data.error || "{% filter escapejs %}{% translate 'Erreur lors de l\'enregistrement.' %}{% endfilter %}"; {% translate "Erreur lors de l'enregistrement." as err_save_msg %}
alertBox.textContent = data.error || "{{ err_save_msg|escapejs }}";
alertBox.classList.remove('d-none'); alertBox.classList.remove('d-none');
} }
if (submitBtn) submitBtn.disabled = false; if (submitBtn) submitBtn.disabled = false;
@ -279,6 +287,25 @@ document.addEventListener('DOMContentLoaded', function() {
{% endif %} {% endif %}
<script> <script>
(function() {
function loadLazyModalImages(modal) {
if (!modal || typeof modal.querySelectorAll !== 'function') return;
modal.querySelectorAll('img[data-src]').forEach(function(img) {
const dataSrc = img.getAttribute('data-src');
if (dataSrc) {
img.src = dataSrc;
img.removeAttribute('data-src');
}
});
}
document.addEventListener('show.bs.modal', function(e) {
loadLazyModalImages(e.target);
});
document.addEventListener('shown.bs.modal', function(e) {
loadLazyModalImages(e.target);
});
})();
window.handleToggleCancelAssetInspection = function(inspectionId, action) { window.handleToggleCancelAssetInspection = function(inspectionId, action) {
const confirmMsg = action === 'cancel' const confirmMsg = action === 'cancel'
? "{% filter escapejs %}{% translate 'Êtes-vous sûr de vouloir annuler cette inspection ?' %}{% endfilter %}" ? "{% filter escapejs %}{% translate 'Êtes-vous sûr de vouloir annuler cette inspection ?' %}{% endfilter %}"

View file

@ -156,3 +156,48 @@ class ParkingSpotAndInspectionTest(TestCase):
self.assertEqual(config["parking"]["assets_geojson_url"], reverse("assets:parking_assets_geojson")) self.assertEqual(config["parking"]["assets_geojson_url"], reverse("assets:parking_assets_geojson"))
self.assertEqual(config["parking"]["locations_geojson_url"], reverse("assets:parking_locations_geojson")) self.assertEqual(config["parking"]["locations_geojson_url"], reverse("assets:parking_locations_geojson"))
def test_inspection_modal_title_and_thumbnails(self):
from django.contrib.contenttypes.models import ContentType
from django.core.files.uploadedfile import SimpleUploadedFile
from inspections.models import InspectionDocument
from django.utils.translation import activate
self.user_config.is_intern = True
self.user_config.save()
activate('fr')
# Create an inspection on this parking spot
inspection = ParkingSpotInspection.objects.create(
asset=self.spot,
inspector=self.user,
inspection_date=timezone.now(),
result_status="compliant",
verified_standard_stand_count=3,
notes="Inspection vélo conforme"
)
photo = SimpleUploadedFile("arceau_velo.jpg", b"\xff\xd8\xff\xe0testimage", content_type="image/jpeg")
doc_img = InspectionDocument.objects.create(inspection=inspection, file=photo)
url = reverse('assets:parking_assets_detail', kwargs={'asset_model': 'parkingspot', 'asset_id': self.spot.id})
# Test in French
self.client.cookies['django_language'] = 'fr'
response_fr = self.client.get(url)
self.assertEqual(response_fr.status_code, 200)
content_fr = response_fr.content.decode('utf-8')
self.assertIn("Détail de l'inspection", content_fr)
self.assertNotIn("Détail de \\", content_fr)
# Test in English
self.client.cookies['django_language'] = 'en'
response_en = self.client.get(url)
self.assertEqual(response_en.status_code, 200)
content_en = response_en.content.decode('utf-8')
self.assertIn("Inspection detail", content_en)
# Check thumbnail rendering
self.assertIn('data-src="', content_fr)
self.assertIn(doc_img.filename, content_fr)
self.assertIn('show.bs.modal', content_fr)

View file

@ -372,6 +372,25 @@ if ('serviceWorker' in navigator) {
}); });
}); });
// Lazy loading des images dans les modales Bootstrap
(function() {
function loadLazyModalImages(modal) {
if (!modal || typeof modal.querySelectorAll !== 'function') return;
modal.querySelectorAll('img[data-src]').forEach(function(img) {
const dataSrc = img.getAttribute('data-src');
if (dataSrc) {
img.src = dataSrc;
img.removeAttribute('data-src');
}
});
}
document.addEventListener('show.bs.modal', function(e) {
loadLazyModalImages(e.target);
});
document.addEventListener('shown.bs.modal', function(e) {
loadLazyModalImages(e.target);
});
})();
</script> </script>
<!-- MapLibre GL 5.7.0 (local) --> <!-- MapLibre GL 5.7.0 (local) -->

View file

@ -546,6 +546,26 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
}); });
// Lazy loading des images dans les modales Bootstrap
(function() {
function loadLazyModalImages(modal) {
if (!modal || typeof modal.querySelectorAll !== 'function') return;
modal.querySelectorAll('img[data-src]').forEach(function(img) {
const dataSrc = img.getAttribute('data-src');
if (dataSrc) {
img.src = dataSrc;
img.removeAttribute('data-src');
}
});
}
document.addEventListener('show.bs.modal', function(e) {
loadLazyModalImages(e.target);
});
document.addEventListener('shown.bs.modal', function(e) {
loadLazyModalImages(e.target);
});
})();
</script> </script>
<script src="{% static 'common/help_panel.js' %}"></script> <script src="{% static 'common/help_panel.js' %}"></script>