Files
Inv_web/templates/base.html
2026-02-24 15:48:14 -08:00

649 lines
21 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.
<!doctype html>
<html>
<head>
<title>IT-Инвентаризация</title>
<link rel="icon" type="image/png" href="{{ url_for('static', filename='Icon_tab.png') }}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
@font-face {
font-family: 'Chicago Regular';
src: url('{{ url_for("static", filename="chicago-regular.ttf") }}') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
html, body {
height: 100%;
}
html {
font-size: clamp(14px, 0.95vw, 18px);
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
body.page-bg {
background-repeat: no-repeat;
background-position: right bottom;
background-size: auto 100vh;
background-attachment: fixed;
}
body.bg-cabinets { background-image: none; }
body.bg-cartridges {
background-color: #B3B3DA;
background-image:
linear-gradient(rgba(179, 179, 218, 0.75), rgba(179, 179, 218, 0.75)),
url('{{ url_for("static", filename="bg/bg_cart.png", v=6) }}');
background-size: auto 40vh;
}
body.bg-devices { background-image: url('{{ url_for("static", filename="bg_dev.png", v=2) }}'); }
body.bg-history {
background-color: #B3B3DA;
background-image:
linear-gradient(rgba(179, 179, 218, 0.75), rgba(179, 179, 218, 0.75)),
url('{{ url_for("static", filename="bg/bg_history.png", v=1) }}');
background-size: auto 40vh;
}
body.bg-orders { background-image: none; }
body.bg-home { background: #B3B3DA; }
table.table,
table.stock-table,
table.excel-table {
width: 100%;
border-collapse: collapse;
background: #f8f8f8;
font-family: 'Chicago Regular', 'Segoe UI', Arial, sans-serif;
font-size: 0.88rem;
}
table.table th,
table.table td,
table.stock-table th,
table.stock-table td,
table.excel-table th,
table.excel-table td {
border: 1px solid #7a7a7a;
padding: 6px 8px;
background: #ffffff;
}
table.table th,
table.stock-table th,
table.excel-table th {
background: linear-gradient(#d9d9d9, #c0c0c0);
color: #111;
font-weight: 600;
text-align: left;
box-shadow: inset 1px 1px 0 #f7f7f7, inset -1px -1px 0 #9a9a9a;
}
table.table tr.table-danger td,
table.stock-table tr.table-danger td,
table.excel-table tr.table-danger td {
background: #f5c2c7;
}
body:not(.bg-home) .container .btn,
body:not(.bg-home) .container .btn:hover,
body:not(.bg-home) .container .btn:focus {
background: #d9d9d9;
border: 1px solid #000;
color: #000;
padding: 2px 14px;
font-family: 'Chicago Regular', 'Segoe UI', Arial, sans-serif;
font-size: 0.85rem;
line-height: 1.2;
border-radius: 6px;
box-shadow: inset 1px 1px 0 #f7f7f7, inset -1px -1px 0 #8a8a8a;
text-decoration: none;
min-height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
}
body:not(.bg-home) .container .btn:active {
box-shadow: inset -1px -1px 0 #f7f7f7, inset 1px 1px 0 #8a8a8a;
}
body:not(.bg-home) {
font-family: 'Chicago Regular', 'Segoe UI', Arial, sans-serif;
font-size: 0.88rem;
}
body:not(.bg-home) .container .form-control,
body:not(.bg-home) .container .form-select,
body:not(.bg-home) .container textarea {
border: 1px solid #7a7a7a;
border-radius: 6px;
background: #ffffff;
font-family: 'Chicago Regular', 'Segoe UI', Arial, sans-serif;
font-size: 0.88rem;
padding: 4px 6px;
box-shadow: inset 1px 1px 0 #f2f2f2, inset -1px -1px 0 #b3b3b3;
}
.nav-wrap {
position: sticky;
top: 0;
z-index: 1000;
position: relative;
}
.nav-legacy,
.nav-legacy .navbar-brand,
.nav-legacy .nav-main-btn,
.nav-legacy .nav-link,
.nav-legacy .btn,
.nav-legacy .btn-outline-light,
.subnav,
.subnav-link,
.subnav-sublink,
.subnav-nested > summary {
font-family: 'Chicago Regular', 'Segoe UI', Arial, sans-serif;
}
.flash-stack {
position: fixed;
left: 12px;
bottom: 12px;
z-index: 2000;
display: flex;
flex-direction: column;
gap: 8px;
}
.flash-dialog {
background: #dcdcdc;
border: 1px solid #000;
box-shadow: 1px 1px 0 #000, 2px 2px 0 #777;
min-width: 280px;
max-width: 420px;
font-family: 'Chicago Regular', 'Segoe UI', Arial, sans-serif;
font-size: 0.9rem;
}
.flash-dialog__titlebar {
height: 16px;
background: #c0c0c0;
border-bottom: 1px solid #000;
}
.flash-dialog__body {
display: grid;
grid-template-columns: 36px 1fr;
gap: 10px;
align-items: start;
padding: 10px 12px 8px;
background: #efefef;
}
.flash-dialog__icon {
width: 32px;
height: 32px;
border: 1px solid #000;
border-radius: 4px;
background: #f4e135;
display: grid;
place-items: center;
font-weight: 700;
}
.flash-dialog__actions {
border-top: 1px solid #7f7f7f;
background: #dcdcdc;
padding: 6px 10px;
display: flex;
justify-content: center;
}
.flash-dialog__btn {
border: 1px solid #000;
background: #d9d9d9;
padding: 2px 18px;
font-family: 'Chicago Regular', 'Segoe UI', Arial, sans-serif;
font-size: 0.85rem;
}
.nav-legacy {
background: #d9d9d9;
border-bottom: 1px solid #808080;
box-shadow: inset 0 1px 0 #f7f7f7, inset 0 -1px 0 #b5b5b5;
padding-top: 0;
padding-bottom: 0;
min-height: 28px;
align-items: stretch;
}
.nav-legacy::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -1px;
height: 1px;
background: #000;
}
.nav-legacy .navbar-brand {
color: #111;
font-weight: 700;
letter-spacing: 0.2px;
font-size: 0.95rem;
padding: 0 0.4rem;
margin: 0;
display: flex;
align-items: center;
}
.nav-legacy .navbar-brand img {
height: 18px;
width: auto;
display: block;
}
.nav-main-btn {
color: #111;
background: transparent;
border: 0;
padding: 0 0.5rem;
font-weight: 600;
border-radius: 2px;
line-height: 1.1;
font-size: 0.95rem;
align-self: stretch;
display: flex;
align-items: center;
height: 100%;
text-decoration: none;
}
.nav-main-btn.nav-link {
color: #111;
}
.nav-main-btn.nav-link:hover,
.nav-main-btn.nav-link:focus {
color: #fff;
text-decoration: none;
}
.nav-main-btn:hover,
.nav-main-btn:focus {
color: #fff;
background: #372ba6;
box-shadow: none;
outline: none;
}
.nav-main-btn.active {
color: #fff;
background: #372ba6;
box-shadow: none;
}
.nav-legacy .btn,
.nav-legacy .btn:focus,
.nav-legacy .btn:hover {
color: #111 !important;
border-color: #9a9a9a;
background: #bdbdbd;
}
.nav-legacy .btn-outline-light,
.nav-legacy .btn-outline-light:hover,
.nav-legacy .btn-outline-light:focus {
color: #111 !important;
border-color: #9a9a9a;
background: #bdbdbd;
}
.subnav {
position: absolute;
left: 0;
width: max-content;
min-width: 0;
top: 100%;
background: #d7d7d7;
border-top: 1px solid #7d7d7d;
border-bottom: 1px solid #000;
overflow: hidden;
opacity: 0;
pointer-events: none;
transform: translateY(-4px);
transition: opacity 0.12s ease, transform 0.12s ease;
}
.subnav-inner {
width: max-content;
}
.subnav.open {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
.subnav-inner {
overflow: hidden;
}
.subnav-panel {
display: none;
padding: 0.35rem 0.5rem;
}
.subnav-panel.active {
display: flex;
flex-direction: column;
gap: 0.35rem;
align-items: stretch;
}
.subnav-panel.history-panel.active {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
width: max-content;
}
.subnav-link {
color: #111;
text-decoration: none;
padding: 0.12rem 0.4rem;
border-radius: 2px;
background: transparent;
font-size: 0.92rem;
display: block;
width: 100%;
box-sizing: border-box;
}
.subnav-link:hover {
background: #372ba6;
color: #fff;
box-shadow: none;
}
.subnav-sublink {
display: block;
padding: 0.12rem 0.4rem;
border-radius: 2px;
color: #111;
text-decoration: none;
font-size: 0.9rem;
width: 100%;
box-sizing: border-box;
}
.subnav-sublink:hover {
background: #372ba6;
color: #fff;
box-shadow: none;
}
.subnav-nested {
color: #111;
margin: 0.2rem 0.6rem;
}
.subnav-nested > summary {
list-style: none;
cursor: pointer;
font-weight: 600;
padding: 0.12rem 0.4rem;
border-radius: 2px;
width: 100%;
display: block;
box-sizing: border-box;
}
.subnav-nested > summary::-webkit-details-marker {
display: none;
}
.subnav-nested[open] > summary {
color: #111;
background: #bdbdbd;
box-shadow: inset 0 0 0 1px #9a9a9a;
}
.subnav-nested-menu {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.4rem 0.5rem 0.2rem 0.75rem;
max-height: 0;
opacity: 0;
transform: translateY(-4px);
overflow: hidden;
transition: max-height 0.18s steps(4, end), opacity 0.12s ease, transform 0.12s ease;
}
.subnav-nested[open] .subnav-nested-menu {
max-height: 120px;
opacity: 1;
transform: translateY(0);
}
.history-tabs {
display: flex;
flex-direction: column;
gap: 0.35rem;
align-items: flex-start;
width: max-content;
}
.history-tab-btn {
color: #111;
background: transparent;
border: 0;
padding: 0.12rem 0.2rem;
border-radius: 2px;
font-weight: 400;
font-size: 0.92rem;
text-align: left;
width: auto;
}
.history-tab-btn:hover,
.history-tab-btn:focus {
color: #fff;
background: #372ba6;
}
.history-tab-btn.active {
color: #fff;
background: #372ba6;
}
.history-subpanel {
display: flex;
width: max-content;
gap: 0.6rem;
flex-direction: column;
align-items: flex-start;
max-height: 0;
transform: translateY(-8px);
overflow: hidden;
pointer-events: none;
margin-top: 0;
transition: max-height 0.25s ease, transform 0.25s ease, margin-top 0.2s ease;
}
.history-subpanel.active {
max-height: 120px;
transform: translateY(0);
pointer-events: auto;
margin-top: 0.35rem;
}
.history-subpanel .subnav-sublink {
display: inline-flex;
align-items: center;
width: auto;
}
@media (prefers-reduced-motion: reduce) {
.subnav-nested-menu {
transition: none;
}
}
</style>
</head>
{% set body_class_value = (session.get('role') in ('admin','storekeeper')) and (self.body_class() | trim) or '' %}
<body class="{{ body_class_value }}">
<div class="nav-wrap">
<nav class="navbar nav-legacy px-3">
<a class="navbar-brand" href="/">
<img src="{{ url_for('static', filename='main_menu_logo.png') }}" alt="Главное меню">
</a>
{% if session.get('user') %}
{% set role = session.get('role') %}
<div class="d-flex align-items-center gap-3">
{% if role != 'viewer' %}
<button class="nav-main-btn" type="button" data-subnav="printers" onclick="window.__toggleSubnav && window.__toggleSubnav('printers')">МФУ/Принтеры</button>
<button class="nav-main-btn" type="button" data-subnav="devices" onclick="window.__toggleSubnav && window.__toggleSubnav('devices')">Устройства</button>
<button class="nav-main-btn" type="button" data-subnav="building" onclick="window.__toggleSubnav && window.__toggleSubnav('building')">Здание</button>
{% endif %}
<button class="nav-main-btn" type="button" data-subnav="history" onclick="window.__toggleSubnav && window.__toggleSubnav('history')">История</button>
<button class="nav-main-btn" type="button" data-subnav="orders" onclick="window.__toggleSubnav && window.__toggleSubnav('orders')">Заказы</button>
{% if role != 'viewer' %}
<button class="nav-main-btn" type="button" data-subnav="stock" onclick="window.__toggleSubnav && window.__toggleSubnav('stock')">Склад</button>
{% endif %}
{% if role == 'admin' %}
<a class="nav-main-btn nav-link" href="/users">Пользователи</a>
{% endif %}
</div>
{% endif %}
<div class="ms-auto d-flex align-items-center gap-2">
{% if session.get('user') %}
<span class="text-black small">Пользователь: {{ session.get('user') }} ({{ session.get('role_label', session.get('role')) }})</span>
<a class="btn btn-info btn-sm p-0" href="/report/all.zip" aria-label="Полный отчёт (ZIP)">
<img src="{{ url_for('static', filename='Logo_menu_excel.png') }}" alt="Полный отчёт (ZIP)" style="height:18px;width:auto;display:block;">
</a>
<a class="btn btn-outline-light btn-sm p-0" href="/logout" aria-label="Выйти">
<img src="{{ url_for('static', filename='Logo_menu_off.png') }}" alt="Выйти" style="height:18px;width:auto;display:block;">
</a>
{% else %}
<a class="btn btn-outline-light btn-sm" href="/login">Войти</a>
{% endif %}
</div>
</nav>
{% if session.get('user') %}
{% set role = session.get('role') %}
<div class="subnav" id="subnav">
<div class="subnav-inner">
{% if role != 'viewer' %}
<div class="subnav-panel" data-panel="printers">
<a class="subnav-link" href="/cartridges">Картриджи</a>
<a class="subnav-link" href="/consumables">Расходные материалы</a>
<a class="subnav-link" href="/catalog">Каталог</a>
</div>
<div class="subnav-panel" data-panel="devices">
<a class="subnav-link" href="/devices">МФУ/Принтеры</a>
<a class="subnav-link" href="/computers">Компьютеры/Ноутбуки</a>
<a class="subnav-link" href="/projectors">Презентационное оборудование</a>
<a class="subnav-link" href="/document-cameras">Документ-камеры</a>
</div>
<div class="subnav-panel" data-panel="building">
<a class="subnav-link" href="/cabinets">Кабинеты</a>
</div>
<div class="subnav-panel" data-panel="stock">
<a class="subnav-link" href="/stock?view=active">Склад рабочих устройств</a>
<a class="subnav-link" href="/stock?view=writtenoff">Склад списанных устройств</a>
<a class="subnav-link" href="/stock/components">Компьютерные комплектующие</a>
</div>
{% endif %}
<div class="subnav-panel history-panel" data-panel="history">
<div class="history-tabs" role="tablist" aria-label="История">
<button class="history-tab-btn" type="button" data-history-tab="cartridges">Картриджи</button>
<button class="history-tab-btn" type="button" data-history-tab="consumables">Расходные материалы</button>
<button class="history-tab-btn" type="button" data-history-tab="devices">Устройства</button>
</div>
<div class="history-subpanel" data-history-panel="cartridges" role="tabpanel">
<a class="subnav-sublink" href="/history?type=ADD">Приход</a>
<a class="subnav-sublink" href="/history?type=ISSUE">Выдача</a>
<a class="subnav-sublink" href="/history?type=ORDER">Заказы</a>
</div>
<div class="history-subpanel" data-history-panel="consumables" role="tabpanel">
<a class="subnav-sublink" href="/consumables/history?type=ADD">Приход</a>
</div>
<div class="history-subpanel" data-history-panel="devices" role="tabpanel">
<a class="subnav-sublink" href="/equipment/history/additions">История добавления</a>
<a class="subnav-sublink" href="/equipment/history/movements">Движение техники</a>
</div>
</div>
<div class="subnav-panel" data-panel="orders">
<a class="subnav-link" href="/orders">Заказанные картриджи</a>
<a class="subnav-link" href="/orders/consumables">Заказанные расходные материалы</a>
<a class="subnav-link" href="/orders/single">Разовые заказы</a>
</div>
</div>
</div>
{% endif %}
</div>
<div class="container mt-4 flex-grow-1">
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="flash-stack" role="alert">
{% for message in messages %}
<div class="flash-dialog">
<div class="flash-dialog__titlebar"></div>
<div class="flash-dialog__body">
<div class="flash-dialog__icon">!</div>
<div class="flash-dialog__text">{{ message }}</div>
</div>
<div class="flash-dialog__actions">
<button class="flash-dialog__btn" type="button" onclick="this.closest('.flash-dialog').remove()">OK</button>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<footer class="text-center text-muted py-3">
Система инвентаризации © {{ current_year }} г. Alexey Sergeevich
</footer>
<script>
(function() {
const subnav = document.getElementById('subnav');
if (!subnav) return;
const panels = Array.from(subnav.querySelectorAll('.subnav-panel'));
const triggers = Array.from(document.querySelectorAll('[data-subnav]'));
const historyTabs = Array.from(subnav.querySelectorAll('[data-history-tab]'));
const historyPanels = Array.from(subnav.querySelectorAll('[data-history-panel]'));
let active = null;
function setHistoryTab(name) {
if (!historyTabs.length || !historyPanels.length) return;
historyTabs.forEach(btn => btn.classList.toggle('active', btn.dataset.historyTab === name));
historyPanels.forEach(panel => panel.classList.toggle('active', panel.dataset.historyPanel === name));
}
function getTrigger(target) {
let el = target;
if (el && el.nodeType === 3) el = el.parentNode;
while (el && el.nodeType === 1) {
if (el.getAttribute && el.getAttribute('data-subnav')) return el;
el = el.parentNode;
}
return null;
}
function closeAll() {
panels.forEach(p => p.classList.remove('active'));
triggers.forEach(t => t.classList.remove('active'));
subnav.classList.remove('open');
active = null;
}
function openPanel(name, triggerEl) {
const panel = subnav.querySelector(`[data-panel="${name}"]`);
if (!panel) return;
panels.forEach(p => p.classList.toggle('active', p === panel));
triggers.forEach(t => t.classList.toggle('active', t.dataset.subnav === name));
subnav.classList.add('open');
const navWrap = document.querySelector('.nav-wrap');
if (navWrap && triggerEl) {
const navRect = navWrap.getBoundingClientRect();
const trigRect = triggerEl.getBoundingClientRect();
const left = Math.max(0, trigRect.left - navRect.left);
subnav.style.left = `${left}px`;
subnav.style.minWidth = '0';
subnav.style.width = 'max-content';
}
active = name;
if (name === 'history') {
setHistoryTab(null);
}
}
window.__toggleSubnav = function(name) {
if (active === name) {
closeAll();
return;
}
const triggerEl = document.querySelector(`[data-subnav="${name}"]`);
openPanel(name, triggerEl);
};
triggers.forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
});
});
historyTabs.forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const isActive = btn.classList.contains('active');
setHistoryTab(isActive ? null : btn.dataset.historyTab);
});
});
document.addEventListener('click', (e) => {
let target = e.target;
if (target && target.nodeType === 3) target = target.parentNode;
const isTrigger = !!getTrigger(target);
const isSubnav = target && subnav.contains(target);
if (!isTrigger && !isSubnav) closeAll();
});
subnav.addEventListener('click', (e) => e.stopPropagation());
})();
</script>
</body>
</html>