Добавлено inline редактирование позиций в документе списания
- Реализовано редактирование количества, причины и примечаний прямо в таблице - Кнопка редактирования (карандаш) включает режим редактирования - Кнопка сохранения (галочка) отправляет изменения на сервер через AJAX - Кнопка отмены восстанавливает исходные значения - Автофокус на поле количества при входе в режим редактирования - Spinner при сохранении для визуальной обратной связи - Не нужно удалять и заново добавлять позицию при ошибке в количестве
This commit is contained in:
@@ -189,24 +189,60 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item in document.items.all %}
|
{% for item in document.items.all %}
|
||||||
<tr>
|
<tr id="item-row-{{ item.id }}" data-item-id="{{ item.id }}">
|
||||||
<td class="px-3 py-2">
|
<td class="px-3 py-2">
|
||||||
<a href="{% url 'products:product-detail' item.product.id %}">{{ item.product.name }}</a>
|
<a href="{% url 'products:product-detail' item.product.id %}">{{ item.product.name }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2 text-end">{{ item.quantity }}</td>
|
<td class="px-3 py-2 text-end">
|
||||||
<td class="px-3 py-2">
|
<span class="item-quantity-display">{{ item.quantity }}</span>
|
||||||
<span class="badge bg-light text-dark">{{ item.get_reason_display }}</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>
|
||||||
<td class="px-3 py-2">
|
<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>
|
</td>
|
||||||
{% if document.can_edit %}
|
{% if document.can_edit %}
|
||||||
<td class="px-3 py-2 text-end">
|
<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">
|
<div class="btn-group btn-group-sm item-action-buttons">
|
||||||
{% csrf_token %}
|
<button type="button" class="btn btn-outline-primary btn-edit-item" title="Редактировать">
|
||||||
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Удалить позицию?')">
|
<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>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</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>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -311,6 +347,149 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (clearSelectedBtn) {
|
if (clearSelectedBtn) {
|
||||||
clearSelectedBtn.addEventListener('click', clearSelectedProduct);
|
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>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user