refactor: удалены неиспользуемые файлы заявлений
- Удалены шаблоны: my_requests.html, all_requests.html (старые) - Удален JS компонент: requestListHandler.js (неиспользуемый) - Обновлены views: my_requests() и all_requests() теперь редиректят на requests:request_list - Обновлены все редиректы с requests_app:my_requests на requests:request_list - Основная страница заявлений: /requests/ (request_list_full.html)
This commit is contained in:
parent
9e0e5fa01a
commit
ff41eb62b0
@ -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)
|
||||
|
||||
@ -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<Array>} Массив новых заявлений
|
||||
*/
|
||||
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 = '<div class="p-4 text-center text-danger">Не удалось загрузить заявления</div>';
|
||||
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 = `
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
`;
|
||||
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 = '<div class="p-4 text-center text-secondary">Нет заявлений.</div>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let items = filterBySearch(requests);
|
||||
|
||||
if (items.length === 0 && !append) {
|
||||
listElement.innerHTML = '<div class="p-4 text-center text-secondary">Нет заявлений.</div>';
|
||||
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 `
|
||||
<article class="card">
|
||||
<header class="card-header">
|
||||
<div class="card-icon" style="width:48px;height:48px;">
|
||||
<i class="bi-file-earmark-text"></i>
|
||||
</div>
|
||||
|
||||
<div class="card-meta flex-grow-1">
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<div class="card-title">${escapeHtml(typeDisplay)}</div>
|
||||
${statusBadge}
|
||||
</div>
|
||||
<div class="card-subtitle mt-1">
|
||||
<time datetime="${req.created_at}">${createdAt}</time>
|
||||
${req.comment ? ` • ${escapeHtml(req.comment.substring(0, 100))}${req.comment.length > 100 ? '...' : ''}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ms-auto">
|
||||
<a href="${detailUrl}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi-eye"></i> Подробнее
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает HTML для бейджа статуса
|
||||
* @param {string} status - Статус заявления
|
||||
* @returns {string} HTML
|
||||
*/
|
||||
function getStatusBadge(status) {
|
||||
const badges = {
|
||||
'pending': '<span class="badge bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle">На рассмотрении</span>',
|
||||
'approved': '<span class="badge bg-success-subtle text-success-emphasis border border-success-subtle">Одобрено</span>',
|
||||
'rejected': '<span class="badge bg-danger-subtle text-danger-emphasis border border-danger-subtle">Отклонено</span>',
|
||||
'cancelled': '<span class="badge bg-dark-subtle text-dark-emphasis border border-dark-subtle">Отменено</span>',
|
||||
'draft': '<span class="badge bg-light text-body border">Черновик</span>'
|
||||
};
|
||||
return badges[status] || '<span class="badge bg-light text-body border">Неизвестно</span>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирует дату
|
||||
* @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;
|
||||
}
|
||||
@ -1,146 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static django_bootstrap5 %}
|
||||
|
||||
{% block title %}Все заявления сотрудников{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row gx-0">
|
||||
<div class="col-12">
|
||||
|
||||
<!-- Заголовок -->
|
||||
<div class="section-header">
|
||||
<i class="bi-clipboard2-check fs-2 text-primary"></i>
|
||||
<h2 class="title mb-0">Заявления сотрудников</h2>
|
||||
</div>
|
||||
|
||||
<!-- Фильтры -->
|
||||
<div class="feed-new mb-3">
|
||||
<form method="get" class="row g-2 align-items-end">
|
||||
<div class="col-12 col-sm-4">
|
||||
<label class="form-label small text-secondary mb-1">Статус</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="" {% if not filters.status %}selected{% endif %}>Все</option>
|
||||
<option value="pending" {% if filters.status == 'pending' %}selected{% endif %}>На рассмотрении</option>
|
||||
<option value="approved" {% if filters.status == 'approved' %}selected{% endif %}>Одобрено</option>
|
||||
<option value="rejected" {% if filters.status == 'rejected' %}selected{% endif %}>Отклонено</option>
|
||||
<option value="cancelled" {% if filters.status == 'cancelled' %}selected{% endif %}>Отменено</option>
|
||||
<option value="draft" {% if filters.status == 'draft' %}selected{% endif %}>Черновик</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-sm-4 col-md-3">
|
||||
<label class="form-label small text-secondary mb-1">С даты</label>
|
||||
<input type="date" name="from" class="form-control" value="{{ filters.from }}">
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-sm-4 col-md-3">
|
||||
<label class="form-label small text-secondary mb-1">По дату</label>
|
||||
<input type="date" name="to" class="form-control" value="{{ filters.to }}">
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-2">
|
||||
<label class="form-label small text-secondary mb-1">Сортировка</label>
|
||||
<select name="sort" class="form-select">
|
||||
<option value="" {% if not filters.sort %}selected{% endif %}>По умолчанию</option>
|
||||
<option value="-created_at" {% if filters.sort == '-created_at' %}selected{% endif %}>Новые сверху</option>
|
||||
<option value="created_at" {% if filters.sort == 'created_at' %}selected{% endif %}>Старые сверху</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 d-flex gap-2">
|
||||
<button class="btn btn-primary"><i class="bi-funnel me-1"></i>Применить</button>
|
||||
<a class="btn btn-ghost" href="?"><i class="bi-x-circle me-1"></i>Сбросить</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% with items=requests|default:page.object_list %}
|
||||
{% if items %}
|
||||
<div class="card-list">
|
||||
{% for req in items %}
|
||||
<article class="card">
|
||||
<header class="card-header">
|
||||
<div class="card-icon" style="width:44px;height:44px;">
|
||||
{% if req.employee.avatar %}
|
||||
<img src="{{ req.employee.avatar }}" alt="">
|
||||
{% else %}
|
||||
<i class="bi-person"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card-meta flex-grow-1">
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<a href="{% url 'employees:employee_detail' req.employee.pk %}"
|
||||
class="card-title text-decoration-none">
|
||||
{{ req.employee.get_full_name|default:req.employee.username }}
|
||||
</a>
|
||||
{% if req.department %}
|
||||
<span class="badge bg-light text-dark border">
|
||||
<i class="bi-diagram-3 me-1"></i>{{ req.department.name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card-subtitle mt-1">
|
||||
<span class="text-secondary">Подано:</span>
|
||||
{{ req.created_at|date:"d.m.Y H:i" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Бейдж статуса справа -->
|
||||
<div class="ms-auto">
|
||||
{% if req.status == 'pending' %}
|
||||
<span class="badge bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle">На рассмотрении</span>
|
||||
{% elif req.status == 'approved' %}
|
||||
<span class="badge bg-success-subtle text-success-emphasis border border-success-subtle">Одобрено</span>
|
||||
{% elif req.status == 'rejected' %}
|
||||
<span class="badge bg-danger-subtle text-danger-emphasis border border-danger-subtle">Отклонено</span>
|
||||
{% elif req.status == 'cancelled' %}
|
||||
<span class="badge bg-warning-subtle text-warning-emphasis border border-warning-subtle">Отменено</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Черновик</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="card-body pt-0">
|
||||
<div class="mb-2">
|
||||
<span class="badge bg-primary-subtle text-primary-emphasis border border-primary-subtle">
|
||||
{{ req.get_type_display }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="small text-muted">
|
||||
<span class="me-1 text-secondary">Период:</span>
|
||||
{% 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 %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="card-actions">
|
||||
<a href="{% url 'requests_app:request_process' req.pk %}"
|
||||
class="btn btn-ghost d-inline-flex align-items-center gap-1">
|
||||
<i class="bi-box-arrow-up-right"></i><span>Рассмотреть</span>
|
||||
</a>
|
||||
</footer>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if page %}
|
||||
<div class="mt-4">
|
||||
{% bootstrap_pagination page %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-info shadow-sm">Заявлений нет.</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,54 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static django_bootstrap5 %}
|
||||
|
||||
{% block title %}Мои заявления{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'css/components/common.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/components/ios-search.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/components/page-header.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/components/list-view.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row gx-0">
|
||||
<div class="col-12">
|
||||
|
||||
{# — Шапка страницы — #}
|
||||
{% 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="Подать новое заявление" %}
|
||||
|
||||
<div class="card mb-3">
|
||||
<div id="requestList" class="list-view card-list">
|
||||
{# Список загружается через JavaScript из requestListHandler.js #}
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script type="module">
|
||||
import { initRequestListHandler } from "{% static 'js/components/requestListHandler.js' %}";
|
||||
|
||||
// Получаем токен доступа
|
||||
const accessMeta = document.querySelector('meta[name="api-access"]');
|
||||
const ACCESS = accessMeta ? accessMeta.content : '';
|
||||
const headers = ACCESS ? { 'Authorization': 'Bearer ' + ACCESS } : {};
|
||||
|
||||
// Инициализация списка заявлений
|
||||
initRequestListHandler({
|
||||
apiListUrl: '/api/v1/requests/',
|
||||
scope: 'my',
|
||||
userId: {{ user.id }},
|
||||
headers
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user