Corp/backend/templates/requests_app/request_process.html
Игорь Надеин 8159c6f472 feat: Replace Bootstrap buttons with custom button system
### Custom Button System Implementation

Created modern button system to replace Bootstrap buttons:

- ✅ Created core/_buttons-custom.scss (550+ lines)
- ✅ CSS Custom Properties for easy customization
- ✅ 8 color variants (primary, secondary, success, danger, warning, info, light, dark)
- ✅ Outline and Ghost variants
- ✅ 4 sizes (sm, md, lg, xl)
- ✅ Special variants (block, icon, circle, link)
- ✅ Loading state with spinner animation
- ✅ Button groups support
- ✅ Modern animations (hover, active, focus)

### Template Migration

Migrated 65 HTML templates using automated Python script:
- Replaced .btn → .button
- Replaced .btn-primary → .button--primary
- Replaced .btn-outline-* → .button--outline button--*
- Replaced size classes .btn-sm → .button--sm
- Total changes: 354 → 347 button usages

### Results

- app.css: 137 KB → 148 KB (+11 KB for custom buttons)
- Templates changed: 65 files
- Remaining .btn classes: 133 (mostly btn-close for modals)
- New .button classes: 347

### Backward Compatibility

Kept @extend rules in _buttons-custom.scss for gradual migration.
Can be removed after complete migration.

Next step: Remove Bootstrap button imports from custom-bootstrap.scss
2026-01-15 14:23:44 +03:00

