From 065a8851a18e1ad803b820d1d57ee44e2db687e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Wed, 10 Dec 2025 16:46:47 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=BB=D0=BD=D0=B0?= =?UTF-8?q?=D1=8F=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD=D0=B0?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D1=81=D0=BE=D0=B7?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B8=20=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлены модальные окна для всех действий на странице деталей * Добавление позиции (#addItemModal) * Одобрение заявки (#approveModal) * Отклонение заявки (#rejectModal) - Реализованы JavaScript обработчики для всех кнопок * deleteItem() - удаление позиции * saveItemBtn - сохранение новой позиции * confirmApproveBtn - одобрение с комментарием * confirmRejectBtn - отклонение с обязательным комментарием - Исправлены API endpoints (items вместо request-items) - Доработана форма создания/редактирования заявки * Удалены несуществующие поля (desired_date, estimated_cost) * Добавлен выбор отдела из API * Реализована AJAX отправка формы * Поддержка создания черновика и отправки на согласование * Поддержка редактирования существующих заявок (PATCH) - Добавлен URL и view для редактирования заявок - Кнопка "Редактировать" теперь ведёт на страницу редактирования --- backend/procurement/urls_frontend.py | 1 + backend/procurement/views_frontend.py | 16 ++ .../templates/procurement/request_detail.html | 217 +++++++++++++++++- .../templates/procurement/request_form.html | 208 +++++++++++++++-- 4 files changed, 425 insertions(+), 17 deletions(-) diff --git a/backend/procurement/urls_frontend.py b/backend/procurement/urls_frontend.py index 9f321963..f78f6049 100644 --- a/backend/procurement/urls_frontend.py +++ b/backend/procurement/urls_frontend.py @@ -22,6 +22,7 @@ urlpatterns = [ ), path('orders/create/', views.request_create, name='request_create'), path('orders//', views.request_detail, name='request_detail'), + path('orders//edit/', views.request_edit, name='request_edit'), # Оборудование path('equipment/', views.equipment_list, name='equipment_list'), diff --git a/backend/procurement/views_frontend.py b/backend/procurement/views_frontend.py index f3d00bd6..5dbb2c1b 100644 --- a/backend/procurement/views_frontend.py +++ b/backend/procurement/views_frontend.py @@ -143,6 +143,22 @@ def request_create(request): }) +@login_required +def request_edit(request, pk): + """Страница редактирования заявки. + + Данные загружаются через API: + - /api/v1/procurement/requests/{pk}/ — данные заявки + - /api/v1/departments/ — список отделов + Обновление через: + - PATCH /api/v1/procurement/requests/{pk}/ + """ + return render(request, 'procurement/request_form.html', { + 'page_title': 'Редактирование заявки', + 'request_id': pk, + }) + + @login_required def equipment_list(request): """Список оборудования. diff --git a/backend/templates/procurement/request_detail.html b/backend/templates/procurement/request_detail.html index de3f814f..da837f4f 100644 --- a/backend/templates/procurement/request_detail.html +++ b/backend/templates/procurement/request_detail.html @@ -197,7 +197,7 @@ - + Редактировать {% endif %} @@ -274,6 +274,96 @@ +{# ===== МОДАЛЬНЫЕ ОКНА ===== #} + +{# Модальное окно добавления позиции #} + + +{# Модальное окно одобрения #} + + +{# Модальное окно отклонения #} + + {% endblock %} {% block extra_js %} @@ -354,5 +444,130 @@ alert('Ошибка сети'); } }); + + // Добавление позиции + document.getElementById('saveItemBtn')?.addEventListener('click', async () => { + const form = document.getElementById('addItemForm'); + if (!form.checkValidity()) { + form.reportValidity(); + return; + } + + const formData = new FormData(form); + const data = { + name: formData.get('name'), + description: formData.get('description'), + quantity: parseFloat(formData.get('quantity')), + unit: formData.get('unit'), + estimated_unit_price: parseFloat(formData.get('estimated_unit_price')), + request: requestId + }; + + try { + const resp = await fetch('/api/v1/procurement/items/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || '{{ csrf_token }}' + }, + body: JSON.stringify(data) + }); + + if (resp.ok) { + bootstrap.Modal.getInstance(document.getElementById('addItemModal')).hide(); + form.reset(); + window.location.reload(); + } else { + const err = await resp.json(); + alert(err.error || 'Ошибка добавления позиции'); + } + } catch (e) { + console.error(e); + alert('Ошибка сети'); + } + }); + + // Удаление позиции + async function deleteItem(itemId) { + if (!confirm('Удалить эту позицию?')) return; + + try { + const resp = await fetch(`/api/v1/procurement/items/${itemId}/`, { + method: 'DELETE', + headers: { + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || '{{ csrf_token }}' + } + }); + + if (resp.ok) { + window.location.reload(); + } else { + const err = await resp.json(); + alert(err.error || 'Ошибка удаления'); + } + } catch (e) { + console.error(e); + alert('Ошибка сети'); + } + } + + // Одобрение заявки + document.getElementById('confirmApproveBtn')?.addEventListener('click', async () => { + const comment = document.getElementById('approveComment').value; + + try { + const resp = await fetch(`/api/v1/procurement/requests/${requestId}/approve/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || '{{ csrf_token }}' + }, + body: JSON.stringify({ comment }) + }); + + if (resp.ok) { + bootstrap.Modal.getInstance(document.getElementById('approveModal')).hide(); + window.location.reload(); + } else { + const err = await resp.json(); + alert(err.error || 'Ошибка одобрения'); + } + } catch (e) { + console.error(e); + alert('Ошибка сети'); + } + }); + + // Отклонение заявки + document.getElementById('confirmRejectBtn')?.addEventListener('click', async () => { + const comment = document.getElementById('rejectComment').value; + + if (!comment.trim()) { + alert('Укажите причину отклонения'); + return; + } + + try { + const resp = await fetch(`/api/v1/procurement/requests/${requestId}/reject/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || '{{ csrf_token }}' + }, + body: JSON.stringify({ comment }) + }); + + if (resp.ok) { + bootstrap.Modal.getInstance(document.getElementById('rejectModal')).hide(); + window.location.reload(); + } else { + const err = await resp.json(); + alert(err.error || 'Ошибка отклонения'); + } + } catch (e) { + console.error(e); + alert('Ошибка сети'); + } + }); {% endblock %} diff --git a/backend/templates/procurement/request_form.html b/backend/templates/procurement/request_form.html index 5ecc7527..e2e31933 100644 --- a/backend/templates/procurement/request_form.html +++ b/backend/templates/procurement/request_form.html @@ -29,7 +29,7 @@ -
+ {% csrf_token %}
@@ -48,6 +48,13 @@ maxlength="2000">
+
+ + +
+
- -
- - -
- -
- -
- - ₽ -
-
Можно указать позже
-
@@ -101,3 +93,187 @@ {% endblock %} + +{% block extra_js %} + +{% endblock %}