Corp/backend/templates/requests_app/request_detail.html
2026-04-13 16:00:53 +03:00

520 lines
20 KiB
HTML
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.

{% extends "base.html" %}
{% load static req_tags %}
{% block title %}Заявление #{{ request_obj.id }}{% endblock %}
{% block extra_css %}{% endblock %}
{% block content %}
<div class="container-fluid py-3">
{# ===== Хлебные крошки ===== #}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item">
<a href="{% url 'requests:request_list' %}" class="text-decoration-none">
<i class="bi-clipboard-check me-1"></i>Заявления
</a>
</li>
<li class="breadcrumb-item active" aria-current="page">
#{{ request_obj.id }}
</li>
</ol>
</nav>
{% if error %}
{# ===== Ошибка ===== #}
<div class="card">
<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-body-secondary">Код ошибки: {{ 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 %}
<div class="row g-3">
{# ===== Основная карточка заявления ===== #}
<div class="col-12 col-lg-8">
<article class="card request-detail-card" id="reqList">
{# Шапка как в списке #}
<div class="req-detail-header">
<div class="req-detail-row">
{# Иконка типа #}
<div class="req-detail-icon">
{% if request_obj.type == 'vacation' %}
<div class="card-icon"><i class="bi-calendar-check text-primary"></i></div>
{% elif request_obj.type == 'sick_leave' %}
<div class="card-icon"><i class="bi-capsule text-danger"></i></div>
{% elif request_obj.type == 'day_off' %}
<div class="card-icon"><i class="bi-umbrella-fill text-info"></i></div>
{% elif request_obj.type == 'transfer' %}
<div class="card-icon"><i class="bi-box-arrow-right text-info"></i></div>
{% elif request_obj.type == 'dismissal' %}
<div class="card-icon"><i class="bi-door-open text-warning"></i></div>
{% else %}
<div class="card-icon"><i class="bi-clipboard-check text-secondary"></i></div>
{% endif %}
</div>
{# Заголовок и мета #}
<div class="req-detail-content">
<div class="d-flex align-items-center flex-wrap gap-2 mb-1">
<h1 class="h5 mb-0">{{ request_obj.title|default:"Без заголовка" }}</h1>
{# Бейдж типа #}
<span class="badge text-bg-light">{{ request_obj.get_type_display }}</span>
{# Бейдж статуса #}
{% if request_obj.status == 'draft' %}
<span class="badge text-bg-info">Черновик</span>
{% elif request_obj.status == 'pending' %}
<span class="badge text-bg-warning">На рассмотрении</span>
{% elif request_obj.status == 'approved' %}
<span class="badge text-bg-success">Одобрено</span>
{% elif request_obj.status == 'rejected' %}
<span class="badge text-bg-danger">Отклонено</span>
{% elif request_obj.status == 'cancelled' %}
<span class="badge text-bg-secondary">Отменено</span>
{% endif %}
</div>
<div class="text-body-secondary small">
Создано: {{ request_obj.created_at }}
{% if request_obj.date_from or request_obj.date_to %}
<span class="mx-1">·</span>
Период: {{ request_obj.date_from|default:"—" }}{% if request_obj.date_to %} — {{ request_obj.date_to }}{% endif %}
{% endif %}
</div>
</div>
{# Кнопки действий #}
{% if request_obj|can_decide_request:request.user and request_obj.status == 'pending' %}
<div class="req-detail-actions">
<button type="button" class="btn btn-success btn-sm"
data-bs-toggle="modal"
data-bs-target="#reqApproveModal"
data-id="{{ request_obj.id }}">
<i class="bi-hand-thumbs-up me-1"></i>Одобрить
</button>
<button type="button" class="btn btn-danger btn-sm"
data-bs-toggle="modal"
data-bs-target="#reqRejectModal"
data-id="{{ request_obj.id }}">
<i class="bi-hand-thumbs-down me-1"></i>Отклонить
</button>
</div>
{% endif %}
</div>
</div>
{# Тело карточки #}
<div class="card-body">
{# Автор заявления #}
<div class="req-info-block">
<div class="req-info-label">
<i class="bi-person me-1"></i>Автор
</div>
<div class="req-info-value">
{% if request_obj.employee.id %}
<a href="{% url 'employees:employee_detail' request_obj.employee.id %}" class="text-decoration-none">
{{ request_obj.employee.full_name|default:request_obj.employee.display_name }}
</a>
{% else %}
{{ request_obj.employee.full_name|default:request_obj.employee.display_name }}
{% endif %}
</div>
</div>
{# Комментарий автора #}
{% if request_obj.comment %}
<div class="req-info-block">
<div class="req-info-label">
<i class="bi-chat-left-text me-1"></i>Описание
</div>
<div class="req-info-value req-comment-text">
{{ request_obj.comment|linebreaksbr }}
</div>
</div>
{% endif %}
{# Получатели #}
{% if request_obj.recipients %}
<div class="req-info-block">
<div class="req-info-label">
<i class="bi-people me-1"></i>Получатели
</div>
<div class="req-info-value">
<div class="d-flex flex-wrap gap-1">
{% for recipient in request_obj.recipients %}
{% if recipient.id %}
<a href="{% url 'employees:employee_detail' recipient.id %}"
class="badge text-bg-primary text-decoration-none">
{{ recipient.full_name|default:recipient.display_name }}
</a>
{% else %}
<span class="badge text-bg-primary">
{{ recipient.full_name|default:recipient.display_name }}
</span>
{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endif %}
{# В копии #}
{% if request_obj.cc_users %}
<div class="req-info-block">
<div class="req-info-label">
<i class="bi-people-fill me-1"></i>В копии
</div>
<div class="req-info-value">
<div class="d-flex flex-wrap gap-1">
{% for cc in request_obj.cc_users %}
{% if cc.id %}
<a href="{% url 'employees:employee_detail' cc.id %}"
class="badge text-bg-secondary text-decoration-none">
{{ cc.full_name|default:cc.display_name }}
</a>
{% else %}
<span class="badge text-bg-secondary">
{{ cc.full_name|default:cc.display_name }}
</span>
{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endif %}
{# Кто обработал #}
{% if request_obj.approver %}
<div class="req-info-block">
<div class="req-info-label">
<i class="bi-person-check me-1"></i>
{% if request_obj.status == 'approved' %}Одобрил{% elif request_obj.status == 'rejected' %}Отклонил{% else %}Обработал{% endif %}
</div>
<div class="req-info-value">
{% if request_obj.approver.id %}
<a href="{% url 'employees:employee_detail' request_obj.approver.id %}"
class="badge {% if request_obj.status == 'approved' %}text-bg-success{% elif request_obj.status == 'rejected' %}text-bg-danger{% else %}text-bg-secondary{% endif %} text-decoration-none">
{{ request_obj.approver.full_name|default:request_obj.approver.display_name }}
</a>
{% else %}
<span class="badge {% if request_obj.status == 'approved' %}text-bg-success{% elif request_obj.status == 'rejected' %}text-bg-danger{% else %}text-bg-secondary{% endif %}">
{{ request_obj.approver.full_name|default:request_obj.approver.display_name }}
</span>
{% endif %}
{% if request_obj.decided_at %}
<span class="text-body-secondary small ms-2">{{ request_obj.decided_at }}</span>
{% endif %}
</div>
</div>
{% endif %}
{# Вложение #}
{% if request_obj.attachment_url %}
<div class="req-info-block">
<div class="req-info-label">
<i class="bi-paperclip me-1"></i>Вложение
</div>
<div class="req-info-value">
<a href="{{ request_obj.attachment_url }}" target="_blank" rel="noopener"
class="btn btn-sm btn-outline-secondary">
<i class="bi-download me-1"></i>Скачать
</a>
</div>
</div>
{% endif %}
</div>
</article>
</div>
{# ===== Боковая панель с комментариями ===== #}
<div class="col-12 col-lg-4">
<article class="card" id="comments-section">
<div class="card-header d-flex align-items-center gap-2">
<i class="bi-chat-dots"></i>
<span class="fw-semibold">Комментарии</span>
<span class="badge text-bg-secondary ms-auto">{{ comments|length }}</span>
</div>
<div class="card-body p-0">
{% if comments %}
<div class="list-group list-group-flush">
{% for c in comments %}
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-start mb-1">
<div>
<strong class="small">{{ c.author.full_name|default:c.author.display_name }}</strong>
<span class="text-body-secondary small ms-1">{{ c.created_at }}</span>
</div>
{# Удаление — только автору #}
{% if c.author.id == request.user.id %}
<button class="btn btn-link btn-sm text-danger p-0 delete-comment-btn"
type="button" title="Удалить"
data-request-id="{{ request_obj.id }}"
data-comment-id="{{ c.id }}">
<i class="bi-trash"></i>
</button>
{% endif %}
</div>
<div class="small">{{ c.text|linebreaksbr }}</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-body-secondary small text-center py-4">
Комментариев пока нет
</div>
{% endif %}
</div>
{# Форма добавления комментария #}
<div class="card-footer">
<form method="post" action="{% url 'requests:request_comment_add' request_obj.id %}"
class="request-comment-form">
{% 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="form-control message-input" rows="1"
placeholder="Добавить комментарий..." required></textarea>
{# Кнопка отправки #}
<button type="submit" class="btn btn-primary message-send" title="Отправить">
<i class="bi-send"></i>
</button>
</div>
</form>
</div>
</article>
</div>
</div>
{% endif %}
</div>
{# ===== Модальные окна ===== #}
<!-- Approve -->
<div class="modal fade" id="reqApproveModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form class="modal-content" id="reqApproveForm">
{% csrf_token %}
<input type="hidden" name="id" id="approveId">
<div class="modal-header border-0">
<div class="d-flex align-items-center gap-2">
<i class="bi-hand-thumbs-up text-success fs-4"></i>
<h5 class="modal-title mb-0">Одобрить заявление?</h5>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body pt-0">
<p class="text-body-secondary mb-0">
Заявление #{{ request_obj.id }} будет одобрено. Автор получит уведомление.
</p>
</div>
<div class="modal-footer border-0">
<button class="btn btn-secondary" type="button" data-bs-dismiss="modal">Отмена</button>
<button class="btn btn-success" type="submit">
<i class="bi-check-lg me-1"></i>Одобрить
</button>
</div>
</form>
</div>
</div>
<!-- Reject -->
<div class="modal fade" id="reqRejectModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form class="modal-content" id="reqRejectForm">
{% csrf_token %}
<input type="hidden" name="id" id="rejectId">
<div class="modal-header border-0">
<div class="d-flex align-items-center gap-2">
<i class="bi-hand-thumbs-down text-danger fs-4"></i>
<h5 class="modal-title mb-0">Отклонить заявление?</h5>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body pt-0">
<p class="text-body-secondary mb-0">
Заявление #{{ request_obj.id }} будет отклонено. Автор получит уведомление.
</p>
</div>
<div class="modal-footer border-0">
<button class="btn btn-secondary" type="button" data-bs-dismiss="modal">Отмена</button>
<button class="btn btn-danger" type="submit">
<i class="bi-x-lg me-1"></i>Отклонить
</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script type="module" src="{% static 'vendor/emoji-picker/index.js' %}"></script>
<script>
(function() {
const requestId = {{ request_obj.id|default:0 }};
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]')?.value;
// ===== Обработка Approve =====
const approveForm = document.getElementById('reqApproveForm');
const approveModal = document.getElementById('reqApproveModal');
if (approveModal && approveForm) {
approveModal.addEventListener('show.bs.modal', function(e) {
const btn = e.relatedTarget;
document.getElementById('approveId').value = btn?.dataset.id || requestId;
});
approveForm.addEventListener('submit', async (e) => {
e.preventDefault();
const id = document.getElementById('approveId').value;
try {
const res = await fetch(`/api/v1/requests/${id}/approve/`, {
method: 'POST',
headers: { 'X-CSRFToken': csrfToken, 'Content-Type': 'application/json' },
});
if (!res.ok) throw new Error('Ошибка');
window.location.reload();
} catch (err) {
alert('Ошибка при одобрении: ' + err.message);
}
});
}
// ===== Обработка Reject =====
const rejectForm = document.getElementById('reqRejectForm');
const rejectModal = document.getElementById('reqRejectModal');
if (rejectModal && rejectForm) {
rejectModal.addEventListener('show.bs.modal', function(e) {
const btn = e.relatedTarget;
document.getElementById('rejectId').value = btn?.dataset.id || requestId;
});
rejectForm.addEventListener('submit', async (e) => {
e.preventDefault();
const id = document.getElementById('rejectId').value;
try {
const res = await fetch(`/api/v1/requests/${id}/reject/`, {
method: 'POST',
headers: { 'X-CSRFToken': csrfToken, 'Content-Type': 'application/json' },
});
if (!res.ok) throw new Error('Ошибка');
window.location.reload();
} catch (err) {
alert('Ошибка при отклонении: ' + err.message);
}
});
}
// ===== Обработка комментариев =====
const commentForm = document.querySelector('.request-comment-form');
if (commentForm) {
const textarea = commentForm.querySelector('textarea[name="text"]');
const emojiPicker = commentForm.querySelector('emoji-picker');
const submitBtn = commentForm.querySelector('button[type="submit"]');
// Emoji picker
if (textarea && emojiPicker) {
emojiPicker.addEventListener('emoji-click', (e) => {
textarea.value += e.detail.unicode;
textarea.focus();
});
}
// AJAX отправка комментария
commentForm.addEventListener('submit', async (e) => {
e.preventDefault();
const text = textarea.value.trim();
if (!text) return;
submitBtn.disabled = true;
const originalHTML = submitBtn.innerHTML;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
try {
const formData = new FormData();
formData.append('text', text);
const res = await fetch(commentForm.action, {
method: 'POST',
headers: { 'X-CSRFToken': csrfToken },
body: formData
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.detail || `HTTP ${res.status}`);
}
// Успех — очищаем форму и перезагружаем
textarea.value = '';
window.location.reload();
} catch (err) {
alert('Ошибка: ' + err.message);
submitBtn.disabled = false;
submitBtn.innerHTML = originalHTML;
}
});
}
// ===== Delete comments =====
document.addEventListener('click', async (e) => {
const deleteBtn = e.target.closest('.delete-comment-btn');
if (!deleteBtn) return;
if (!confirm('Удалить комментарий?')) return;
const requestId = deleteBtn.dataset.requestId;
const commentId = deleteBtn.dataset.commentId;
try {
const res = await fetch(
`/requests/comments/${requestId}/delete/${commentId}/`,
{
method: 'POST',
headers: { 'X-CSRFToken': csrfToken }
}
);
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.detail || `HTTP ${res.status}`);
}
window.location.reload();
} catch (err) {
alert('Ошибка: ' + err.message);
}
});
})();
</script>
{% endblock %}