добавил детальный вид итема, пока недопиленый
All checks were successful
Deploy MES Core / deploy (push) Successful in 8s

This commit is contained in:
tertelius
2026-03-29 02:49:28 +03:00
parent a4ba577206
commit b256bec04b
15 changed files with 539 additions and 226 deletions

2
.gitignore vendored
View File

@@ -61,6 +61,8 @@ cover/
local_settings.py
db.sqlite3
db.sqlite3-journal
# Media
media/
# Flask stuff:
instance/

View File

@@ -0,0 +1,120 @@
{% extends 'base.html' %}
{% block title %}ShiftFlow | {{ item.drawing_name|default:"Б/ч" }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-10 col-xl-8">
<div class="card shadow bg-dark text-light border-secondary mb-4">
<div class="card-header border-secondary d-flex justify-content-between align-items-center py-3">
<h3 class="text-accent mb-0">
<i class="bi bi-info-circle me-2"></i>
Деталь: {{ item.drawing_name|default:"Без чертежа" }}
</h3>
<span class="badge bg-secondary opacity-75 fw-normal">ID: {{ item.id }}</span>
</div>
<form method="post" class="card-body p-4">
{% csrf_token %}
<div class="row g-3 mb-4 border-bottom border-secondary pb-4">
<div class="col-md-4 col-6">
<label class="small text-muted">Дата задания:</label>
<div class="fw-bold">{{ item.date|date:"d.m.Y" }}</div>
</div>
<div class="col-md-4 col-6">
<label class="small text-muted">Станок:</label>
<div class="fw-bold"><i class="bi bi-cpu me-1"></i>{{ item.machine.name }}</div>
</div>
<div class="col-md-4 col-12">
<label class="small text-muted">Сделка/Заказ:</label>
<div class="fw-bold text-accent fs-5">№ {{ item.deal.number }}</div>
</div>
<div class="col-md-12">
<label class="small text-muted">Материал / Габариты:</label>
<div class="fw-bold small">{{ item.material.name }} (s{{ item.size_value|default:"-" }})</div>
</div>
</div>
<div class="mb-4">
<h5 class="text-accent mb-3"><i class="bi bi-files me-2"></i>Файлы задания</h5>
<div class="d-flex gap-2">
{% if item.drawing_file %}
<a href="{{ item.drawing_file.url }}" target="_blank" class="btn btn-outline-info">
<i class="bi bi-file-earmark-code me-2"></i>Открыть DXF/STEP
</a>
{% else %}
<button type="button" class="btn btn-sm btn-outline-secondary p-1" disabled><i class="bi bi-file-earmark-code me-1"></i>DXF: нет файла</button>
{% endif %}
{% if item.extra_drawing %}
<a href="{{ item.extra_drawing.url }}" target="_blank" class="btn btn-outline-danger">
<i class="bi bi-file-pdf me-2"></i>Открыть Чертеж PDF
</a>
{% else %}
<button type="button" class="btn btn-sm btn-outline-secondary p-1" disabled><i class="bi bi-file-pdf me-1"></i>PDF: нет файла</button>
{% endif %}
</div>
</div>
<div class="mb-4">
<h5 class="text-accent mb-3"><i class="bi bi-vector-pen me-2"></i>Фактическое исполнение</h5>
<div class="row g-3">
<div class="col-md-4 col-6">
<label class="form-label small text-muted">Заказано штук (План):</label>
<input type="number" class="form-control form-control-lg fw-bold bg-secondary text-info" value="{{ item.quantity_plan }}" readonly disabled>
</div>
<div class="col-md-4 col-6">
<label for="id_quantity_fact" class="form-label small text-muted">Изготовлено штук (Факт):</label>
<input type="number" name="quantity_fact" id="id_quantity_fact" class="form-control form-control-lg bg-dark text-light border-secondary" value="{{ item.quantity_fact }}" min="0" required>
</div>
<div class="col-md-4 col-12 d-flex align-items-end">
<div class="form-check form-switch bg-dark border border-secondary p-3 rounded shadow-sm w-100 h-100 d-flex align-items-center justify-content-between">
<label class="form-check-label ms-1 text-light small" for="id_is_synced_1c">Списано в 1С?</label>
<input class="form-check-input form-switch-lg stop-prop" type="checkbox" name="is_synced_1c" id="id_is_synced_1c" {% if item.is_synced_1c %}checked{% endif %}>
</div>
</div>
</div>
</div>
<div class="mb-5">
<label for="id_status" class="form-label small text-muted">Текущий статус задания:</label>
<select name="status" id="id_status" class="form-select bg-dark text-light border-secondary form-select-lg">
{% for choice_val, choice_label in form.fields.status.choices %}
<option value="{{ choice_val }}" {% if item.status == choice_val %}selected{% endif %}>
{{ choice_label }}
</option>
{% endfor %}
</select>
</div>
<div class="card-footer border-secondary bg-dark p-0 pt-4 d-flex justify-content-between">
<a href="{% url 'registry' %}" class="btn btn-outline-secondary btn-lg px-4">
<i class="bi bi-arrow-left me-2"></i>В реестр
</a>
<button type="submit" class="btn btn-outline-accent btn-lg px-5 fw-bold">
<i class="bi bi-save me-2"></i>Сохранить изменения
</button>
</div>
{% if form.errors %}
<div class="alert alert-danger mt-4 small mb-0 p-3 shadow-sm border-danger">
<h6 class="fw-bold mb-2">Обнаружены ошибки:</h6>
<ul class="mb-0">
{% for field, errors in form.errors.items %}
{% for error in errors %}
<li>{{ field|upper }}: {{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
</div>
{% endif %}
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,126 @@
{% extends 'base.html' %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-10 col-xl-8">
<div class="card shadow-sm border-secondary mb-4">
<div class="card-header border-secondary d-flex justify-content-between align-items-center py-3">
<h3 class="text-accent mb-0"><i class="bi bi-info-circle me-2"></i>{{ item.drawing_name|default:"Без названия" }}</h3>
<span class="badge bg-secondary">Сделка № {{ item.deal.number }}</span>
</div>
<form method="post" id="mainForm" class="card-body p-4">
{% csrf_token %}
<div class="row g-3 mb-4 border-bottom border-secondary pb-3 text-body">
<div class="col-md-4">
<small class="text-muted d-block">Станок</small>
<strong>{{ item.machine.name }}</strong>
</div>
<div class="col-md-4">
<small class="text-muted d-block">Материал</small>
<strong>{{ item.material.name }}</strong>
</div>
<div class="col-md-4">
<small class="text-muted d-block">План</small>
<strong class="text-info fs-5">{{ item.quantity_plan }} шт.</strong>
</div>
</div>
<div class="mb-4 d-flex gap-2">
{% if item.drawing_file %}<a href="{{ item.drawing_file.url }}" target="_blank" class="btn btn-outline-info btn-sm">DXF</a>{% endif %}
{% if item.extra_drawing %}<a href="{{ item.extra_drawing.url }}" target="_blank" class="btn btn-outline-danger btn-sm">PDF</a>{% endif %}
</div>
<input type="hidden" name="status" id="id_status" value="{{ item.status }}">
{% if user_role in 'operator,master' %}
{% if item.status == 'work' %}
<div class="bg-body-tertiary p-3 rounded border mb-4 text-center">
<h5 class="mb-3">Закрыть задание:</h5>
<div class="btn-group btn-group-lg w-100">
<button type="button" class="btn btn-success" onclick="closeTask('done')">
<i class="bi bi-check-all"></i> Выполнено
</button>
<button type="button" class="btn btn-outline-warning" onclick="showPartial()">
Частично
</button>
</div>
<div id="partialInput" class="mt-3 d-none">
<label class="small text-muted">Сколько сделано?</label>
<input type="number" name="quantity_fact" id="id_quantity_fact" class="form-control form-control-lg text-center mx-auto" style="max-width: 200px;" value="{{ item.quantity_fact }}" max="{{ item.quantity_plan }}">
</div>
</div>
{% else %}
<div class="alert alert-success">Статус: {{ item.get_status_display }}. Сделано: {{ item.quantity_fact }} шт.</div>
<input type="hidden" name="quantity_fact" value="{{ item.quantity_fact }}">
{% endif %}
{% endif %}
{% if user_role == 'admin' %}
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="small text-muted">Статус задания (Админ)</label>
<select name="status" class="form-select border-secondary">
{% for val, name in form.fields.status.choices %}
<option value="{{ val }}" {% if item.status == val %}selected{% endif %}>{{ name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="small text-muted">Факт (шт)</label>
<input type="number" name="quantity_fact" class="form-control border-secondary" value="{{ item.quantity_fact }}">
</div>
</div>
{% endif %}
{% if user_role in 'admin,clerk' %}
{% if item.status == 'done' or item.quantity_fact > 0 %}
<div class="form-check form-switch p-3 rounded border border-warning mb-4 bg-body-tertiary d-flex justify-content-between align-items-center">
<label class="form-check-label fw-bold ms-2" for="sync1c">Списано в 1С</label>
<input class="form-check-input ms-0" style="width: 3em; height: 1.5em;" type="checkbox" name="is_synced_1c" id="sync1c" {% if item.is_synced_1c %}checked{% endif %}>
</div>
{% else %}
<div class="text-muted small mb-4"><i class="bi bi-info-circle me-1"></i>Списание будет доступно после выполнения.</div>
{% endif %}
{% if user_role == 'clerk' %}<input type="hidden" name="quantity_fact" value="{{ item.quantity_fact }}">{% endif %}
{% endif %}
<div class="d-flex justify-content-between mt-4">
<a href="{% url 'registry' %}" class="btn btn-outline-secondary">Назад</a>
<button type="submit" class="btn btn-outline-accent px-5 fw-bold">
<i class="bi bi-save me-2"></i>
{% if user_role in 'operator,master' %}Закрыть задание{% else %}Сохранить{% endif %}
</button>
</div>
</form>
</div>
</div>
</div>
<script>
function closeTask(status) {
document.getElementById('id_status').value = status;
// Если "Выполнено", автоматом ставим факт = плану
if(status === 'done') {
const factInput = document.getElementById('id_quantity_fact');
if(factInput) factInput.value = "{{ item.quantity_plan }}";
else {
let hiddenFact = document.createElement('input');
hiddenFact.type = 'hidden';
hiddenFact.name = 'quantity_fact';
hiddenFact.value = "{{ item.quantity_plan }}";
document.getElementById('mainForm').appendChild(hiddenFact);
}
}
document.getElementById('mainForm').submit();
}
function showPartial() {
document.getElementById('partialInput').classList.remove('d-none');
document.getElementById('id_status').value = 'part'; // Статус Частично
}
</script>
{% endblock %}

View File

@@ -1,65 +0,0 @@
{% extends 'base.html' %}
{% block content %}
{% include 'shiftflow/partials/_filter.html' %}
{% if not selected_machines %}
<div class="alert alert-warning text-center mt-4">
<i class="bi bi-info-circle me-2"></i> Станки не выбраны. Выберите станки в фильтре для отображения позиций.
</div>
{% else %}
{% if in_work is not None %}
<h6 class="text-primary mt-4 fw-bold"><i class="bi bi-play-circle me-2"></i>В РАБОТЕ</h6>
<div class="table-responsive rounded border shadow-sm mb-4">
<table class="table table-hover align-middle mb-0">
<thead class="table-custom-header small fw-bold text-uppercase">
<tr>
<th style="width: 80px;">Сделка</th>
<th>Деталь / Чертеж</th>
<th>Материал</th>
<th class="text-center">План</th>
<th class="text-center">Факт</th>
{% if user_role in 'admin,technologist,master,operator' %}
<th class="text-end">Действия</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for item in in_work %}
<tr>
<td class="small">{{ item.deal.number }}</td>
<td><strong>{{ item.drawing_name }}</strong> <small class="text-muted">({{ item.machine.name }})</small></td>
<td><span class="badge bg-secondary opacity-75">{{ item.material.name }}</span></td>
<td class="text-center fw-bold">{{ item.quantity_plan }}</td>
<td class="text-center" style="width: 100px;">
<input type="number" class="form-control form-control-sm text-center" value="{{ item.quantity_fact|default:0 }}" {% if user_role == 'clerk' %}readonly{% endif %}>
</td>
{% if user_role in 'admin,technologist,master,operator' %}
<td class="text-end">
<button class="btn btn-sm btn-outline-success"><i class="bi bi-check2-all"></i></button>
</td>
{% endif %}
</tr>
{% empty %}
<tr><td colspan="6" class="text-center py-4 text-muted small">Активных заданий нет</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% if backlog is not None %}
<h6 class="text-danger fw-bold"><i class="bi bi-exclamation-triangle me-2"></i>НЕДОДЕЛЫ (ХВОСТЫ)</h6>
<div class="table-responsive rounded border shadow-sm mb-4">
</div>
{% endif %}
{% if done_items is not None %}
<h6 class="text-success fw-bold"><i class="bi bi-check-circle me-2"></i>ЗАВЕРШЕНО</h6>
<div class="table-responsive rounded border shadow-sm">
</div>
{% endif %}
{% endif %}
{% endblock %}

View File

@@ -1,38 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow bg-dark text-light border-secondary">
<div class="card-header border-secondary d-flex justify-content-between align-items-center">
<h3 class="text-accent mb-0">Реестр деталей</h3>
{% if user_role == 'admin' or user_role == 'technologist' %}
<button class="btn btn-sm btn-outline-accent">+ Добавить заказ</button>
{% endif %}
</div>
<div class="card-body">
<table class="table table-dark table-hover">
<thead>
<tr class="table-custom-header">
<th>ID</th>
<th>Наименование</th>
<th>Кол-во</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.quantity }}</td>
<td><span class="badge bg-secondary">{{ item.status }}</span></td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center text-muted">Деталей пока нет</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@@ -1,12 +1,14 @@
{% extends 'base.html' %}
{% block content %}
<div class="text-center">
<h1 class="text-accent mb-4 display-3 fw-bold">
<i class="bi bi-gear-fill me-3"></i>ShiftFlow
</h1>
<a href="{% url 'login' %}" class="btn btn-lg btn-outline-accent px-5 py-3 fw-bold shadow">
ВОЙТИ В СИСТЕМУ
</a>
<div class="flex-center-center">
<div class="text-center">
<h1 class="text-accent mb-4 display-3 fw-bold">
<i class="bi bi-gear-fill me-3"></i>ShiftFlow
</h1>
<a href="{% url 'login' %}" class="btn btn-lg btn-outline-accent px-5 py-3 fw-bold shadow">
ВОЙТИ В СИСТЕМУ
</a>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,86 @@
{% extends 'base.html' %}
{% block content %}
<div class="card shadow border-secondary">
<div class="card-header border-secondary py-3">
<h3 class="text-accent mb-0"><i class="bi bi-list-task me-2"></i>Реестр заданий</h3>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead>
<tr class="table-custom-header">
<th>Дата</th>
<th>Сделка</th>
<th>Станок</th>
<th>Наименование</th>
<th>Габариты</th>
<th>План / Факт</th>
<th>Материал</th>
<th class="text-center">Файлы</th>
<th class="text-center">1С</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr class="clickable-row" data-href="{% url 'item_detail' item.pk %}">
<td class="small">{{ item.date|date:"d.m.y" }}</td>
<td><span class="text-accent fw-bold">{{ item.deal.number }}</span></td>
<td><span class="badge bg-dark border border-secondary">{{ item.machine.name }}</span></td>
<td class="fw-bold">{{ item.drawing_name }}</td>
<td class="small">{{ item.size_value }}</td>
<td>
<span class="text-info fw-bold">{{ item.quantity_plan }}</span> /
<span class="text-success">{{ item.quantity_fact }}</span>
</td>
<td class="small text-muted">{{ item.material.name }}</td>
<td class="text-center">
{% if item.drawing_file %}
<a href="{{ item.drawing_file.url }}" target="_blank" class="btn btn-sm btn-outline-info p-1 stop-prop" title="DXF/STEP">
<i class="bi bi-file-earmark-code"></i>
</a>
{% endif %}
{% if item.extra_drawing %}
<a href="{{ item.extra_drawing.url }}" target="_blank" class="btn btn-sm btn-outline-danger p-1 stop-prop" title="Чертеж PDF">
<i class="bi bi-file-pdf"></i>
</a>
{% endif %}
</td>
<td class="text-center">
{% if item.is_synced_1c %}
<i class="bi bi-check-circle-fill text-success" title="Учтено"></i>
{% else %}
<i class="bi bi-clock-history text-muted" title="Ожидает"></i>
{% endif %}
</td>
<td>
<span class="badge {% if item.status == 'work' %}bg-primary{% elif item.status == 'done' %}bg-success{% else %}bg-secondary{% endif %}">
{{ item.get_status_display }}
</span>
</td>
</tr>
{% empty %}
<tr><td colspan="10" class="text-center p-5 text-muted">Заданий не найдено</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
const rows = document.querySelectorAll(".clickable-row");
rows.forEach(row => {
row.addEventListener("click", function(e) {
// Если нажали на ссылку файла (класс stop-prop), не переходим на страницу деталей
if (e.target.closest('.stop-prop')) return;
// Иначе переходим по ссылке из data-href
window.location.href = this.dataset.href;
});
});
});
</script>
{% endblock %}

