Corp/backend/templates/requests_app/request_detail.html
Игорь Надеин bc5f66c6e5 fix: добавлена JavaScript обработка форм на странице заявления
- Форма добавления комментария теперь обрабатывается через AJAX
- Форма удаления комментария с подтверждением
- Форма изменения статуса с подтверждением
- После успешных операций страница перезагружается
- Отображение состояния загрузки (spinner) на кнопках
- Обработка ошибок с alert уведомлениями
2025-12-29 09:28:47 +03:00

341 lines
14 KiB
HTML
Raw 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.

{% extends "base.html" %}
{% load static %}
{% block title %}Заявление #{{ request_obj.id }}{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'css/components/card-list.css' %}">
<link rel="stylesheet" href="{% static 'css/components/message-field.css' %}">
{% endblock %}
{% block content %}
<div class="container-fluid py-3">
<div class="row justify-content-center">
<div class="col-12 col-lg-10 col-xl-8">
{% if error %}
<div class="card mb-3">
<div class="card-body text-center py-5">
<i class="bi bi-exclamation-triangle text-warning" style="font-size: 3rem;"></i>
<h5 class="mt-3">{{ error }}</h5>
<p class="text-muted">Код ошибки: {{ error_code }}</p>
<a href="{% url 'requests:request_list' %}" class="btn btn-primary mt-3">
<i class="bi-arrow-left me-2"></i>Вернуться к списку
</a>
</div>
</div>
{% else %}
{# ===== Основная информация о заявлении ===== #}
<article class="card mb-3">
<header class="card-header d-flex align-items-start gap-3">
<div class="card-icon">
<i class="bi-clipboard-check"></i>
</div>
<div class="card-meta flex-grow-1">
<div class="d-flex align-items-center gap-2 flex-wrap mb-2">
<span class="card-title">
{{ request_obj.employee.full_name|default:request_obj.employee.display_name }}
</span>
{# статус — бейдж #}
{% if request_obj.status == 'pending' %}
<span class="badge bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle">На рассмотрении</span>
{% elif request_obj.status == 'approved' %}
<span class="badge bg-success-subtle text-success-emphasis border border-success-subtle">Одобрено</span>
{% elif request_obj.status == 'rejected' %}
<span class="badge bg-danger-subtle text-danger-emphasis border border-danger-subtle">Отклонено</span>
{% elif request_obj.status == 'cancelled' %}
<span class="badge bg-dark-subtle text-dark-emphasis border border-dark-subtle">Отменено</span>
{% elif request_obj.status == 'draft' %}
<span class="badge bg-light text-body border">Черновик</span>
{% else %}
<span class="badge bg-light text-body border">Неизвестно</span>
{% endif %}
{# тип заявки #}
<span class="badge bg-primary-subtle text-primary-emphasis border border-primary-subtle">
{{ request_obj.get_type_display }}
</span>
</div>
<div class="card-subtitle">
<time datetime="{{ request_obj.created_at|date:'c' }}">{{ request_obj.created_at }}</time>
{% if request_obj.date_from or request_obj.date_to %}
· Период: {{ request_obj.date_from|date:'d.m.Y' }}{% if request_obj.date_to %} — {{ request_obj.date_to|date:'d.m.Y' }}{% endif %}
{% endif %}
</div>
</div>
{# Отозвать — только если можно (ожидание и не финальное) #}
{% if request_obj.status == 'pending' and not request_obj.is_final %}
<a href="{% url 'requests:request_cancel' request_obj.id %}"
class="btn btn-ghost ms-auto">
<i class="bi-x-circle"></i> Отозвать
</a>
{% endif %}
{# Форма смены статуса — для администраторов #}
{% if can_process %}
<form class="request-status-form ms-auto d-flex gap-2" method="post">
{% csrf_token %}
<select class="form-select form-select-sm" name="status" style="max-width: 200px;">
<option value="">Изменить статус...</option>
{% if request_obj.status == 'pending' %}
<option value="approved">✓ Одобрить</option>
<option value="rejected">✗ Отклонить</option>
{% endif %}
{% if not request_obj.is_final %}
<option value="cancelled">⊗ Отменить</option>
{% endif %}
</select>
<button type="submit" class="btn btn-sm btn-primary" {% if not request_obj.status == 'pending' and request_obj.is_final %}disabled{% endif %}>
Применить
</button>
</form>
{% endif %}
</header>
<div class="card-body">
{% if request_obj.title %}
<h6 class="mb-2">{{ request_obj.title }}</h6>
{% endif %}
<div class="mb-3">
<span class="text-secondary small d-block mb-1">Комментарий:</span>
<div>{{ request_obj.comment|default:"—"|linebreaksbr }}</div>
</div>
{% if request_obj.attachment_url %}
<a class="btn btn-ghost d-inline-flex align-items-center gap-2"
href="{{ request_obj.attachment_url }}" target="_blank" rel="noopener">
<i class="bi-paperclip"></i>
<span>Вложение</span>
</a>
{% endif %}
</div>
</article>
{# ===== Комментарии ===== #}
<article class="card" id="comments">
<header class="card-header">
<div class="card-icon">
<i class="bi-chat-dots"></i>
</div>
<div class="card-meta">
<div class="card-title">Комментарии</div>
<div class="card-subtitle">Всего: <span class="comments-count">{{ comments|length }}</span></div>
</div>
</header>
<div class="card-body">
{% if comments %}
<div class="d-flex flex-column gap-2 mb-3">
{% for c in comments %}
<div class="card-list-item">
<div class="d-flex justify-content-between align-items-start mb-1">
<div>
<span class="fw-semibold">{{ c.author.full_name|default:c.author.display_name }}</span>
<span class="text-secondary mx-1">·</span>
<time class="text-muted small">{{ c.created_at }}</time>
</div>
{# удалить — только автору комментария #}
{% if c.author.id == request.user.id %}
<form method="post" action="{% url 'requests:request_comment_delete' request_obj.id c.id %}">
{% csrf_token %}
<button class="btn btn-sm btn-ghost text-danger" type="submit" title="Удалить">
<i class="bi-trash"></i>
</button>
</form>
{% endif %}
</div>
<div class="small">{{ c.text|linebreaksbr }}</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-secondary small text-center py-3">Комментариев пока нет.</div>
{% endif %}
{# Добавить комментарий — #}
<form class="mt-3 request-comment-form" method="post" action="{% url 'requests:request_comment_add' request_obj.id %}">
{% csrf_token %}
<div class="message-field message-field--compact">
<!-- Кнопка выбора эмодзи -->
<div class="dropdown message-emoji">
<button type="button" class="btn btn-ghost btn-emoji message-icon-btn"
data-bs-toggle="dropdown"
aria-expanded="false"
title="Вставить смайлик">
<i class="bi-emoji-smile"></i>
</button>
<div class="dropdown-menu dropdown-menu-start message-emoji-menu">
<emoji-picker data-emoji-picker class="chat-emoji-picker"></emoji-picker>
</div>
</div>
<textarea
name="text"
class="message-input"
placeholder="Добавить комментарий..."
rows="2"
required
></textarea>
<button type="submit" class="btn btn-primary message-send-btn">
<i class="bi-send-fill"></i>
</button>
</div>
</form>
</div>
</article>
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script type="module">
// Обработка формы добавления комментария
const commentForm = document.querySelector('.request-comment-form');
if (commentForm) {
commentForm.addEventListener('submit', async (e) => {
e.preventDefault();
const textarea = commentForm.querySelector('textarea[name="text"]');
const text = textarea.value.trim();
if (!text) {
alert('Пожалуйста, введите текст комментария');
return;
}
const submitBtn = commentForm.querySelector('button[type="submit"]');
const originalBtnContent = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
try {
const response = await fetch(commentForm.action, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': commentForm.querySelector('[name="csrfmiddlewaretoken"]').value
},
body: JSON.stringify({ text: text })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
// Перезагрузить страницу для отображения нового комментария
window.location.reload();
} catch (error) {
console.error('Error:', error);
alert('Ошибка при добавлении комментария: ' + error.message);
submitBtn.disabled = false;
submitBtn.innerHTML = originalBtnContent;
}
});
}
// Обработка форм удаления комментариев
document.querySelectorAll('form[action*="request_comment_delete"]').forEach(form => {
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!confirm('Вы уверены, что хотите удалить этот комментарий?')) {
return;
}
try {
const response = await fetch(form.action, {
method: 'POST',
headers: {
'X-CSRFToken': form.querySelector('[name="csrfmiddlewaretoken"]').value
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
// Перезагрузить страницу
window.location.reload();
} catch (error) {
console.error('Error:', error);
alert('Ошибка при удалении комментария: ' + error.message);
}
});
});
// Обработка формы изменения статуса
const statusForm = document.querySelector('.request-status-form');
if (statusForm) {
statusForm.addEventListener('submit', async (e) => {
e.preventDefault();
const selectField = statusForm.querySelector('select[name="status"]');
const newStatus = selectField.value;
if (!newStatus) {
alert('Пожалуйста, выберите новый статус');
return;
}
const confirmMsg = `Вы уверены, что хотите изменить статус?`;
if (!confirm(confirmMsg)) {
return;
}
const submitBtn = statusForm.querySelector('button[type="submit"]');
const originalBtnContent = submitBtn.innerHTML;
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
try {
const action = newStatus === 'approved' ? 'approve' :
newStatus === 'rejected' ? 'reject' :
newStatus === 'cancelled' ? 'cancel' : null;
if (!action) {
throw new Error('Неподдерживаемый статус');
}
const requestId = window.location.pathname.split('/').filter(p => p)[1];
const response = await fetch(`/api/v1/requests/${requestId}/${action}/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': statusForm.querySelector('[name="csrfmiddlewaretoken"]').value
}
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `HTTP ${response.status}`);
}
// Перезагрузить страницу
window.location.reload();
} catch (error) {
console.error('Error:', error);
alert('Ошибка при изменении статуса: ' + error.message);
submitBtn.disabled = false;
submitBtn.innerHTML = originalBtnContent;
}
});
}
</script>
{% endblock %}