feat: implement per-order toggle-all material status and real-time usage statistics UI

This commit is contained in:
kdeterme 2026-09-11 11:00:46 +02:00
parent 26c07a769f
commit fd10b3f8f7
4 changed files with 105 additions and 23 deletions

View file

@ -68,14 +68,9 @@
<div class="d-flex align-items-center gap-2">
{% with items=order.preparationorderitem_set.all other_items=order.other_items.all %}
{% if items or other_items %}
<button type="button"
class="btn btn-sm btn-outline-secondary py-1 px-2"
style="font-size:0.75rem;"
onclick="toggleAllMaterialUsed('{{ intervention.id }}', '{{ order.id }}')"
{% if not can_edit_intervention %}disabled{% endif %}
title="{% translate 'Cocher / décocher tout pour ce bon' %}">
<i class="bi bi-check2-all"></i> {% translate "Tout cocher (ce bon)" %}
</button>
<span class="badge bg-white text-dark border small" id="order-stats-badge-{{ order.id }}">
<span id="order-used-count-{{ order.id }}">{{ order.used_items_count }}</span> / {{ order.total_items_count }} {% translate "utilisé(s)" %}
</span>
{% endif %}
{% endwith %}
<a href="{% url 'stock:preparation_order_detail' order.pk %}"
@ -92,7 +87,20 @@
<table class="table table-sm table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th style="width: 100px;" class="text-center">{% translate "Utilisé ?" %}</th>
<th style="width: 130px;" class="text-center align-middle py-2">
<button type="button"
class="btn btn-sm btn-outline-success d-inline-flex align-items-center gap-1 py-1 px-2 btn-order-toggle-all"
id="btn-order-toggle-all-{{ order.id }}"
onclick="toggleAllMaterialUsed('{{ intervention.id }}', '{{ order.id }}')"
{% if not can_edit_intervention %}disabled title="{% translate 'Permissions insuffisantes' %}"{% else %}title="{% translate 'Tout cocher ou tout décocher pour ce bon' %}"{% endif %}
style="font-size: 0.75rem;">
<i class="bi bi-check-all fs-6"></i>
<span class="btn-order-toggle-all-text" id="btn-order-toggle-all-text-{{ order.id }}">
{% if order.all_items_used %}{% translate "Tout décocher" %}{% else %}{% translate "Tout cocher" %}{% endif %}
</span>
</button>
<div class="text-muted" style="font-size: 0.7rem; font-weight: normal; margin-top: 2px;">{% translate "Utilisé ?" %}</div>
</th>
<th>{% translate "Article" %}</th>
<th style="width: 150px;">{% translate "Référence" %}</th>
<th style="width: 120px;" class="text-center">{% translate "Quantité" %}</th>

View file

