/**
* 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;