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:
parent
c14ee746c9
commit
2ab5920386
211
backend/static/js/modules/requestDetail.js
Normal file
211
backend/static/js/modules/requestDetail.js
Normal 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;
|
||||
@ -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 -->
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user