Corp/backend/static/js/modules/requestDetail.js

470 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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));
}
// Обработчик для кнопок удаления комментариев
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 =
'<span class="spinner-border spinner-border-sm me-2"></span>Отправка...';
}
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 =
'<i class="bi-check-circle me-2"></i>Комментарий добавлен!';
// Вернуть кнопку в исходное состояние
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 = `
<strong>Ошибка:</strong> ${error.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
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 =
'<span class="spinner-border spinner-border-sm me-2"></span>Обновление...';
}
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 =
'<i class="bi-check-circle me-2"></i>Статус обновлен!';
// Вернуть кнопку в исходное состояние
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 = `
<strong>Ошибка:</strong> ${error.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
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;