/** * 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)); } // Обработчик для кнопок удаления комментариев const deleteCommentForms = this.container?.querySelectorAll('[action*="request_comment_delete"]'); if (deleteCommentForms) { deleteCommentForms.forEach(form => { form.addEventListener('submit', (e) => this.handleDeleteComment(e)); }); } // Обработчик для кнопок смены статуса const statusForm = this.container?.querySelector('.request-status-form'); if (statusForm) { statusForm.addEventListener('submit', (e) => this.handleStatusChange(e)); } // Инициализация emoji picker если доступна this.initEmojiPicker(); // Обновить счетчик комментариев this.updateCommentsCount(); } /** * Обработчик отправки комментария */ 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; } // Показать состояние загрузки const submitBtn = form.querySelector('button[type="submit"]'); const originalBtnText = submitBtn?.textContent || 'Отправить'; if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = 'Отправка...'; } try { const response = await fetch(form.action, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': form.querySelector('[name="csrfmiddlewaretoken"]')?.value || '' }, body: JSON.stringify({ text: text }), credentials: 'same-origin' }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.detail || `HTTP ${response.status}`); } // Очистить форму textarea.value = ''; // Показать успешное сообщение if (submitBtn) { const originalClass = submitBtn.className; submitBtn.className = submitBtn.className.replace('btn-primary', 'btn-success'); submitBtn.innerHTML = 'Комментарий добавлен!'; // Вернуть кнопку в исходное состояние setTimeout(() => { submitBtn.className = originalClass; submitBtn.innerHTML = originalBtnText; submitBtn.disabled = false; }, 2000); } // Перезагрузить содержимое модали if (this.currentRequestId) { // Небольшая задержка для лучшего UX setTimeout(() => { this.loadData(this.currentRequestId); this.updateCommentsCount(); }, 500); } } catch (error) { console.error('Error submitting comment:', error); // Восстановить кнопку при ошибке if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = originalBtnText; } // Показать ошибку в UI const alertDiv = form.querySelector('.alert'); if (alertDiv) { alertDiv.remove(); } const errorAlert = document.createElement('div'); errorAlert.className = 'alert alert-danger alert-dismissible fade show mb-2'; errorAlert.innerHTML = ` Ошибка: ${error.message} `; form.parentNode.insertBefore(errorAlert, form); } } /** * Обработчик удаления комментария */ async handleDeleteComment(e) { e.preventDefault(); if (!confirm('Вы уверены, что хотите удалить этот комментарий?')) { return; } const form = e.target; try { const response = await fetch(form.action, { method: 'POST', headers: { 'X-CSRFToken': form.querySelector('[name="csrfmiddlewaretoken"]')?.value || '' }, credentials: 'same-origin' }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } // Перезагрузить содержимое модали if (this.currentRequestId) { await this.loadData(this.currentRequestId); this.updateCommentsCount(); } } catch (error) { console.error('Error deleting comment:', error); alert('Ошибка при удалении комментария: ' + error.message); } } /** * Инициализировать emoji picker если доступен */ initEmojiPicker() { const emojiPicker = this.container?.querySelector('emoji-picker'); const textarea = this.container?.querySelector('textarea[name="text"]'); if (emojiPicker && textarea) { emojiPicker.addEventListener('emoji-click', (e) => { textarea.value += e.detail.emoji.native; textarea.focus(); }); } } /** * Обновить счетчик комментариев */ updateCommentsCount() { const commentsContainer = this.container?.querySelector('.d-flex.flex-column.gap-2.mb-3'); const countSpan = this.container?.querySelector('.comments-count'); if (commentsContainer && countSpan) { const commentCount = commentsContainer.querySelectorAll('.bg-body-tertiary.border').length; countSpan.textContent = commentCount; } } /** * Обработчик изменения статуса заявления */ async handleStatusChange(e) { e.preventDefault(); const form = e.target; const selectField = form.querySelector('select[name="status"]'); const newStatus = selectField?.value; if (!newStatus) { alert('Пожалуйста, выберите новый статус'); return; } const confirmMsg = form.dataset.confirmMsg || `Вы уверены, что хотите изменить статус на "${form.querySelector(`option[value="${newStatus}"]`)?.textContent}"?`; if (!confirm(confirmMsg)) { return; } // Показать состояние загрузки const submitBtn = form.querySelector('button[type="submit"]'); const originalBtnText = submitBtn?.textContent || 'Применить'; if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = 'Обновление...'; } try { // Использовать правильный endpoint для изменения статуса const action = newStatus === 'approved' ? 'approve' : newStatus === 'rejected' ? 'reject' : newStatus === 'cancelled' ? 'cancel' : null; if (!action) { throw new Error('Неподдерживаемый статус'); } const response = await fetch( `/api/v1/requests/${this.currentRequestId}/${action}/`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': form.querySelector('[name="csrfmiddlewaretoken"]')?.value || '' }, credentials: 'same-origin' } ); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.detail || `HTTP ${response.status}`); } // Показать успешное сообщение if (submitBtn) { const originalClass = submitBtn.className; submitBtn.className = submitBtn.className.replace('btn-primary', 'btn-success'); submitBtn.innerHTML = 'Статус обновлен!'; // Вернуть кнопку в исходное состояние setTimeout(() => { submitBtn.className = originalClass; submitBtn.innerHTML = originalBtnText; submitBtn.disabled = false; }, 2000); } // Перезагрузить содержимое модали if (this.currentRequestId) { setTimeout(() => { this.loadData(this.currentRequestId); }, 500); } } catch (error) { console.error('Error changing status:', error); // Восстановить кнопку при ошибке if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = originalBtnText; } // Показать ошибку в UI const alertDiv = form.querySelector('.alert'); if (alertDiv) { alertDiv.remove(); } const errorAlert = document.createElement('div'); errorAlert.className = 'alert alert-danger alert-dismissible fade show mb-2'; errorAlert.innerHTML = ` Ошибка: ${error.message} `; form.parentNode.insertBefore(errorAlert, form); } } /** * Закрыть модальное окно */ 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;