- Изменен текст кнопки на 'Создать заявку' на всех страницах - Добавлен параметр create_as_card=True на странице списка заявок - Перемещен модал внутрь блока content в dashboard.html (исправление ошибки Bootstrap)
358 lines
14 KiB
HTML
358 lines
14 KiB
HTML
{% extends "base.html" %}
|
||
{% load static %}
|
||
|
||
{% block title %}Закупки{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<link rel="stylesheet" href="{% static 'css/components/page-header.css' %}">
|
||
<link rel="stylesheet" href="{% static 'css/components/procurement.css' %}">
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
|
||
{# ===== ШАПКА СТРАНИЦЫ ===== #}
|
||
{% include "includes/components/page_header.html" with icon="bi-cart3" title="Закупки" no_search=True create_as_card=True create_modal="procCreateModal" create_text="Создать заявку" %}
|
||
|
||
{# ===== СТАТИСТИКА ===== #}
|
||
<div class="proc-stats">
|
||
<div class="proc-stat-card">
|
||
<div class="proc-stat-icon pending">
|
||
<i class="bi-hourglass-split"></i>
|
||
</div>
|
||
<div class="proc-stat-content">
|
||
<div class="proc-stat-value" id="statPending">{{ stats.pending|default:0 }}</div>
|
||
<div class="proc-stat-label">На согласовании</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="proc-stat-card">
|
||
<div class="proc-stat-icon approved">
|
||
<i class="bi-check-circle"></i>
|
||
</div>
|
||
<div class="proc-stat-content">
|
||
<div class="proc-stat-value" id="statApproved">{{ stats.approved|default:0 }}</div>
|
||
<div class="proc-stat-label">Одобрено</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="proc-stat-card">
|
||
<div class="proc-stat-icon budget">
|
||
<i class="bi-wallet2"></i>
|
||
</div>
|
||
<div class="proc-stat-content">
|
||
<div class="proc-stat-value" id="statBudget">
|
||
{% if budget %}
|
||
{{ budget.remaining_percent|floatformat:0 }}%
|
||
{% else %}
|
||
—
|
||
{% endif %}
|
||
</div>
|
||
<div class="proc-stat-label">Бюджет отдела</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="proc-stat-card">
|
||
<div class="proc-stat-icon equipment">
|
||
<i class="bi-pc-display"></i>
|
||
</div>
|
||
<div class="proc-stat-content">
|
||
<div class="proc-stat-value" id="statEquipment">{{ stats.equipment|default:0 }}</div>
|
||
<div class="proc-stat-label">Оборудование</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{# ===== БЫСТРЫЕ ДЕЙСТВИЯ ===== #}
|
||
<div class="row g-3 mb-4">
|
||
<div class="col-md-6 col-lg-3">
|
||
<a href="{% url 'procurement_frontend:request_list' %}" class="card text-decoration-none h-100">
|
||
<div class="card-body text-center py-4">
|
||
<i class="bi-list-ul text-primary fs-1 mb-2 d-block"></i>
|
||
<h6 class="mb-1">Заявки</h6>
|
||
<small class="text-muted">Все заявки на закупку</small>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
|
||
<div class="col-md-6 col-lg-3">
|
||
<a href="{% url 'procurement_frontend:my_requests' %}" class="card text-decoration-none h-100">
|
||
<div class="card-body text-center py-4">
|
||
<i class="bi-person-circle text-info fs-1 mb-2 d-block"></i>
|
||
<h6 class="mb-1">Мои заявки</h6>
|
||
<small class="text-muted">Созданные мной</small>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
|
||
<div class="col-md-6 col-lg-3">
|
||
<a href="{% url 'procurement_frontend:equipment_list' %}" class="card text-decoration-none h-100">
|
||
<div class="card-body text-center py-4">
|
||
<i class="bi-laptop text-success fs-1 mb-2 d-block"></i>
|
||
<h6 class="mb-1">Оборудование</h6>
|
||
<small class="text-muted">Каталог инвентаря</small>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
|
||
{% if can_approve %}
|
||
<div class="col-md-6 col-lg-3">
|
||
<a href="{% url 'procurement_frontend:pending_approvals' %}" class="card text-decoration-none h-100">
|
||
<div class="card-body text-center py-4">
|
||
<i class="bi-clipboard-check text-warning fs-1 mb-2 d-block"></i>
|
||
<h6 class="mb-1">Согласование</h6>
|
||
<small class="text-muted">Ожидают решения</small>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{# ===== МОЙ БЮДЖЕТ (если есть) ===== #}
|
||
{% if budget %}
|
||
<div class="card mb-4">
|
||
<div class="card-header">
|
||
<h5 class="mb-0">
|
||
<i class="bi-pie-chart me-2"></i>Бюджет отдела на {{ budget.quarter }} кв. {{ budget.year }}
|
||
</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="proc-budget-amounts">
|
||
<div class="proc-budget-amount">
|
||
<div class="proc-budget-value text-success">{{ budget.allocated_amount|floatformat:0 }} ₽</div>
|
||
<div class="proc-budget-label">Выделено</div>
|
||
</div>
|
||
<div class="proc-budget-amount">
|
||
<div class="proc-budget-value text-danger">{{ budget.spent_amount|floatformat:0 }} ₽</div>
|
||
<div class="proc-budget-label">Потрачено</div>
|
||
</div>
|
||
<div class="proc-budget-amount">
|
||
<div class="proc-budget-value text-warning">{{ budget.reserved_amount|floatformat:0 }} ₽</div>
|
||
<div class="proc-budget-label">Зарезервировано</div>
|
||
</div>
|
||
<div class="proc-budget-amount">
|
||
<div class="proc-budget-value text-primary">{{ budget.available_amount|floatformat:0 }} ₽</div>
|
||
<div class="proc-budget-label">Доступно</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="proc-budget-progress">
|
||
{% with percent=budget.spent_percent %}
|
||
<div class="proc-budget-bar {% if percent > 80 %}danger{% elif percent > 60 %}warning{% else %}safe{% endif %}"
|
||
style="width: {{ percent|floatformat:0 }}%"></div>
|
||
{% endwith %}
|
||
</div>
|
||
<div class="text-end mt-2">
|
||
<small class="text-muted">
|
||
Использовано {{ budget.spent_percent|floatformat:0 }}% бюджета
|
||
</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{# ===== ПОСЛЕДНИЕ ЗАЯВКИ ===== #}
|
||
<div class="card mb-4">
|
||
<div class="card-header d-flex align-items-center justify-content-between">
|
||
<h5 class="mb-0">
|
||
<i class="bi-clock-history me-2"></i>Последние заявки
|
||
</h5>
|
||
<a href="{% url 'procurement_frontend:request_list' %}" class="btn btn-sm btn-outline-primary">
|
||
Все заявки <i class="bi-arrow-right"></i>
|
||
</a>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
{% if recent_requests %}
|
||
<div class="proc-table-header d-none d-lg-block">
|
||
<div class="proc-table-row">
|
||
<div class="proc-cell"></div>
|
||
<div class="proc-cell"><h6 class="mb-0 text-secondary">Заявка</h6></div>
|
||
<div class="proc-cell"><h6 class="mb-0 text-secondary">Отдел</h6></div>
|
||
<div class="proc-cell"><h6 class="mb-0 text-secondary">Сумма</h6></div>
|
||
<div class="proc-cell"><h6 class="mb-0 text-secondary">Статус</h6></div>
|
||
<div class="proc-cell"><h6 class="mb-0 text-secondary">Дата</h6></div>
|
||
</div>
|
||
</div>
|
||
|
||
{% for request in recent_requests %}
|
||
<div class="proc-request-item">
|
||
<div class="proc-urgency-icon {{ request.urgency }}">
|
||
{% if request.urgency == 'critical' %}
|
||
<i class="bi-exclamation-triangle-fill"></i>
|
||
{% elif request.urgency == 'high' %}
|
||
<i class="bi-arrow-up-circle"></i>
|
||
{% elif request.urgency == 'medium' %}
|
||
<i class="bi-dash-circle"></i>
|
||
{% else %}
|
||
<i class="bi-arrow-down-circle"></i>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="proc-request-info">
|
||
<div class="proc-request-title">
|
||
<a href="{% url 'procurement_frontend:request_detail' request.id %}">{{ request.title }}</a>
|
||
</div>
|
||
<div class="proc-request-desc">{{ request.description|truncatechars:50 }}</div>
|
||
</div>
|
||
|
||
<div class="proc-request-dept">{{ request.department.name }}</div>
|
||
|
||
<div class="proc-request-amount">{{ request.estimated_cost|floatformat:0 }} ₽</div>
|
||
|
||
<div class="proc-request-status">
|
||
{% include "procurement/_status_badge.html" with status=request.status %}
|
||
</div>
|
||
|
||
<div class="proc-request-date">
|
||
{{ request.created_at|date:"d.m.Y" }}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
{% else %}
|
||
<div class="text-center py-5 text-muted">
|
||
<i class="bi-inbox fs-1 d-block mb-2"></i>
|
||
<p class="mb-0">Заявок пока нет</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
{# ===== МОДАЛКА СОЗДАНИЯ ===== #}
|
||
<div class="modal fade" id="procCreateModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||
<form class="modal-content" id="procCreateForm">
|
||
{% csrf_token %}
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">
|
||
<i class="bi-cart-plus me-2"></i>Новая заявка на закупку
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||
</div>
|
||
|
||
<div class="modal-body">
|
||
<div class="row g-3">
|
||
<div class="col-12">
|
||
<label class="form-label">Название заявки <span class="text-danger">*</span></label>
|
||
<input type="text" name="title" class="form-control"
|
||
placeholder="Например: Закупка ноутбуков для отдела" required maxlength="255">
|
||
</div>
|
||
|
||
<div class="col-12">
|
||
<label class="form-label">Описание / обоснование</label>
|
||
<textarea name="description" class="form-control" rows="3"
|
||
placeholder="Опишите необходимость закупки..." maxlength="2000"></textarea>
|
||
</div>
|
||
|
||
<div class="col-md-6">
|
||
<label class="form-label">Срочность <span class="text-danger">*</span></label>
|
||
<select name="urgency" class="form-select" required>
|
||
<option value="low">🟢 Низкая</option>
|
||
<option value="medium" selected>🟡 Средняя</option>
|
||
<option value="high">🟠 Высокая</option>
|
||
<option value="critical">🔴 Критическая</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="col-md-6">
|
||
<label class="form-label">Ориентировочная сумма</label>
|
||
<div class="input-group">
|
||
<input type="number" name="estimated_cost" class="form-control"
|
||
placeholder="0" min="0" step="0.01">
|
||
<span class="input-group-text">₽</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-12">
|
||
<label class="form-label">Желаемая дата получения</label>
|
||
<input type="date" name="desired_date" class="form-control">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="alert alert-info mt-3 mb-0">
|
||
<i class="bi-info-circle me-2"></i>
|
||
После создания вы сможете добавить позиции (товары) в заявку.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Отмена</button>
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="bi-check-lg me-1"></i>Создать заявку
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
// Обработка создания заявки
|
||
document.getElementById('procCreateForm')?.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
|
||
const form = e.target;
|
||
const submitBtn = form.querySelector('[type="submit"]');
|
||
const originalText = submitBtn.innerHTML;
|
||
|
||
submitBtn.disabled = true;
|
||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Создание...';
|
||
|
||
try {
|
||
const formData = new FormData(form);
|
||
const data = {
|
||
title: formData.get('title'),
|
||
description: formData.get('description') || '',
|
||
urgency: formData.get('urgency'),
|
||
estimated_cost: formData.get('estimated_cost') || null,
|
||
desired_date: formData.get('desired_date') || null,
|
||
};
|
||
|
||
const resp = await fetch('/api/procurement/requests/', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': formData.get('csrfmiddlewaretoken')
|
||
},
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
const err = await resp.json();
|
||
throw new Error(err.detail || JSON.stringify(err));
|
||
}
|
||
|
||
const request = await resp.json();
|
||
|
||
// Закрыть модалку
|
||
const modal = bootstrap.Modal.getInstance(document.querySelector('#procCreateModal'));
|
||
modal?.hide();
|
||
|
||
// Перейти к заявке
|
||
window.location.href = `/procurement/requests/${request.id}/`;
|
||
|
||
} catch (error) {
|
||
console.error('Error creating request:', error);
|
||
alert('Ошибка создания заявки: ' + error.message);
|
||
submitBtn.disabled = false;
|
||
submitBtn.innerHTML = originalText;
|
||
}
|
||
});
|
||
|
||
// Автообновление статистики каждые 60 секунд
|
||
setInterval(async () => {
|
||
try {
|
||
const resp = await fetch('/api/procurement/stats/overview/');
|
||
if (resp.ok) {
|
||
const data = await resp.json();
|
||
document.getElementById('statPending').textContent = data.pending || 0;
|
||
document.getElementById('statApproved').textContent = data.approved || 0;
|
||
document.getElementById('statEquipment').textContent = data.equipment || 0;
|
||
}
|
||
} catch (e) {
|
||
console.warn('Stats update failed:', e);
|
||
}
|
||
}, 60000);
|
||
</script>
|
||
{% endblock %}
|