297 lines
13 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 django_bootstrap5 i18n %}
{# ====== Страница обработки заявления HR/Руководителем ====== #}
{% block title %}Рассмотрение заявления{% endblock %}
{% block content %}
<div class="container">
<div class="row gx-0">
<div class="col-12">
{# — Шапка: заголовок + кнопка назад — #}
<div class="section-header">
<i class="bi-clipboard-check fs-2 text-primary"></i>
<h2 class="title mb-0">Рассмотрение заявления</h2>
<div class="ms-auto">
<a href="{% url 'requests:request_list' %}" class="button button--ghost">
<i class="bi-arrow-left"></i> К списку
</a>
</div>
</div>
{# — Карточка заявки: кто подал, статус, тип, даты, комментарий, вложение — #}
<article class="card mb-3">
<header class="card-header">
<div class="card-icon" style="width:48px;height:48px;">
{% if request_obj.employee.avatar %}
<img src="{{ request_obj.employee.avatar }}" alt="Аватар сотрудника"
loading="lazy">
{% 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' request_obj.employee.pk %}"
class="card-title text-decoration-none">
{{ request_obj.employee.get_full_name|default:request_obj.employee.username }}
</a>
{# Бейдж статуса заявки #}
{% 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 mt-1">
{# Дата создания #}
<time datetime="">
{{ request_obj.created_at }}
</time>
{# Период, если указан #}
{% if request_obj.date_from or request_obj.date_to %}
· Период:
{{ request_obj.date_from }}
{% if request_obj.date_to %} — {{ request_obj.date_to }}{% endif %}
{% endif %}
</div>
</div>
</header>
<div class="card-body pt-0">
{# Комментарий сотрудника (если есть) #}
<div class="mb-2">
<span class="text-secondary">Комментарий сотрудника:</span>
{{ request_obj.comment|default:"—" }}
</div>
{# Вложение сотрудника (если есть) #}
{% if request_obj.attachment_url %}
<a class="button button--ghost d-inline-flex align-items-center gap-1"
href="{{ request_obj.attachment_url }}" target="_blank"
rel="noopener noreferrer" download>
<i class="bi-paperclip"></i><span>Вложение сотрудника</span>
</a>
{% endif %}
</div>
</article>
{# — Инфоблок при финальном статусе — #}
{% if request_obj.is_final %}
<div class="alert alert-warning d-flex align-items-start gap-2">
<i class="bi-exclamation-triangle-fill"></i>
<div>
По заявлению установлен финальный статус (изменить статус нельзя).
Вы можете добавить служебный комментарий и/или вложение.
</div>
</div>
{% endif %}
{# — Форма решения: статус + служебный комментарий/вложение — #}
<article class="card mb-3">
<header class="card-header">
<div class="card-icon" style="width:44px;height:44px;">
<i class="bi-check2-circle"></i>
</div>
<div class="card-meta">
<div class="card-title">Принять решение</div>
<div class="card-subtitle">Статус + служебный комментарий/вложение</div>
</div>
</header>
<div class="card-body pt-0">
<form method="post" enctype="multipart/form-data" id="decisionForm">
{% csrf_token %}
{# Неформатные ошибки формы (например, запрет смены финального статуса) #}
{% if form.non_field_errors %}
<div class="alert alert-danger mb-2">
{{ form.non_field_errors }}
</div>
{% endif %}
{# Поля формы через django-bootstrap5 #}
{% bootstrap_form form %}
<div class="d-flex gap-2 mt-2">
<button type="submit" class="button button--primary">
<i class="bi-check-circle"></i> Сохранить решение
</button>
<a href="{% url 'requests:request_list' %}" class="button button--ghost">
Отмена
</a>
</div>
</form>
</div>
</article>
{# — Комментарии по заявлению — #}
<article class="card" id="comments">
<header class="card-header">
<div class="card-icon" style="width:44px;height:44px;">
<i class="bi-chat-dots"></i>
</div>
<div class="card-meta">
<div class="card-title">Комментарии</div>
<div class="card-subtitle">
{# Избегаем неправильных русских окончаний — просто покажем число #}
{% with cnt=request_obj.comments.count %}
Комментариев: {{ cnt }}
{% endwith %}
</div>
</div>
</header>
<div class="card-body pt-0">
{# Список комментариев. Используем {% empty %}, чтобы корректно показать «нет комментариев» #}
<div class="d-flex flex-column gap-2 mb-3">
{% for c in request_obj.comments.all %}
<div class="bg-body-tertiary border rounded p-2">
<div class="small d-flex justify-content-between align-items-start">
<div>
<span class="fw-semibold">
{{ c.author.get_full_name|default:c.author.username }}
</span>
<span class="text-secondary">·</span>
<time class="text-muted">{{ c.created_at }}</time>
</div>
<form method="post"
action="{% url 'requests_app:request_comment_delete' request_obj.pk c.pk %}?next={{ request.get_full_path|urlencode }}#comments"
onsubmit="return confirm('Удалить комментарий №{{ c.pk }}?');">
{% csrf_token %}
<button class="button button--sm button--ghost text-danger" type="submit" title="Удалить">
<i class="bi-trash"></i>
</button>
</form>
</div>
<div class="small mt-1">{{ c.text|linebreaksbr }}</div>
</div>
{% empty %}
<div class="text-secondary small mb-3">Комментариев пока нет.</div>
{% endfor %}
</div>
<form class="mt-2 request-comment-form"
method="post"
action="{% url 'requests_app:request_comment_add' request_obj.pk %}?next={{ request.get_full_path|urlencode }}#comments">
{% csrf_token %}
<div class="message-field message-field--compact">
<!-- Кнопка выбора эмодзи -->
<div class="dropdown message-emoji">
<button type="button" class="button button--ghost btn-emoji message-icon-button"
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="Оставить комментарий…"
maxlength="{{ max_comment_len|default:1000 }}"></textarea>
<button class="button button--primary message-send" type="submit" title="Отправить">
<i class="bi-send"></i>
</button>
</div>
</form>
</div>
</article>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{{ block.super }}
<script src="{% static 'js/components/requestComments.js' %}"></script>
<!-- Emoji Picker Library -->
<script type="module" src="{% static 'vendor/emoji-picker/index.js' %}"></script>
<script>
// Обработка формы решения без перезагрузки страницы
document.addEventListener('DOMContentLoaded', function() {
const decisionForm = document.getElementById('decisionForm');
if (!decisionForm) return;
decisionForm.addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const submitBtn = this.querySelector('button[type="submit"]');
const originalText = submitBtn.innerHTML;
try {
// Показываем что загружаем
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Сохранение...';
const response = await fetch(this.action || '', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('HTTP ' + response.status);
}
// Получаем обновленное содержимое
const html = await response.text();
// Заменяем контент страницы
const parser = new DOMParser();
const newDoc = parser.parseFromString(html, 'text/html');
const newContent = newDoc.querySelector('[role="main"]') || newDoc.body;
const currentContent = document.querySelector('[role="main"]') || document.body;
currentContent.innerHTML = newContent.innerHTML;
// Показываем успех
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="bi-check-circle"></i> ✓ Сохранено';
submitBtn.classList.add('button--success');
submitBtn.classList.remove('button--primary');
// Возвращаем кнопку в исходное состояние через 2 секунды
setTimeout(() => {
submitBtn.innerHTML = originalText;
submitBtn.classList.remove('button--success');
submitBtn.classList.add('button--primary');
}, 2000);
// Прокручиваем вверх к результату
window.scrollTo({ top: 0, behavior: 'smooth' });
} catch (error) {
alert('Ошибка при сохранении: ' + error.message);
submitBtn.disabled = false;
submitBtn.innerHTML = originalText;
}
});
});
</script>
{% endblock %}