feat: add dynamic active tab management and URL query param support for warehouse index view

This commit is contained in:
kdeterme 2026-10-12 13:42:21 +02:00
parent 20669d545b
commit 30051938d5
3 changed files with 55 additions and 13 deletions

View file

@ -96,21 +96,21 @@
<div class="col-md-6" id="col-left" style="max-height: calc(100vh - 100px); overflow: auto;"> <div class="col-md-6" id="col-left" style="max-height: calc(100vh - 100px); overflow: auto;">
<ul class="nav nav-tabs" id="tablesTab" role="tablist"> <ul class="nav nav-tabs" id="tablesTab" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link active" id="warehouses-table-tab" data-bs-toggle="tab" data-bs-target="#warehouses-table-content" type="button" role="tab" aria-controls="warehouses-table" aria-selected="true" style="color: black;"><i class="bi bi-buildings"></i> {% translate "Entrepôts" %}</button> <button class="nav-link {% if active_tab == 'warehouses' or not active_tab %}active{% endif %}" id="warehouses-table-tab" data-bs-toggle="tab" data-bs-target="#warehouses-table-content" type="button" role="tab" aria-controls="warehouses-table" aria-selected="{% if active_tab == 'warehouses' or not active_tab %}true{% else %}false{% endif %}" style="color: black;"><i class="bi bi-buildings"></i> {% translate "Entrepôts" %}</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="warehouse-locations-table-tab" data-bs-toggle="tab" data-bs-target="#warehouse-locations-table-content" type="button" role="tab" aria-controls="warehouse-locations-table" aria-selected="false" style="color: black;"><i class="bi bi-geo-alt"></i> {% translate "Emplacements" %}</button> <button class="nav-link {% if active_tab == 'warehouse-locations' %}active{% endif %}" id="warehouse-locations-table-tab" data-bs-toggle="tab" data-bs-target="#warehouse-locations-table-content" type="button" role="tab" aria-controls="warehouse-locations-table" aria-selected="{% if active_tab == 'warehouse-locations' %}true{% else %}false{% endif %}" style="color: black;"><i class="bi bi-geo-alt"></i> {% translate "Emplacements" %}</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="products-table-tab" data-bs-toggle="tab" data-bs-target="#products-table-content" type="button" role="tab" aria-controls="products-table" aria-selected="false" style="color: black;"><i class="bi bi-box-seam"></i> {% translate "Produits" %}</button> <button class="nav-link {% if active_tab == 'products' %}active{% endif %}" id="products-table-tab" data-bs-toggle="tab" data-bs-target="#products-table-content" type="button" role="tab" aria-controls="products-table" aria-selected="{% if active_tab == 'products' %}true{% else %}false{% endif %}" style="color: black;"><i class="bi bi-box-seam"></i> {% translate "Produits" %}</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="serialized-items-table-tab" data-bs-toggle="tab" data-bs-target="#serialized-items-table-content" type="button" role="tab" aria-controls="serialized-items-table" aria-selected="false" style="color: black;"><i class="bi bi-upc-scan"></i> {% translate "Articles tracés" %}</button> <button class="nav-link {% if active_tab == 'serialized-items' %}active{% endif %}" id="serialized-items-table-tab" data-bs-toggle="tab" data-bs-target="#serialized-items-table-content" type="button" role="tab" aria-controls="serialized-items-table" aria-selected="{% if active_tab == 'serialized-items' %}true{% else %}false{% endif %}" style="color: black;"><i class="bi bi-upc-scan"></i> {% translate "Articles tracés" %}</button>
</li> </li>
{# Onglet Conception masqué (modèle conservé, non supprimé) #} {# Onglet Conception masqué (modèle conservé, non supprimé) #}
</ul> </ul>
<div class="tab-content my-2" id="tablesTabContent"> <div class="tab-content my-2" id="tablesTabContent">
<div class="tab-pane fade show active" id="warehouses-table-content" role="tabpanel" aria-labelledby="warehouses-table-tab"> <div class="tab-pane fade {% if active_tab == 'warehouses' or not active_tab %}show active{% endif %}" id="warehouses-table-content" role="tabpanel" aria-labelledby="warehouses-table-tab">
<!-- Spinner --> <!-- Spinner -->
<div id="warehouses-spinner" class="table-spinner"> <div id="warehouses-spinner" class="table-spinner">
<div class="spinner-border text-primary" role="status"> <div class="spinner-border text-primary" role="status">
@ -126,7 +126,7 @@
</table> </table>
</div> </div>
</div> </div>
<div class="tab-pane fade show" id="warehouse-locations-table-content" role="tabpanel" aria-labelledby="warehouse-locations-table-tab"> <div class="tab-pane fade {% if active_tab == 'warehouse-locations' %}show active{% endif %}" id="warehouse-locations-table-content" role="tabpanel" aria-labelledby="warehouse-locations-table-tab">
<div id="warehouse-locations-table-container"> <div id="warehouse-locations-table-container">
<table id="warehouse-locations-table" class="table" <table id="warehouse-locations-table" class="table"
data-height="500" data-height="500"
@ -177,7 +177,7 @@
</table> </table>
</div> </div>
</div> </div>
<div class="tab-pane fade show" id="products-table-content" role="tabpanel" aria-labelledby="products-table-tab"> <div class="tab-pane fade {% if active_tab == 'products' %}show active{% endif %}" id="products-table-content" role="tabpanel" aria-labelledby="products-table-tab">
<!-- Bouton ajout produit --> <!-- Bouton ajout produit -->
{% if can_manage_products %} {% if can_manage_products %}
<div class="d-flex justify-content-end mt-2 mb-1"> <div class="d-flex justify-content-end mt-2 mb-1">
@ -274,7 +274,7 @@
{# <div class="tab-pane fade show" id="designs-table-content" ...> ... </div> #} {# <div class="tab-pane fade show" id="designs-table-content" ...> ... </div> #}
<!-- Onglet Articles tracés --> <!-- Onglet Articles tracés -->
<div class="tab-pane fade" id="serialized-items-table-content" role="tabpanel" aria-labelledby="serialized-items-table-tab"> <div class="tab-pane fade {% if active_tab == 'serialized-items' %}show active{% endif %}" id="serialized-items-table-content" role="tabpanel" aria-labelledby="serialized-items-table-tab">
{% if can_manage_products %} {% if can_manage_products %}
<div class="d-flex justify-content-end mt-2 mb-1"> <div class="d-flex justify-content-end mt-2 mb-1">
<a href="{% url 'stock:serialized_item_create' %}" class="btn btn-sm btn-primary"> <a href="{% url 'stock:serialized_item_create' %}" class="btn btn-sm btn-primary">
@ -284,7 +284,7 @@
{% endif %} {% endif %}
<form method="get" action="{% url 'stock:warehouses' %}" class="card mb-2 border-0 bg-light"> <form method="get" action="{% url 'stock:warehouses' %}" class="card mb-2 border-0 bg-light">
<input type="hidden" name="tab" value="serialized-items-table"> <input type="hidden" name="tab" value="serialized-items">
<div class="card-body py-2 row g-2 align-items-end"> <div class="card-body py-2 row g-2 align-items-end">
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-semibold small mb-1">{% translate "Recherche" %}</label> <label class="form-label fw-semibold small mb-1">{% translate "Recherche" %}</label>
@ -313,7 +313,7 @@
<button type="submit" class="btn btn-sm btn-primary"> <button type="submit" class="btn btn-sm btn-primary">
<i class="bi bi-search"></i> {% translate "Filtrer" %} <i class="bi bi-search"></i> {% translate "Filtrer" %}
</button> </button>
<a href="{% url 'stock:warehouses' %}?tab=serialized-items-table" class="btn btn-sm btn-outline-secondary ms-1"> <a href="{% url 'stock:warehouses' %}?tab=serialized-items" class="btn btn-sm btn-outline-secondary ms-1">
<i class="bi bi-x-circle"></i> {% translate "Réinitialiser" %} <i class="bi bi-x-circle"></i> {% translate "Réinitialiser" %}
</a> </a>
</div> </div>
@ -446,14 +446,27 @@
document.querySelectorAll('#tablesTab button[data-bs-toggle="tab"]').forEach(function(btn) { document.querySelectorAll('#tablesTab button[data-bs-toggle="tab"]').forEach(function(btn) {
btn.addEventListener('shown.bs.tab', function() { btn.addEventListener('shown.bs.tab', function() {
const tabName = btn.id.replace('-table-tab', ''); const tabName = btn.id.replace('-table-tab', '');
history.replaceState(null, '', '?tab=' + tabName); const currentUrl = new URL(window.location.href);
if (currentUrl.searchParams.get('tab') !== tabName) {
if (tabName === 'serialized-items') {
currentUrl.searchParams.set('tab', tabName);
} else {
currentUrl.search = '?tab=' + tabName;
}
history.replaceState(null, '', currentUrl.pathname + currentUrl.search);
}
}); });
}); });
// Activer l'onglet indiqué par ?tab= au chargement de la page // Activer l'onglet indiqué par ?tab= au chargement de la page
const tab = new URLSearchParams(window.location.search).get('tab'); const urlParams = new URLSearchParams(window.location.search);
let tab = urlParams.get('tab');
if (!tab && (urlParams.get('si_q') || urlParams.get('si_product') || urlParams.get('si_status'))) {
tab = 'serialized-items';
}
if (tab) { if (tab) {
const tabBtn = document.getElementById(tab + '-table-tab'); const normalizedTab = tab.replace(/-table$/, '');
const tabBtn = document.getElementById(normalizedTab + '-table-tab') || document.getElementById(tab + '-table-tab');
if (tabBtn) { if (tabBtn) {
tabBtn.click(); tabBtn.click();
tabBtn.blur(); tabBtn.blur();

View file

@ -1162,6 +1162,26 @@ class ProductLocationsTests(TestCase):
self.assertContains(response, "SN-MOB-TEST") self.assertContains(response, "SN-MOB-TEST")
self.assertContains(response, "editItemMobileModal") self.assertContains(response, "editItemMobileModal")
def test_warehouses_index_serialized_items_tab_active(self):
response = self.client.get(reverse("stock:warehouses") + "?tab=serialized-items")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context["active_tab"], "serialized-items")
def test_warehouses_index_serialized_items_table_legacy_tab_active(self):
response = self.client.get(reverse("stock:warehouses") + "?tab=serialized-items-table")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context["active_tab"], "serialized-items")
def test_warehouses_index_filter_by_query_activates_serialized_items_tab(self):
response = self.client.get(reverse("stock:warehouses") + "?si_q=makita")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context["active_tab"], "serialized-items")
def test_warehouses_index_default_tab_is_warehouses(self):
response = self.client.get(reverse("stock:warehouses"))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context["active_tab"], "warehouses")
class AssociateStockLocationsCommandTests(TestCase): class AssociateStockLocationsCommandTests(TestCase):
def setUp(self): def setUp(self):

View file

@ -100,6 +100,14 @@ def warehouses_index(request):
si_query = request.GET.get("si_q", "").strip() si_query = request.GET.get("si_q", "").strip()
si_product_id = request.GET.get("si_product", "").strip() si_product_id = request.GET.get("si_product", "").strip()
si_status = request.GET.get("si_status", "").strip() si_status = request.GET.get("si_status", "").strip()
tab_param = request.GET.get("tab", "").strip()
if tab_param in ("serialized-items", "serialized-items-table") or (not tab_param and (si_query or si_product_id or si_status)):
active_tab = "serialized-items"
elif tab_param in ("products", "warehouse-locations", "warehouses"):
active_tab = tab_param
else:
active_tab = "warehouses"
serialized_items_qs = SerializedItem.objects.select_related( serialized_items_qs = SerializedItem.objects.select_related(
"product", "current_location__warehouse", "current_holder" "product", "current_location__warehouse", "current_holder"
@ -129,6 +137,7 @@ def warehouses_index(request):
"si_product_id": si_product_id, "si_product_id": si_product_id,
"si_status": si_status, "si_status": si_status,
"si_status_choices": SerializedItem.STATUS_CHOICES, "si_status_choices": SerializedItem.STATUS_CHOICES,
"active_tab": active_tab,
} }
return render(request, "stock/warehouses_index.html", context) return render(request, "stock/warehouses_index.html", context)