diff --git a/backend/static/js/modules/requestDetail.js b/backend/static/js/modules/requestDetail.js
new file mode 100644
index 00000000..ad101341
--- /dev/null
+++ b/backend/static/js/modules/requestDetail.js
@@ -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 = '
';
+ }
+
+ // Загрузить данные
+ 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));
+ }
+ }
+
+ /**
+ * Обработчик отправки комментария
+ */
+ 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;
diff --git a/backend/templates/requests_app/request_list_full.html b/backend/templates/requests_app/request_list_full.html
index 914d9432..7fb627eb 100644
--- a/backend/templates/requests_app/request_list_full.html
+++ b/backend/templates/requests_app/request_list_full.html
@@ -359,6 +359,23 @@
{% endblock %}
+
+
+
{% block extra_js %}