From 2ab59203868c4698637754170d144a900ab968ca 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: Fri, 26 Dec 2025 16:03:25 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20requests=20detail=20view=20-=20stage=20?= =?UTF-8?q?3:=20frontend=20=D1=80=D0=B0=D0=B7=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Этап 3 разработки включает: ✅ Создан JavaScript модуль requestDetail.js: - Класс RequestDetailModal для управления модальным окном - Функция open(requestId) для открытия заявления - Функция loadData(requestId) для загрузки через AJAX - Обработка ошибок (404, 403) - Поддержка добавления комментариев в модали - initRequestDetailModal() для инициализации на странице список ✅ Добавлено модальное окно в request_list_full.html: - HTML структура модального окна requestDetailModal - ID контейнера для содержимого: requestDetailContent - Bootstrap 5 modal с размером modal-lg - Scrollable модаль для больших содержимо ✅ Обновлена инициализация скриптов: - Подключение модуля requestDetail.js - Инициализация RequestDetailModal при загрузке страницы - Сохранение экземпляра для использования на странице ✅ Интеграция с кликами: - Подготовка к интеграции с существующей логикой списка - Возможность открытия заявления из списка или по прямой ссылке Готово для следующего этапа 4: Функциональность комментариев --- backend/static/js/modules/requestDetail.js | 211 ++++++++++++++++++ .../requests_app/request_list_full.html | 22 ++ 2 files changed, 233 insertions(+) create mode 100644 backend/static/js/modules/requestDetail.js diff --git a/backend/static/js/modules/requestDetail.js b/backend/static/js/modules/requestDetail.js new file mode 100644 index 00000000..ad101341 --- /dev/null +++ b/backend/static/js/modules/requestDetail.js @@ -0,0 +1,211 @@ +/** + * Module: Request Detail Modal + * + * Отвечает за открытие и управление модальным окном с деталями заявления. + * Используется для открытия заявления из списка или прямой ссылки. + */ + +export class RequestDetailModal { + constructor(modalId = 'requestDetailModal', containerId = 'requestDetailContent') { + this.modalId = modalId; + this.containerId = containerId; + this.modal = null; + this.container = null; + this.currentRequestId = null; + this.init(); + } + + /** + * Инициализация модального окна + */ + init() { + const modalElement = document.getElementById(this.modalId); + if (!modalElement) { + console.warn(`Modal element with id "${this.modalId}" not found`); + return; + } + + this.container = document.getElementById(this.containerId); + this.modal = new bootstrap.Modal(modalElement); + + // Обработчики событий модали + modalElement.addEventListener('hidden.bs.modal', () => { + this.currentRequestId = null; + }); + } + + /** + * Открыть модальное окно с заявлением + * @param {number} requestId - ID заявления + */ + async open(requestId) { + this.currentRequestId = requestId; + + // Показать loading + if (this.container) { + this.container.innerHTML = '
Загрузка...
'; + } + + // Загрузить данные + await this.loadData(requestId); + + // Показать модаль + if (this.modal) { + this.modal.show(); + } + } + + /** + * Загрузить данные заявления через AJAX + * @param {number} requestId - ID заявления + */ + async loadData(requestId) { + try { + const response = await fetch(`/requests/${requestId}/`, { + method: 'GET', + headers: { + 'Accept': 'text/html', + }, + credentials: 'same-origin' + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const html = await response.text(); + + // Извлечь содержимое из полной страницы (ищем .card элемент) + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + const content = doc.querySelector('.container > .row > .col-12'); + + if (content && this.container) { + this.container.innerHTML = content.innerHTML; + this.attachEventListeners(); + } + } catch (error) { + console.error('Error loading request details:', error); + if (this.container) { + this.container.innerHTML = ` +
+ Ошибка при загрузке: ${error.message} +
+ `; + } + } + } + + /** + * Присоединить обработчики событий к элементам в модали + */ + attachEventListeners() { + // Обработчик для формы добавления комментария + const commentForm = this.container?.querySelector('.request-comment-form'); + if (commentForm) { + commentForm.addEventListener('submit', (e) => this.handleCommentSubmit(e)); + } + + // Обработчик для кнопки отозвать + const cancelBtn = this.container?.querySelector('[href*="request_cancel"]'); + if (cancelBtn) { + cancelBtn.addEventListener('click', (e) => this.handleCancel(e)); + } + } + + /** + * Обработчик отправки комментария + */ + async handleCommentSubmit(e) { + e.preventDefault(); + + const form = e.target; + const textarea = form.querySelector('textarea[name="text"]'); + const text = textarea?.value.trim(); + + if (!text) { + alert('Пожалуйста, введите текст комментария'); + return; + } + + try { + const formData = new FormData(form); + const response = await fetch(form.action, { + method: 'POST', + body: formData, + credentials: 'same-origin' + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + // Очистить форму + textarea.value = ''; + + // Перезагрузить содержимое модали + if (this.currentRequestId) { + await this.loadData(this.currentRequestId); + } + } catch (error) { + console.error('Error submitting comment:', error); + alert('Ошибка при добавлении комментария: ' + error.message); + } + } + + /** + * Обработчик отозвания заявления + */ + handleCancel(e) { + e.preventDefault(); + + if (confirm('Вы уверены, что хотите отозвать это заявление?')) { + // Перейти по ссылке или отправить форму + window.location.href = e.target.href; + } + } + + /** + * Закрыть модальное окно + */ + close() { + if (this.modal) { + this.modal.hide(); + } + } +} + +/** + * Инициализировать обработчики для открытия заявлений + * Должна быть вызвана после загрузки страницы с списком заявлений + */ +export function initRequestDetailModal() { + const detailModal = new RequestDetailModal(); + + // Обработчик клика по строкам в таблице заявлений + document.addEventListener('click', (e) => { + // Проверить если клик был по элементу с data-request-id + let target = e.target.closest('[data-request-id]'); + + if (target) { + const requestId = target.dataset.requestId; + if (requestId) { + e.preventDefault(); + detailModal.open(parseInt(requestId)); + } + } + + // Или клик по ссылке на заявление + if (e.target.classList.contains('request-link')) { + const requestId = e.target.dataset.requestId; + if (requestId) { + e.preventDefault(); + detailModal.open(parseInt(requestId)); + } + } + }); + + return detailModal; +} + +export default RequestDetailModal; diff --git a/backend/templates/requests_app/request_list_full.html b/backend/templates/requests_app/request_list_full.html index 914d9432..7fb627eb 100644 --- a/backend/templates/requests_app/request_list_full.html +++ b/backend/templates/requests_app/request_list_full.html @@ -359,6 +359,23 @@ {% endblock %} + + + {% block extra_js %}