feat: requests detail view - stage 3: frontend разработка

Этап 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: Функциональность комментариев
This commit is contained in:
Игорь Надеин 2025-12-26 16:03:25 +03:00
parent c14ee746c9
commit 2ab5920386
2 changed files with 233 additions and 0 deletions

View File

@ -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 = '<div class="d-flex justify-content-center p-4"><div class="spinner-border" role="status"><span class="visually-hidden">Загрузка...</span></div></div>';
}
// Загрузить данные
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 = `
<div class="alert alert-danger mb-0">
<strong>Ошибка при загрузке:</strong> ${error.message}
</div>
`;
}
}
}
/**
* Присоединить обработчики событий к элементам в модали
*/
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;

View File

@ -359,6 +359,23 @@
{% endblock %}
<!-- Request Detail Modal -->
<div class="modal fade" id="requestDetailModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi-file-earmark-text me-2"></i>Детали заявления
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
</div>
<div class="modal-body" id="requestDetailContent">
<!-- Содержимое загружается здесь -->
</div>
</div>
</div>
</div>
{% block extra_js %}
<script>
// Передаем данные для инициализации страницы
@ -367,7 +384,9 @@
</script>
<script type="module">
import { initRequestListPage } from "{% static 'js/pages/requestList.js' %}";
import { initRequestDetailModal } from "{% static 'js/modules/requestDetail.js' %}";
// Инициализировать список заявлений
initRequestListPage({
apiListUrl: "{% url 'api:v1:request-list' %}",
apiDetailBase: "{% url 'api:v1:request-list' %}",
@ -376,6 +395,9 @@
autoShowComments: {% if comments_for %}true{% else %}false{% endif %},
useAjaxList: true // Включаем AJAX режим для списка
});
// Инициализировать модальное окно для detail view
const requestDetailModal = initRequestDetailModal();
</script>
<script src="{% static 'js/components/requestComments.js' %}"></script>
<!-- Emoji Picker Library -->