feat: implement mobile-optimized intervention list view with user-specific preference persistence and associated tests

This commit is contained in:
Karl Determe 2026-09-01 15:45:28 +02:00
parent fecd7430ad
commit da038b7c4f
3 changed files with 101 additions and 44 deletions

View file

@ -39,7 +39,8 @@
</div>
{% for intervention in interventions %}
<div class="card mb-2 shadow-sm" data-href="{% url 'mobile:intervention_detail_mobile' intervention.id %}">
<div class="card-body d-flex justify-content-between align-items-center">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-title mb-1">
<i class="bi bi-gear
@ -87,6 +88,37 @@
{% endif %}
</div>
</div>
{% if intervention.preparation_orders.all %}
<div class="d-flex flex-wrap align-items-center gap-1 mt-2 pt-2 border-top">
<i class="bi bi-box-seam text-primary me-1" style="font-size:0.8rem;" title="{% translate 'Matériel / Stock' %}"></i>
{% for order in intervention.preparation_orders.all %}
{% if order.is_loan %}
<span class="badge bg-primary-subtle text-primary border border-primary-subtle" style="font-size:0.7rem;">
<i class="bi bi-arrow-left-right"></i> {% translate "Prêt" %}
</span>
{% endif %}
{% for item in order.preparationorderitem_set.all|slice:":3" %}
<span class="badge bg-light text-dark border text-truncate" style="font-size:0.7rem; max-width: 160px;" title="{{ item.product.name }} &times; {{ item.quantity }}">
{{ item.product.name }} &times; {{ item.quantity }}
</span>
{% endfor %}
{% for item in order.other_items.all|slice:":2" %}
<span class="badge bg-light text-dark border text-truncate" style="font-size:0.7rem; max-width: 160px;" title="{{ item.name }} &times; {{ item.quantity }}">
{{ item.name }} &times; {{ item.quantity }}
</span>
{% endfor %}
{% with total_count=order.preparationorderitem_set.all|length|add:order.other_items.all|length %}
{% if total_count > 3 %}
<span class="badge bg-secondary-subtle text-muted" style="font-size:0.7rem;">
+{{ total_count|add:"-3" }}
</span>
{% endif %}
{% endwith %}
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% empty %}
<p class="text-muted">{% translate "Aucune intervention trouvée." %}</p>

View file

@ -364,6 +364,12 @@ def interventions_index_mobile(request):
# Apply limit (slice) at the very end, after all filtering
interventions = interventions[:100]
# Preload preparation orders and items to avoid N+1 queries in mobile list cards
interventions = interventions.prefetch_related(
'preparation_orders__preparationorderitem_set__product',
'preparation_orders__other_items',
)
# Evaluate queryset once to avoid double DB round-trip
interventions_list = list(interventions)

View file

@ -172,4 +172,23 @@ class MobileInterventionDetailStockTests(TestCase):
self.assertFalse(response.context["can_create_preparation_order"])
self.assertEqual(len(response.context["preparation_orders"]), 1)
def test_stock_items_rendered_in_mobile_interventions_list(self):
url = reverse("mobile:interventions_index_mobile")
response = self.client.get(url, follow=True)
self.assertEqual(response.status_code, 200)
content = response.content.decode("utf-8")
self.assertIn("Poteau feux", content)
self.assertIn("&times; 3", content)
def test_stock_items_rendered_in_mobile_ajax_list(self):
url = reverse("mobile:interventions_index_mobile") + "?status=in_progress"
response = self.client.get(url, headers={"x-requested-with": "XMLHttpRequest"})
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertIn("html", payload)
self.assertIn("Poteau feux", payload["html"])
self.assertIn("&times; 3", payload["html"])