Добавлено inline редактирование позиций в документе списания

- Реализовано редактирование количества, причины и примечаний прямо в таблице
- Кнопка редактирования (карандаш) включает режим редактирования
- Кнопка сохранения (галочка) отправляет изменения на сервер через AJAX
- Кнопка отмены восстанавливает исходные значения
- Автофокус на поле количества при входе в режим редактирования
- Spinner при сохранении для визуальной обратной связи
- Не нужно удалять и заново добавлять позицию при ошибке в количестве
This commit is contained in:
2025-12-11 00:15:29 +03:00
parent d79c523d09
commit e9fb776b6f

View File

@@ -189,24 +189,60 @@
</thead>
<tbody>
{% for item in document.items.all %}
<tr>
<tr id="item-row-{{ item.id }}" data-item-id="{{ item.id }}">
<td class="px-3 py-2">
<a href="{% url 'products:product-detail' item.product.id %}">{{ item.product.name }}</a>
</td>
<td class="px-3 py-2 text-end">{{ item.quantity }}</td>
<td class="px-3 py-2">
<span class="badge bg-light text-dark">{{ item.get_reason_display }}</span>
<td class="px-3 py-2 text-end">
<span class="item-quantity-display">{{ item.quantity }}</span>
{% if document.can_edit %}
<input type="number" class="form-control form-control-sm item-quantity-input"
value="{{ item.quantity }}" step="0.001" min="0.001"
style="display: none; width: 100px; text-align: right;">
{% endif %}
</td>
<td class="px-3 py-2">
{% if item.notes %}{{ item.notes|truncatechars:50 }}{% else %}-{% endif %}
<span class="item-reason-display"><span class="badge bg-light text-dark">{{ item.get_reason_display }}</span></span>
{% if document.can_edit %}
<select class="form-select form-select-sm item-reason-input" style="display: none;">
{% for value, label in item.REASON_CHOICES %}
<option value="{{ value }}" {% if item.reason == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
{% endif %}
</td>
<td class="px-3 py-2">
<span class="item-notes-display">{% if item.notes %}{{ item.notes|truncatechars:50 }}{% else %}-{% endif %}</span>
{% if document.can_edit %}
<input type="text" class="form-control form-control-sm item-notes-input"
value="{{ item.notes }}" placeholder="Примечания"
style="display: none;">
{% endif %}
</td>
{% if document.can_edit %}
<td class="px-3 py-2 text-end">
<form method="post" action="{% url 'inventory:writeoff-document-remove-item' document.pk item.pk %}" class="d-inline">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Удалить позицию?')">
<div class="btn-group btn-group-sm item-action-buttons">
<button type="button" class="btn btn-outline-primary btn-edit-item" title="Редактировать">
<i class="bi bi-pencil"></i>
</button>
<button type="button" class="btn btn-outline-danger"
onclick="if(confirm('Удалить позицию?')) document.getElementById('delete-form-{{ item.id }}').submit();"
title="Удалить">
<i class="bi bi-trash"></i>
</button>
</div>
<div class="btn-group btn-group-sm item-edit-buttons" style="display: none;">
<button type="button" class="btn btn-success btn-save-item" title="Сохранить">
<i class="bi bi-check-lg"></i>
</button>
<button type="button" class="btn btn-secondary btn-cancel-edit" title="Отменить">
<i class="bi bi-x-lg"></i>
</button>
</div>
<form id="delete-form-{{ item.id }}" method="post"
action="{% url 'inventory:writeoff-document-remove-item' document.pk item.pk %}"
style="display: none;">
{% csrf_token %}
</form>
</td>
{% endif %}
@@ -311,6 +347,149 @@ document.addEventListener('DOMContentLoaded', function() {
if (clearSelectedBtn) {
clearSelectedBtn.addEventListener('click', clearSelectedProduct);
}
// ============================================
// Inline редактирование позиций в таблице
// ============================================
// Хранилище оригинальных значений при редактировании
const originalValues = {};
// Обработчики для кнопок редактирования
document.querySelectorAll('.btn-edit-item').forEach(btn => {
btn.addEventListener('click', function() {
const row = this.closest('tr');
const itemId = row.dataset.itemId;
// Сохраняем оригинальные значения
originalValues[itemId] = {
quantity: row.querySelector('.item-quantity-input').value,
reason: row.querySelector('.item-reason-input').value,
notes: row.querySelector('.item-notes-input').value
};
// Переключаем в режим редактирования
toggleEditMode(row, true);
});
});
// Обработчики для кнопок сохранения
document.querySelectorAll('.btn-save-item').forEach(btn => {
btn.addEventListener('click', function() {
const row = this.closest('tr');
const itemId = row.dataset.itemId;
saveItemChanges(itemId, row);
});
});
// Обработчики для кнопок отмены
document.querySelectorAll('.btn-cancel-edit').forEach(btn => {
btn.addEventListener('click', function() {
const row = this.closest('tr');
const itemId = row.dataset.itemId;
// Восстанавливаем оригинальные значения
if (originalValues[itemId]) {
row.querySelector('.item-quantity-input').value = originalValues[itemId].quantity;
row.querySelector('.item-reason-input').value = originalValues[itemId].reason;
row.querySelector('.item-notes-input').value = originalValues[itemId].notes;
}
// Выходим из режима редактирования
toggleEditMode(row, false);
});
});
/**
* Переключение режима редактирования строки
*/
function toggleEditMode(row, isEditing) {
// Переключаем видимость полей отображения/ввода
row.querySelectorAll('.item-quantity-display, .item-reason-display, .item-notes-display').forEach(el => {
el.style.display = isEditing ? 'none' : '';
});
row.querySelectorAll('.item-quantity-input, .item-reason-input, .item-notes-input').forEach(el => {
el.style.display = isEditing ? '' : 'none';
});
// Переключаем видимость кнопок
row.querySelector('.item-action-buttons').style.display = isEditing ? 'none' : '';
row.querySelector('.item-edit-buttons').style.display = isEditing ? '' : 'none';
// Фокус на поле количества при входе в режим редактирования
if (isEditing) {
const qtyInput = row.querySelector('.item-quantity-input');
if (qtyInput) {
qtyInput.focus();
qtyInput.select();
}
}
}
/**
* Сохранение изменений позиции
*/
function saveItemChanges(itemId, row) {
const quantity = row.querySelector('.item-quantity-input').value;
const reason = row.querySelector('.item-reason-input').value;
const notes = row.querySelector('.item-notes-input').value;
// Валидация
if (!quantity || parseFloat(quantity) <= 0) {
alert('Количество должно быть больше нуля');
return;
}
// Отправляем на сервер
const formData = new FormData();
formData.append('quantity', quantity);
formData.append('reason', reason);
formData.append('notes', notes);
formData.append('csrfmiddlewaretoken', document.querySelector('[name=csrfmiddlewaretoken]').value);
// Блокируем кнопки во время сохранения
const saveBtn = row.querySelector('.btn-save-item');
const cancelBtn = row.querySelector('.btn-cancel-edit');
saveBtn.disabled = true;
cancelBtn.disabled = true;
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status"></span>';
fetch(`/inventory/writeoff-documents/{{ document.pk }}/update-item/${itemId}/`, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Обновляем отображение
row.querySelector('.item-quantity-display').textContent = quantity;
const reasonSelect = row.querySelector('.item-reason-input');
const reasonLabel = reasonSelect.options[reasonSelect.selectedIndex].text;
row.querySelector('.item-reason-display').innerHTML = `<span class="badge bg-light text-dark">${reasonLabel}</span>`;
row.querySelector('.item-notes-display').textContent = notes || '-';
// Выходим из режима редактирования
toggleEditMode(row, false);
// Показываем успешное сообщение (опционально)
// alert(data.message);
} else {
alert('Ошибка: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Произошла ошибка при сохранении');
})
.finally(() => {
saveBtn.disabled = false;
cancelBtn.disabled = false;
saveBtn.innerHTML = '<i class="bi bi-check-lg"></i>';
});
}
});
</script>
{% endblock %}