@ -612,6 +612,18 @@ function updateMaterialUI(data) {
el.textContent = data.all_used ? "{% translate 'Tout décocher' %}" : "{% translate 'Tout cocher' %}";
});
if (data.order_id) {
const orderAllUsed = data.order_all_used !== undefined ? data.order_all_used : data.all_used;
const orderBtnTexts = document.querySelectorAll(`#btn-order-toggle-all-text-${data.order_id}, #btn-mobile-order-toggle-all-text-${data.order_id}`);
orderBtnTexts.forEach(el => {
el.textContent = orderAllUsed ? "{% translate 'Tout décocher' %}" : "{% translate 'Tout cocher' %}";
});
if (data.order_used_count !== undefined) {
const orderUsedCountEls = document.querySelectorAll(`#order-used-count-${data.order_id}, #mobile-order-used-count-${data.order_id}`);
orderUsedCountEls.forEach(el => el.textContent = data.order_used_count);
}
}
if (data.item_id !== undefined && data.is_other !== undefined) {
const prefix = data.is_other ? 'other-' : 'item-';
const badges = document.querySelectorAll(`#material-badge-${prefix}${data.item_id}, #mobile-material-item-badge-${prefix}${data.item_id}`);
@ -670,7 +682,7 @@ function toggleAllMaterialUsed(interventionId, orderId) {
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value
|| document.cookie.split('; ').find(r => r.startsWith('csrftoken='))?.split('=')[1] || '';
const buttons = document.querySelectorAll('#btn-desktop-toggle-all-material, #btn-mobile-toggle-all-material');
const buttons = document.querySelectorAll('#btn-desktop-toggle-all-material, #btn-mobile-toggle-all-material, .btn-order-toggle-all');
buttons.forEach(b => b.disabled = true);
const formData = new FormData();
@ -712,6 +724,27 @@ function toggleAllMaterialUsed(interventionId, orderId) {
});
});
if (orderId) {
const orderBtnTexts = document.querySelectorAll(`#btn-order-toggle-all-text-${orderId}, #btn-mobile-order-toggle-all-text-${orderId}`);
orderBtnTexts.forEach(el => {
el.textContent = isAllUsed ? "{% translate 'Tout décocher' %}" : "{% translate 'Tout cocher' %}";
});
if (data.order_used_count !== undefined) {
const orderUsedCountEls = document.querySelectorAll(`#order-used-count-${orderId}, #mobile-order-used-count-${orderId}`);
orderUsedCountEls.forEach(el => el.textContent = data.order_used_count);
}
} else {
document.querySelectorAll('.btn-order-toggle-all-text').forEach(el => {
el.textContent = isAllUsed ? "{% translate 'Tout décocher' %}" : "{% translate 'Tout cocher' %}";
});
document.querySelectorAll('.btn-order-toggle-all').forEach(btn => {
const id = btn.id.replace('btn-order-toggle-all-', '').replace('btn-mobile-order-toggle-all-', '');
const totalCheckboxes = document.querySelectorAll(`.material-used-checkbox[data-order-id="${id}"]`).length;
const orderUsedCountEls = document.querySelectorAll(`#order-used-count-${id}, #mobile-order-used-count-${id}`);
orderUsedCountEls.forEach(el => el.textContent = isAllUsed ? totalCheckboxes : 0);
});
}
updateMaterialUI({
used_count: data.used_count,
total_count: data.total_count,

View file

@ -1156,18 +1156,6 @@
</div>
</div>
<div class="d-flex align-items-center gap-1">
{% with items=order.preparationorderitem_set.all other_items=order.other_items.all %}
{% if items or other_items %}
<button type="button"
class="btn btn-sm btn-outline-secondary py-0 px-2"
style="font-size:0.7rem;"
onclick="toggleAllMaterialUsed('{{ intervention.id }}', '{{ order.id }}')"
{% if not can_edit_intervention %}disabled{% endif %}
title="{% translate 'Cocher / décocher tout pour ce bon' %}">
<i class="bi bi-check2-all"></i>
</button>
{% endif %}
{% endwith %}
<a href="{% url 'mobile:stock_preparation_order_detail_mobile' order.pk %}"
class="btn btn-sm btn-outline-primary p-1"
title="{% translate 'Voir les détails' %}">
@ -1179,6 +1167,22 @@
{% with items=order.preparationorderitem_set.all other_items=order.other_items.all %}
{% if items or other_items %}
<div class="list-group list-group-flush">
<div class="list-group-item px-3 py-2 bg-light border-bottom d-flex align-items-center justify-content-between">
<button type="button"
class="btn btn-sm btn-outline-success d-inline-flex align-items-center gap-1 py-1 px-2 btn-order-toggle-all"
id="btn-mobile-order-toggle-all-{{ order.id }}"
onclick="toggleAllMaterialUsed('{{ intervention.id }}', '{{ order.id }}')"
{% if not can_edit_intervention %}disabled{% endif %}
style="font-size: 0.75rem;">
<i class="bi bi-check-all fs-6"></i>
<span class="btn-order-toggle-all-text" id="btn-mobile-order-toggle-all-text-{{ order.id }}">
{% if order.all_items_used %}{% translate "Tout décocher" %}{% else %}{% translate "Tout cocher" %}{% endif %}
</span>
</button>
<span class="badge bg-white text-dark border font-monospace" id="mobile-order-stats-badge-{{ order.id }}" style="font-size: 0.75rem;">
<span id="mobile-order-used-count-{{ order.id }}">{{ order.used_items_count }}</span> / {{ order.total_items_count }} {% translate "utilisé(s)" %}
</span>
</div>
{% for item in items %}
<div class="list-group-item px-3 py-2 d-flex align-items-center justify-content-between gap-2">
<div class="d-flex align-items-center gap-2 flex-grow-1" style="min-width: 0;">
@ -6933,6 +6937,18 @@ function updateMaterialUI(data) {
el.textContent = data.all_used ? "{% translate 'Tout décocher' %}" : "{% translate 'Tout cocher' %}";
});
if (data.order_id) {
const orderAllUsed = data.order_all_used !== undefined ? data.order_all_used : data.all_used;
const orderBtnTexts = document.querySelectorAll(`#btn-order-toggle-all-text-${data.order_id}, #btn-mobile-order-toggle-all-text-${data.order_id}`);
orderBtnTexts.forEach(el => {
el.textContent = orderAllUsed ? "{% translate 'Tout décocher' %}" : "{% translate 'Tout cocher' %}";
});
if (data.order_used_count !== undefined) {
const orderUsedCountEls = document.querySelectorAll(`#order-used-count-${data.order_id}, #mobile-order-used-count-${data.order_id}`);
orderUsedCountEls.forEach(el => el.textContent = data.order_used_count);
}
}
if (data.item_id !== undefined && data.is_other !== undefined) {
const prefix = data.is_other ? 'other-' : 'item-';
const badges = document.querySelectorAll(`#material-badge-${prefix}${data.item_id}, #mobile-material-item-badge-${prefix}${data.item_id}`);
@ -6995,7 +7011,7 @@ function toggleAllMaterialUsed(interventionId, orderId) {
|| document.cookie.split('; ').find(r => r.startsWith('csrftoken='))?.split('=')[1]
|| '';
const buttons = document.querySelectorAll('#btn-desktop-toggle-all-material, #btn-mobile-toggle-all-material');
const buttons = document.querySelectorAll('#btn-desktop-toggle-all-material, #btn-mobile-toggle-all-material, .btn-order-toggle-all');
buttons.forEach(b => b.disabled = true);
const formData = new FormData();
@ -7037,6 +7053,27 @@ function toggleAllMaterialUsed(interventionId, orderId) {
});
});
if (orderId) {
const orderBtnTexts = document.querySelectorAll(`#btn-order-toggle-all-text-${orderId}, #btn-mobile-order-toggle-all-text-${orderId}`);
orderBtnTexts.forEach(el => {
el.textContent = isAllUsed ? "{% translate 'Tout décocher' %}" : "{% translate 'Tout cocher' %}";
});
if (data.order_used_count !== undefined) {
const orderUsedCountEls = document.querySelectorAll(`#order-used-count-${orderId}, #mobile-order-used-count-${orderId}`);
orderUsedCountEls.forEach(el => el.textContent = data.order_used_count);
}
} else {
document.querySelectorAll('.btn-order-toggle-all-text').forEach(el => {
el.textContent = isAllUsed ? "{% translate 'Tout décocher' %}" : "{% translate 'Tout cocher' %}";
});
document.querySelectorAll('.btn-order-toggle-all').forEach(btn => {
const id = btn.id.replace('btn-order-toggle-all-', '').replace('btn-mobile-order-toggle-all-', '');
const totalCheckboxes = document.querySelectorAll(`.material-used-checkbox[data-order-id="${id}"]`).length;
const orderUsedCountEls = document.querySelectorAll(`#order-used-count-${id}, #mobile-order-used-count-${id}`);
orderUsedCountEls.forEach(el => el.textContent = isAllUsed ? totalCheckboxes : 0);
});
}
updateMaterialUI({
used_count: data.used_count,
total_count: data.total_count,

View file

@ -3834,6 +3834,8 @@ def toggle_material_used(request, intervention_id, item_id):
'is_used': item.is_used,
'order_id': order.id,
'order_all_used': order_all_used,
'order_used_count': order.used_items_count,
'order_total_count': order.total_items_count,
'used_count': stats['used'],
'total_count': stats['total'],
'all_used': stats['all_used'],
@ -3891,5 +3893,7 @@ def toggle_all_material_used(request, intervention_id):
'used_count': stats['used'],
'total_count': stats['total'],
'order_id': int(order_id) if order_id else None,
'order_used_count': total_target if target_state else 0,
'order_total_count': total_target,
})