View File

@@ -1,5 +1,5 @@
from django.urls import path
from .views import IndexView, RegistryView
from .views import IndexView, ItemUpdateView, RegistryView
urlpatterns = [
# Главная страница (путь пустой)
@@ -7,4 +7,5 @@ urlpatterns = [
# Реестр
path('registry/', RegistryView.as_view(), name='registry'),
path('item/<int:pk>/', ItemUpdateView.as_view(), name='item_detail'),
]

View File

@@ -1,72 +0,0 @@
from django.shortcuts import render
from .models import Item, Machine
from django.utils import timezone
from datetime import datetime
def items_list_view(request):
# Если не авторизован, просто отдаем пустой контекст для страницы входа
if not request.user.is_authenticated:
return render(request, 'shiftflow/items_list.html', {})
# ОПРЕДЕЛЕНИЕ РОЛИ (Ищем в группах Джанго или ставим суперюзера)
user_role = 'guest'
if request.user.is_superuser:
user_role = 'admin'
elif request.user.groups.filter(name='Технолог').exists():
user_role = 'technologist'
elif request.user.groups.filter(name='Мастер').exists():
user_role = 'master'
elif request.user.groups.filter(name='Оператор').exists():
user_role = 'operator'
elif request.user.groups.filter(name='Учетчик').exists():
user_role = 'clerk'
# ФИЛЬТРЫ
all_machines = Machine.objects.all()
all_machine_ids = list(all_machines.values_list('id', flat=True))
# Проверяем, был ли нажат фильтр (есть ли параметр 'filtered' в URL)
is_filtered = 'filtered' in request.GET
if is_filtered:
selected_machines = [int(x) for x in request.GET.getlist('m_ids')]
selected_statuses = request.GET.getlist('statuses')
start_date = request.GET.get('start_date')
end_date = request.GET.get('end_date')
else:
# Значения по умолчанию (при первом заходе или сбросе)
selected_machines = all_machine_ids
selected_statuses = ['work', 'partial'] # В работе и недоделы
start_date = timezone.now().strftime('%Y-%m-%d')
end_date = timezone.now().strftime('%Y-%m-%d')
# ВЫБОРКА ДАННЫХ
items = Item.objects.select_related('deal', 'material', 'machine')
# Защита логики: если станки не выбраны — список пуст
if not selected_machines:
items = Item.objects.none()
else:
items = items.filter(
machine_id__in=selected_machines,
status__in=selected_statuses,
date__range=[start_date, end_date]
)
# Разбиваем по статусам для вывода в разные таблицы (если они выбраны в фильтре)
in_work = items.filter(status='work') if 'work' in selected_statuses else None
backlog = items.filter(status='partial') if 'partial' in selected_statuses else None
done_items = items.filter(status='done') if 'done' in selected_statuses else None
context = {
'user_role': user_role,
'machines': all_machines,
'selected_machines': selected_machines,
'selected_statuses': selected_statuses,
'start_date': start_date,
'end_date': end_date,
'in_work': in_work,
'backlog': backlog,
'done_items': done_items,
}
return render(request, 'shiftflow/items_list.html', context)

View File

@@ -1,5 +1,6 @@
from django.shortcuts import redirect
from django.views.generic import TemplateView, ListView
from django.urls import reverse_lazy
from django.views.generic import TemplateView, ListView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Item # Проверь, как точно называется твоя модель деталей/заказов
@@ -17,16 +18,40 @@ class IndexView(TemplateView):
# Класс реестра деталей (защищен LoginRequiredMixin)
class RegistryView(LoginRequiredMixin, ListView):
model = Item
template_name = 'shiftflow/items_list.html'
template_name = 'shiftflow/registry.html'
context_object_name = 'items'
def get_queryset(self):
# Позже здесь добавим: .filter(machine__in=request.user.profile.machines.all())
return Item.objects.all().order_by('-id')
# Сортируем: сначала статус (по алфавиту или логике choices),
# затем по дате (свежие сверху), по станку и по номеру сделки
return Item.objects.all().order_by('status', '-date', 'machine__name', 'deal__number')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Передаем роль в шаблон, чтобы скрывать/показывать кнопки
if hasattr(self.request.user, 'profile'):
context['user_role'] = self.request.user.profile.role
return context
return context
# Вьюха детального вида и редактирования
class ItemUpdateView(LoginRequiredMixin, UpdateView):
model = Item
template_name = 'shiftflow/item_detail.html'
# Перечисляем поля, которые можно редактировать (укажи нужные)
fields = [
'drawing_name', 'machine', 'quantity_plan', 'quantity_fact',
'material', 'size_value', 'status', 'is_synced_1c', 'extra_drawing'
]
context_object_name = 'item'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Обязательно добавляем роль в контекст этого шаблона!
if hasattr(self.request.user, 'profile'):
context['user_role'] = self.request.user.profile.role
return context
def get_success_url(self):
# После сохранения возвращаемся в реестр
return reverse_lazy('registry')

View File

@@ -1,38 +1,105 @@
/* Акцентные цвета для темной темы */
[data-bs-theme="dark"] {
--bs-body-bg: #121212;
--bs-body-color: #e9ecef;
--bs-accent: #ffc107; /* Тот самый желтый */
/* --- ГЛОБАЛЬНЫЕ НАСТРОЙКИ --- */
body {
display: flex;
flex-direction: column;
min-height: 100vh;
/* Убрали общее центрирование, чтобы реестр был сверху */
justify-content: flex-start;
}
[data-bs-theme="dark"] .table-custom-header {
/* Навбар и Футер: жестко фиксируем цвет для обеих тем */
.navbar, .footer-custom {
/* Темный графит, который хорошо смотрится и там, и там */
background-color: #2c3034 !important;
border-bottom: 1px solid #3d4246 !important;
border-top: 1px solid #3d4246 !important; /* Для футера */
}
/* Принудительно светлый текст для футера и навбара */
.navbar .nav-link,
.navbar .navbar-brand,
.footer-custom span,
.footer-custom strong {
color: #e9ecef !important;
}
/* Состояние активной ссылки в меню */
.nav-link.active {
color: var(--bs-accent) !important;
border-bottom: 2px solid var(--bs-accent);
}
/* Цвет ссылок в темном навбаре, чтобы не сливались */
.navbar .nav-link, .navbar .navbar-brand, .navbar .text-reset {
color: #e9ecef !important;
}
/* --- РЕЕСТР --- */
/* Делаем строку таблицы визуально кликабельной */
.clickable-row {
cursor: pointer;
transition: background-color 0.2s ease;
}
/* Подсветка при наведении */
.clickable-row:hover {
background-color: rgba(255, 193, 7, 0.05) !important; /* Легкий отсвет нашего акцента */
}
/* --- ТЕМЫ --- */
[data-bs-theme="dark"] {
--bs-body-bg: #121212; /* Глубокий черный фон */
--bs-body-color: #e9ecef; /* Светло-серый текст */
--bs-accent: #ffc107; /* Желтый акцент (Amber) */
}
[data-bs-theme="light"] {
--bs-body-bg: #f8f9fa; /* Почти белый фон */
--bs-body-color: #212529; /* Темный текст */
--bs-accent: #0d6efd; /* Синий акцент для светлой темы */
}
/* --- ТАБЛИЦА И КАРТОЧКИ --- */
/* Заголовок таблицы: всегда темный с акцентным текстом */
.table-custom-header {
background-color: #1e1e1e !important;
color: var(--bs-accent) !important;
font-size: 0.9rem;
text-transform: uppercase; /* Все буквы заглавные */
}
/* Акцентные цвета для светлой темы */
[data-bs-theme="light"] {
--bs-body-bg: #e2e2e2;
--bs-body-color: #212529;
--bs-accent: #0f5132; /* Темно-зеленый */
/* Фикс для таблиц в светлой теме */
[data-bs-theme="light"] .table {
--bs-table-bg: #ffffff;
--bs-table-color: #212529;
--bs-table-hover-bg: #f1f3f5;
}
/* Общие классы */
.text-accent {
color: var(--bs-accent) !important;
}
/* --- ВСПОМОГАТЕЛЬНЫЕ КЛАССЫ --- */
/* Текст акцентного цвета */
.text-accent { color: var(--bs-accent) !important; }
/* Кнопка с контуром акцентного цвета */
.btn-outline-accent {
color: var(--bs-accent) !important;
border-color: var(--bs-accent) !important;
}
/* Состояние кнопки при наведении */
.btn-outline-accent:hover {
background-color: var(--bs-accent) !important;
color: #000 !important;
color: #000 !important; /* Текст становится черным для контраста */
}
/* Фикс для навигации */
.nav-link.active {
border-bottom: 2px solid var(--bs-accent);
/* Специальный класс для центрирования окна логина (вернем его только там) */
.flex-center-center {
display: flex;
flex-grow: 1;
align-items: center; /* Центр по вертикали */
justify-content: center; /* Центр по горизонтали */
}

View File

@@ -15,7 +15,7 @@
{% include 'components/_navbar.html' %}
{% endif %}
<main class="container-fluid py-3 flex-grow-1 d-flex flex-column justify-content-center">
<main class="container-fluid py-3 flex-grow-1 d-flex flex-column">
{% block content %}{% endblock %}
</main>

View File

@@ -1,7 +1,8 @@
<footer class="mt-auto py-3 bg-body-tertiary border-top">
<footer class="footer-custom mt-auto py-3">
<div class="container-fluid text-center">
<span class="text-muted small">
Система учета сменных заданий, разработана <strong>ACK</strong> &copy; 2026
<i class="bi bi-cpu ms-2 text-accent"></i>
</span>
</div>
</footer>

View File

@@ -0,0 +1,61 @@
<nav class="navbar navbar-expand-lg border-bottom shadow-sm">
<div class="container-fluid">
<a class="navbar-brand fw-bold text-accent" href="/">
<i class="bi bi-gear-fill me-2"></i>ShiftFlow
</a>
<button class="navbar-toggler text-light border-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon" style="filter: invert(1);"></span>
</button>
<div class="collapse navbar-collapse">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'items_list' %}active{% endif %}" href="{% url 'registry' %}">Реестр</a>
</li>
{% if user_role in 'admin,technologist' %}
<li class="nav-item"><a class="nav-link" href="#">Планирование</a></li>
{% endif %}
{% if user_role in 'admin,technologist,master,operator' %}
<li class="nav-item"><a class="nav-link" href="#">Закрытие</a></li>
{% endif %}
{% if user_role in 'admin,technologist,clerk' %}
<li class="nav-item"><a class="nav-link" href="#">Списание</a></li>
{% endif %}
</ul>
<div class="d-flex align-items-center gap-3">
<span class="badge bg-secondary opacity-75 px-3 py-2 fw-normal">
<i class="bi bi-person-circle me-1"></i>
{% if user_role == 'admin' %}Админ
{% elif user_role == 'technologist' %}Технолог
{% elif user_role == 'master' %}Мастер
{% elif user_role == 'operator' %}Оператор
{% elif user_role == 'clerk' %}Учетчик
{% endif %}
({{ request.user.username|upper }})
</span>
{% if user_role == 'admin' %}
<a href="/admin/" class="btn btn-link text-decoration-none text-reset p-0" title="Админка">
<i class="bi bi-shield-lock fs-5"></i>
</a>
{% endif %}
<button class="btn btn-link text-reset p-0" onclick="toggleTheme()" title="Сменить тему">
<i id="theme-icon" class="bi fs-5"></i>
</button>
<form action="{% url 'logout' %}" method="post" class="d-inline">
{% csrf_token %}
<button type="submit" class="btn btn-link text-danger p-0 ms-2" title="Выйти">
<i class="bi bi-box-arrow-right fs-5"></i>
</button>
</form>
</div>
</div>
</div>
</nav>

View File

@@ -4,10 +4,14 @@
<i class="bi bi-gear-fill me-2"></i>ShiftFlow
</a>
<div class="collapse navbar-collapse">
<button class="navbar-toggler border-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon" style="filter: invert(1);"></span> </button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link {% if request.resolver_match.url_name == 'items_list' %}active{% endif %}" href="{% url 'registry' %}">Реестр</a>
<a class="nav-link {% if request.resolver_match.url_name == 'registry' %}active{% endif %}" href="{% url 'registry' %}">Реестр</a>
</li>
{% if user_role in 'admin,technologist' %}
@@ -23,26 +27,19 @@
{% endif %}
</ul>
<div class="d-flex align-items-center gap-3">
<span class="badge bg-secondary opacity-75 px-3 py-2 fw-normal">
<i class="bi bi-person-circle me-1"></i>
{% if user_role == 'admin' %}Админ
{% elif user_role == 'technologist' %}Технолог
{% elif user_role == 'master' %}Мастер
{% elif user_role == 'operator' %}Оператор
{% elif user_role == 'clerk' %}Учетчик
{% endif %}
({{ request.user.username|upper }})
</span>
<div class="d-flex align-items-center gap-2 mt-lg-0 mt-3">
{% if user_role == 'admin' %}
<a href="/admin/" class="btn btn-link text-decoration-none text-reset p-0" title="Админка">
<i class="bi bi-shield-lock fs-5"></i>
</a>
<a href="/admin/" class="btn btn-link text-decoration-none text-reset p-0 me-1" title="Админка">
<i class="bi bi-shield-lock fs-5 text-accent"></i> </a>
{% endif %}
<span class="badge bg-secondary opacity-75 px-3 py-2 fw-normal">
<i class="bi bi-person-circle me-1"></i> ({{ request.user.username|upper }})
</span>
<button class="btn btn-link text-reset p-0" onclick="toggleTheme()" title="Сменить тему">
<i id="theme-icon" class="bi fs-5"></i>
<i id="theme-icon" class="bi fs-5 text-accent"></i>
</button>
<form action="{% url 'logout' %}" method="post" class="d-inline">