Добавлено inline редактирование позиций в документе списания
- Реализовано редактирование количества, причины и примечаний прямо в таблице - Кнопка редактирования (карандаш) включает режим редактирования - Кнопка сохранения (галочка) отправляет изменения на сервер через AJAX - Кнопка отмены восстанавливает исходные значения - Автофокус на поле количества при входе в режим редактирования - Spinner при сохранении для визуальной обратной связи - Не нужно удалять и заново добавлять позицию при ошибке в количестве
This commit is contained in:
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user