diff --git a/backend/requests_app/views.py b/backend/requests_app/views.py index c8921ab5..86e0dfbe 100644 --- a/backend/requests_app/views.py +++ b/backend/requests_app/views.py @@ -242,10 +242,8 @@ def _get_request_for_comment_access(user, pk: int) -> Request: # ---------- Пользователь ---------- @login_required def my_requests(request): - # Упрощенный view - данные загружаются через AJAX - return render( - request, "requests_app/my_requests.html", {"requests": []} - ) + # Редирект на новую страницу заявлений + return redirect("requests:request_list") @login_required @@ -266,7 +264,7 @@ def request_create(request): obj.employee = request.user obj.save() messages.success(request, "Заявление отправлено!") - return redirect("requests_app:my_requests") + return redirect("requests:request_list") messages.error(request, "Исправьте ошибки в форме.") else: form = RequestForm() @@ -280,18 +278,18 @@ def request_cancel(request, pk): if req.status == Request.STATUS_CANCELLED: messages.info(request, "Заявление уже отменено.") - return redirect("requests_app:my_requests") + return redirect("requests:request_list") if req.is_final and req.status != Request.STATUS_CANCELLED: messages.error( request, "Нельзя отозвать заявление, по которому уже принято решение." ) - return redirect("requests_app:my_requests") + return redirect("requests:request_list") if request.method == "POST": req.cancel() messages.success(request, "Заявление отменено.") - return redirect("requests_app:my_requests") + return redirect("requests:request_list") return render( request, "requests_app/request_confirm_cancel.html", {"request_obj": req} @@ -387,7 +385,7 @@ def request_process(request, pk): if not status_changed and not soft_updates: messages.info(request, "Изменений не обнаружено.") - return redirect("requests_app:all_requests") + return redirect("requests:request_list") # GET req = get_object_or_404(allowed_qs.prefetch_related(COMMENTS_PREFETCH), pk=pk) diff --git a/backend/static/js/components/requestListHandler.js b/backend/static/js/components/requestListHandler.js deleted file mode 100644 index d148a8f4..00000000 --- a/backend/static/js/components/requestListHandler.js +++ /dev/null @@ -1,420 +0,0 @@ -/** - * @module requestListHandler - * @description Загружает и отображает список заявлений через API с бесконечной прокруткой - */ - -/** - * Инициализирует обработчик списка заявлений - * @param {Object} options - Опции инициализации - * @param {string} options.apiListUrl - URL API для списка заявлений - * @param {string} options.scope - Область ('my' | 'all') - * @param {number} options.userId - ID текущего пользователя - * @param {Object} options.headers - HTTP заголовки для запросов - * @returns {Object} API с методами load, destroy - */ -export function initRequestListHandler(options) { - const { - apiListUrl, - scope = 'my', - userId, - headers = {} - } = options; - - const listElement = document.getElementById('requestList'); - const searchInput = document.getElementById('requestFilter'); - - if (!listElement) { - console.warn('initRequestListHandler: #requestList not found'); - return { load: () => {}, destroy: () => {} }; - } - - // Текущее состояние - const urlParams = new URLSearchParams(window.location.search); - let currentScope = scope; - let currentStatus = urlParams.get('status') || ''; // 'pending', 'approved', 'rejected', 'cancelled', 'draft' - let searchQuery = ''; // Поисковый запрос - let allRequests = []; // Все загруженные заявления - let loading = false; - let hasMore = true; - let nextUrl = null; - let totalCount = 0; - - console.log('requestListHandler: init with scope =', currentScope, 'status =', currentStatus); - - // Intersection Observer для бесконечной прокрутки - let observerTarget = null; - let observer = null; - - /** - * Загружает заявления с API - * @param {boolean} append - Добавить к существующим или заменить - * @returns {Promise} Массив новых заявлений - */ - async function loadRequests(append = false) { - if (loading || (append && !hasMore)) { - console.log('loadRequests: skipped', { loading, append, hasMore }); - return []; - } - - loading = true; - showLoadingSpinner(); - - try { - let url; - if (append && nextUrl) { - url = nextUrl; - console.log('loadRequests: using nextUrl', url); - } else if (append && !nextUrl) { - console.warn('loadRequests: append=true but no nextUrl, stopping'); - loading = false; - hideLoadingSpinner(); - return []; - } else { - // Формируем URL с параметрами - const params = new URLSearchParams(); - if (currentScope === 'my') { - params.append('scope', 'my'); - } - if (currentStatus) { - params.append('status', currentStatus); - } - - url = `${apiListUrl}?${params}`; - console.log('loadRequests: initial load', url, 'scope:', currentScope, 'status:', currentStatus); - } - - const response = await fetch(url, { headers }); - - if (!response.ok) { - throw new Error('HTTP ' + response.status); - } - - const data = await response.json(); - const newRequests = data.results || []; - - console.log('loadRequests: received', { - newCount: newRequests.length, - totalCount: data.count, - hasNext: !!data.next, - currentRequests: allRequests.length - }); - - // Сохраняем метаданные - totalCount = data.count || newRequests.length; - nextUrl = data.next || null; - hasMore = !!nextUrl; - - if (append) { - allRequests = [...allRequests, ...newRequests]; - console.log('loadRequests: appended, total now:', allRequests.length); - } else { - allRequests = newRequests; - console.log('loadRequests: replaced, total now:', allRequests.length); - } - - loading = false; - hideLoadingSpinner(); - - return newRequests; - } catch (error) { - console.error('Ошибка загрузки заявлений:', error); - loading = false; - hideLoadingSpinner(); - listElement.innerHTML = '
Не удалось загрузить заявления
'; - return []; - } - } - - /** - * Показать спиннер загрузки - */ - function showLoadingSpinner() { - const existing = listElement.querySelector('.loading-spinner'); - if (existing) return; - - const spinner = document.createElement('div'); - spinner.className = 'loading-spinner text-center py-4'; - spinner.innerHTML = ` -
- Загрузка... -
- `; - listElement.appendChild(spinner); - } - - /** - * Скрыть спиннер загрузки - */ - function hideLoadingSpinner() { - const spinner = listElement.querySelector('.loading-spinner'); - if (spinner) { - spinner.remove(); - } - } - - /** - * Загрузка следующей порции заявлений - */ - async function loadMore() { - console.log('loadMore: triggered', { - hasMore, - loading, - nextUrl, - currentCount: allRequests.length - }); - - try { - const newRequests = await loadRequests(true); // append = true - renderRequests(newRequests, true); // append = true - } catch (error) { - console.error('Failed to load more requests:', error); - } - } - - /** - * Настройка Intersection Observer для бесконечной прокрутки - */ - function setupInfiniteScroll() { - if (!observerTarget) return; - - // Создаем observer - observer = new IntersectionObserver( - (entries) => { - entries.forEach(entry => { - if (entry.isIntersecting && hasMore && !loading) { - loadMore(); - } - }); - }, - { - root: null, - rootMargin: '100px', - threshold: 0.01 - } - ); - - // Наблюдаем за observer target - observer.observe(observerTarget); - } - - /** - * Фильтрует по поисковому запросу - * @param {Array} items - Список заявлений - * @returns {Array} Отфильтрованный список - */ - function filterBySearch(items) { - if (!searchQuery.trim()) return items; - - const query = searchQuery.toLowerCase().trim(); - return items.filter(req => { - const type = (req.type_display || req.type || '').toLowerCase(); - const comment = (req.comment || '').toLowerCase(); - return type.includes(query) || comment.includes(query); - }); - } - - /** - * Отображает список заявлений - * @param {Array} requests - Заявления для рендеринга - * @param {boolean} append - Добавить к существующим или заменить - */ - function renderRequests(requests, append = false) { - if (!requests || requests.length === 0) { - if (!append) { - listElement.innerHTML = '
Нет заявлений.
'; - } - return; - } - - let items = filterBySearch(requests); - - if (items.length === 0 && !append) { - listElement.innerHTML = '
Нет заявлений.
'; - return; - } - - console.log('renderRequests: rendering', { - count: items.length, - append, - totalInMemory: allRequests.length - }); - - const itemsHtml = items.map(req => renderRequestCard(req)).join(''); - - if (append) { - // Удаляем observer target перед добавлением - if (observerTarget && observerTarget.parentNode) { - observerTarget.remove(); - } - // Добавляем новые карточки - listElement.insertAdjacentHTML('beforeend', itemsHtml); - console.log('renderRequests: appended to DOM'); - // Возвращаем observer target обратно - if (observerTarget) { - listElement.appendChild(observerTarget); - } - } else { - listElement.innerHTML = itemsHtml; - console.log('renderRequests: replaced DOM'); - // Создаем observer target при первой загрузке - if (hasMore && !observerTarget) { - observerTarget = document.createElement('div'); - observerTarget.className = 'load-more-trigger'; - observerTarget.style.height = '1px'; - listElement.appendChild(observerTarget); - } - } - } - - /** - * Рендерит карточку заявления - * @param {Object} req - Заявление - * @returns {string} HTML - */ - function renderRequestCard(req) { - const statusBadge = getStatusBadge(req.status); - const typeDisplay = req.type_display || req.type || 'Неизвестный тип'; - const createdAt = formatDate(req.created_at); - const detailUrl = `/requests/${req.id}/`; - - return ` -
-
-
- -
- -
-
-
${escapeHtml(typeDisplay)}
- ${statusBadge} -
-
- - ${req.comment ? ` • ${escapeHtml(req.comment.substring(0, 100))}${req.comment.length > 100 ? '...' : ''}` : ''} -
-
- - -
-
- `; - } - - /** - * Возвращает HTML для бейджа статуса - * @param {string} status - Статус заявления - * @returns {string} HTML - */ - function getStatusBadge(status) { - const badges = { - 'pending': 'На рассмотрении', - 'approved': 'Одобрено', - 'rejected': 'Отклонено', - 'cancelled': 'Отменено', - 'draft': 'Черновик' - }; - return badges[status] || 'Неизвестно'; - } - - /** - * Форматирует дату - * @param {string} dateStr - ISO дата - * @returns {string} Отформатированная дата - */ - function formatDate(dateStr) { - const date = new Date(dateStr); - return date.toLocaleDateString('ru-RU', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - } - - /** - * Экранирует HTML - * @param {string} str - Строка - * @returns {string} Экранированная строка - */ - function escapeHtml(str) { - const div = document.createElement('div'); - div.textContent = str; - return div.innerHTML; - } - - /** - * Публичный API для изменения фильтров - */ - async function setStatus(status) { - currentStatus = status; - hasMore = true; - nextUrl = null; - allRequests = []; - const reqs = await loadRequests(false); - renderRequests(reqs, false); - setupInfiniteScroll(); - } - - /** - * Обработчик поиска (работает только с уже загруженными заявлениями) - */ - function handleSearch() { - // Применяем фильтрацию ко всем загруженным заявлениям - renderRequests(allRequests, false); - } - - /** - * Настройка обработчика поиска - */ - function setupSearch() { - if (!searchInput) return; - - let searchTimeout; - searchInput.addEventListener('input', (e) => { - clearTimeout(searchTimeout); - searchQuery = e.target.value; - - // Debounce: ждем 300ms после последнего ввода - searchTimeout = setTimeout(() => { - console.log('Search query:', searchQuery); - handleSearch(); - }, 300); - }); - } - - /** - * Очистка обработчиков - */ - function destroy() { - if (observer) { - observer.disconnect(); - observer = null; - } - } - - // Начальная загрузка - (async () => { - hasMore = true; - const reqs = await loadRequests(false); - renderRequests(reqs, false); - setupInfiniteScroll(); - setupSearch(); - })(); - - return { - load: loadRequests, - setStatus, - destroy - }; -} - -// Экспорт для совместимости -if (typeof window !== 'undefined') { - window.initRequestListHandler = initRequestListHandler; -} diff --git a/backend/templates/requests_app/all_requests.html b/backend/templates/requests_app/all_requests.html deleted file mode 100644 index 3106ae43..00000000 --- a/backend/templates/requests_app/all_requests.html +++ /dev/null @@ -1,146 +0,0 @@ -{% extends 'base.html' %} -{% load static django_bootstrap5 %} - -{% block title %}Все заявления сотрудников{% endblock %} - -{% block content %} -
-
-
- - -
- -

Заявления сотрудников

-
- - -
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - Сбросить -
-
-
- - {% with items=requests|default:page.object_list %} - {% if items %} -
- {% for req in items %} -
-
-
- {% if req.employee.avatar %} - - {% else %} - - {% endif %} -
- -
-
- - {{ req.employee.get_full_name|default:req.employee.username }} - - {% if req.department %} - - {{ req.department.name }} - - {% endif %} -
- -
- Подано: - {{ req.created_at|date:"d.m.Y H:i" }} -
-
- - -
- {% if req.status == 'pending' %} - На рассмотрении - {% elif req.status == 'approved' %} - Одобрено - {% elif req.status == 'rejected' %} - Отклонено - {% elif req.status == 'cancelled' %} - Отменено - {% else %} - Черновик - {% endif %} -
-
- -
-
- - {{ req.get_type_display }} - -
- -
- Период: - {% if req.date_from or req.date_to %} - {{ req.date_from|date:"d.m.Y" }}{% if req.date_to %} — {{ req.date_to|date:"d.m.Y" }}{% endif %} - {% else %} - — - {% endif %} -
-
- - -
- {% endfor %} -
- - {% if page %} -
- {% bootstrap_pagination page %} -
- {% endif %} - {% else %} -
Заявлений нет.
- {% endif %} - {% endwith %} -
-
-
-{% endblock %} diff --git a/backend/templates/requests_app/my_requests.html b/backend/templates/requests_app/my_requests.html deleted file mode 100644 index bf3e67f4..00000000 --- a/backend/templates/requests_app/my_requests.html +++ /dev/null @@ -1,54 +0,0 @@ -{% extends 'base.html' %} -{% load static django_bootstrap5 %} - -{% block title %}Мои заявления{% endblock %} - -{% block extra_css %} - - - - -{% endblock %} - -{% block content %} -
-
-
- - {# — Шапка страницы — #} - {% include "includes/components/page_header.html" with icon="bi-file-earmark-text" title="Мои заявления" search_id="requestFilter" search_placeholder="Поиск по типу или комментарию" create_url="requests_app:request_create" create_text="Подать новое заявление" %} - -
-
- {# Список загружается через JavaScript из requestListHandler.js #} -
-
- Загрузка... -
-
-
-
- -
-
-
-{% endblock %} - -{% block extra_js %} - -{% endblock %}