Files
Inv_web/templates/network_devices.html
2026-03-23 16:48:17 +03:00

406 lines
15 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" %}
{% block body_class %}page-bg bg-devices{% endblock %}
{% block content %}
<style>
body.bg-devices {
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_printers.png", v=1) }}');
background-size: auto 40vh;
}
.network-devices-page .card {
background-color: #B3B3DA;
border: 1px solid #B3B3DA;
box-shadow: none;
}
.network-devices-page .card-body {
background-color: transparent;
}
.network-devices-page .form-control,
.network-devices-page .form-select {
background-color: #ffffff;
border: 1px solid #B3B3DA;
}
.network-devices-page .form-control:focus,
.network-devices-page .form-select:focus {
box-shadow: 0 0 0 0.1rem rgba(0, 0, 0, 0.15);
border-color: #B3B3DA;
}
.network-devices-table {
table-layout: fixed;
width: 100%;
min-width: 940px;
}
.table td,
.table th {
white-space: normal;
word-break: break-word;
}
.row-actions {
background: #f8f9fa;
}
.row-actions__inner {
max-height: 0;
opacity: 0;
transform: translateY(-6px);
transition: max-height 0.2s ease, opacity 0.2s ease, transform 0.2s ease;
overflow: hidden;
padding: 0 0.75rem;
}
.row-actions.show .row-actions__inner {
max-height: 60px;
opacity: 1;
transform: translateY(0);
}
.edit-modal {
position: fixed;
inset: 0;
background: rgba(15, 23, 32, 0.55);
display: none;
align-items: center;
justify-content: center;
z-index: 1050;
padding: 1rem;
}
.edit-modal.show {
display: flex;
}
.edit-modal__dialog {
background: #fff;
border-radius: 0.75rem;
width: min(900px, 100%);
max-height: 85vh;
overflow: auto;
padding: 1.25rem 1.5rem;
box-shadow: 0 24px 48px rgba(15, 23, 32, 0.25);
}
.edit-modal__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.edit-modal__title {
font-size: 1.1rem;
font-weight: 600;
margin: 0;
}
.edit-modal__footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1rem;
}
.network-devices-summary {
text-align: center;
margin-top: 8px;
color: #5f5f5f;
font-weight: 500;
}
</style>
<div class="network-devices-page">
<div class="d-flex align-items-center justify-content-between mb-2">
<h3 class="mb-0">Сетевое оборудование</h3>
</div>
{% if session.get('role') not in ('admin','storekeeper') %}
<div class="alert alert-secondary">Режим просмотра: изменения доступны только администратору.</div>
{% endif %}
{% if session.get('role') in ('admin','storekeeper') %}
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title mb-2">Добавить оборудование</h5>
<form method="post" action="/network-devices/add" class="row g-2">
<div class="col-md-2">
<input name="inventory_number" class="form-control" placeholder="Инв. №">
</div>
<div class="col-md-2">
<select name="brand_select" class="form-select" id="networkDeviceBrandSelect">
<option value="">Бренд...</option>
{% for b in network_device_brands %}
<option value="{{ b }}">{{ b }}</option>
{% endfor %}
<option value="__custom__">+ Добавить бренд</option>
</select>
<input name="brand_custom" id="networkDeviceBrandCustom" class="form-control mt-2 d-none" placeholder="Новый бренд">
<input type="hidden" name="brand" id="networkDeviceBrandValue">
</div>
<div class="col-md-2">
<input name="model" class="form-control" placeholder="Модель">
</div>
<div class="col-md-2">
<input name="serial_number" class="form-control" placeholder="Серийный №">
</div>
<div class="col-md-2">
<select name="type" class="form-select">
<option value="">Тип...</option>
{% for value, label in network_device_types %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-1">
<input name="date_in_operation" type="date" class="form-control" placeholder="Дата ввода">
</div>
<div class="col-md-1">
<input name="date_added" type="date" class="form-control" placeholder="Добавлено">
</div>
<div class="col-md-12 d-grid d-md-flex justify-content-md-end">
<button class="btn btn-success">Добавить</button>
</div>
</form>
</div>
</div>
{% endif %}
<div class="table-responsive">
<table class="table table-striped align-middle network-devices-table">
<colgroup>
<col style="width: 140px">
<col style="width: 150px">
<col style="width: 220px">
<col style="width: 170px">
<col style="width: 130px">
<col style="width: 130px">
<col style="width: 130px">
</colgroup>
<tr>
<th>Инвентарный номер</th>
<th>Бренд</th>
<th>Модель</th>
<th>Серийный номер</th>
<th>Тип</th>
<th>Дата ввода</th>
<th>Дата добавления</th>
</tr>
{% for id, inv, brand, model, sn, dtype, date_in_operation, date_added in rows %}
<tr class="js-network-device-row"
data-id="{{ id }}"
data-inventory-number="{{ inv }}"
data-brand="{{ brand or '' }}"
data-model="{{ model or '' }}"
data-serial-number="{{ sn or '' }}"
data-type="{{ dtype }}"
data-date-in-operation="{{ date_in_operation.strftime('%Y-%m-%d') if date_in_operation else '' }}"
data-date-added="{{ date_added.strftime('%Y-%m-%d') if date_added else '' }}">
<td>{{ inv }}</td>
<td>{{ brand or '' }}</td>
<td>{{ model or '' }}</td>
<td>{{ sn or '' }}</td>
<td>{{ network_device_type_labels.get(dtype, dtype) }}</td>
<td>{{ date_in_operation or '' }}</td>
<td>{{ (date_added|string)[:10] if date_added else '' }}</td>
</tr>
{% endfor %}
</table>
</div>
<div class="network-devices-summary">
Всего: {{ total_items }} |
Роутеры: {{ type_counts.get('router', 0) }} |
Коммутаторы: {{ type_counts.get('switch', 0) }} |
Серверы: {{ type_counts.get('server', 0) }} |
NAS: {{ type_counts.get('nas', 0) }}
</div>
{% if session.get('role') in ('admin','storekeeper') %}
<div class="edit-modal" id="networkDeviceEditModal" aria-hidden="true">
<div class="edit-modal__dialog">
<div class="edit-modal__header">
<h4 class="edit-modal__title">Редактирование сетевого оборудования</h4>
<button type="button" class="btn btn-sm btn-outline-secondary" data-modal-close>Закрыть</button>
</div>
<form method="post" action="/network-devices/edit" id="networkDeviceEditForm">
<input type="hidden" name="id" id="networkDeviceModalId">
<div class="row g-2">
<div class="col-md-3">
<input name="inventory_number" id="networkDeviceModalInv" class="form-control" placeholder="Инв. №">
</div>
<div class="col-md-3">
<select name="brand_select" id="networkDeviceModalBrandSelect" class="form-select">
<option value="">Бренд...</option>
{% for b in network_device_brands %}
<option value="{{ b }}">{{ b }}</option>
{% endfor %}
<option value="__custom__">+ Добавить бренд</option>
</select>
<input name="brand_custom" id="networkDeviceModalBrandCustom" class="form-control mt-2 d-none" placeholder="Новый бренд">
<input type="hidden" name="brand" id="networkDeviceModalBrand">
</div>
<div class="col-md-3">
<input name="model" id="networkDeviceModalModel" class="form-control" placeholder="Модель">
</div>
<div class="col-md-3">
<input name="serial_number" id="networkDeviceModalSerial" class="form-control" placeholder="Серийный №">
</div>
<div class="col-md-4">
<select name="type" id="networkDeviceModalType" class="form-select">
<option value="">Тип...</option>
{% for value, label in network_device_types %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<input name="date_in_operation" id="networkDeviceModalDateIn" type="date" class="form-control">
</div>
<div class="col-md-4">
<input name="date_added" id="networkDeviceModalDateAdded" type="date" class="form-control">
</div>
</div>
<div class="edit-modal__footer">
<button type="button" class="btn btn-outline-secondary" data-modal-close>Отмена</button>
<button class="btn btn-primary">Сохранить</button>
</div>
</form>
</div>
</div>
{% endif %}
</div>
<script>
(function() {
function setupBrandSelect(selectId, customId, valueId) {
const select = document.getElementById(selectId);
const customInput = document.getElementById(customId);
const valueInput = document.getElementById(valueId);
if (!select || !customInput || !valueInput) return;
function syncBrand() {
if (select.value === '__custom__') {
customInput.classList.remove('d-none');
valueInput.value = customInput.value.trim();
} else {
customInput.classList.add('d-none');
customInput.value = '';
valueInput.value = select.value;
}
}
select.addEventListener('change', syncBrand);
customInput.addEventListener('input', syncBrand);
syncBrand();
}
setupBrandSelect('networkDeviceBrandSelect', 'networkDeviceBrandCustom', 'networkDeviceBrandValue');
setupBrandSelect('networkDeviceModalBrandSelect', 'networkDeviceModalBrandCustom', 'networkDeviceModalBrand');
})();
(function() {
const rows = document.querySelectorAll('.js-network-device-row');
if (!rows.length) return;
const modal = document.getElementById('networkDeviceEditModal');
const form = document.getElementById('networkDeviceEditForm');
if (!modal || !form) return;
const fields = {
id: document.getElementById('networkDeviceModalId'),
inventory_number: document.getElementById('networkDeviceModalInv'),
brand: document.getElementById('networkDeviceModalBrand'),
brand_select: document.getElementById('networkDeviceModalBrandSelect'),
brand_custom: document.getElementById('networkDeviceModalBrandCustom'),
model: document.getElementById('networkDeviceModalModel'),
serial_number: document.getElementById('networkDeviceModalSerial'),
type: document.getElementById('networkDeviceModalType'),
date_in_operation: document.getElementById('networkDeviceModalDateIn'),
date_added: document.getElementById('networkDeviceModalDateAdded'),
};
function closeModal() {
modal.classList.remove('show');
modal.setAttribute('aria-hidden', 'true');
}
function openModal() {
modal.classList.add('show');
modal.setAttribute('aria-hidden', 'false');
}
modal.addEventListener('click', (e) => {
if (e.target === modal || e.target.hasAttribute('data-modal-close')) {
closeModal();
}
});
function fillModalFromRow(row) {
const map = [
'id',
'inventory_number',
'brand',
'brand_select',
'brand_custom',
'model',
'serial_number',
'type',
'date_in_operation',
'date_added',
];
map.forEach((name) => {
if (!fields[name]) return;
const dataKey = name.replace(/_/g, '-');
const attrValue = row.getAttribute(`data-${dataKey}`);
if (attrValue !== null) {
fields[name].value = attrValue || '';
}
});
const modalBrandSelect = fields.brand_select;
const modalBrandCustom = fields.brand_custom;
const modalBrandValue = fields.brand;
if (modalBrandSelect && modalBrandValue) {
const current = modalBrandValue.value || '';
if (current && Array.from(modalBrandSelect.options).some((o) => o.value === current)) {
modalBrandSelect.value = current;
if (modalBrandCustom) {
modalBrandCustom.classList.add('d-none');
modalBrandCustom.value = '';
}
} else {
modalBrandSelect.value = current ? '__custom__' : '';
if (modalBrandCustom) {
modalBrandCustom.classList.toggle('d-none', !current);
modalBrandCustom.value = current;
}
}
}
}
function showActionRow(row) {
const existing = row.parentElement.querySelector('.row-actions');
if (existing) existing.remove();
const rowId = row.getAttribute('data-id') || '';
const actionRow = document.createElement('tr');
actionRow.className = 'row-actions';
actionRow.innerHTML = `<td colspan="7">
<div class="row-actions__inner">
<button type="button" class="btn btn-sm btn-outline-primary js-open-network-device-modal">Редактировать</button>
<form method="post" action="/network-devices/delete" class="d-inline ms-2">
<input type="hidden" name="id" value="${rowId}">
<button class="btn btn-sm btn-outline-danger" type="submit">Удалить</button>
</form>
</div>
</td>`;
row.insertAdjacentElement('afterend', actionRow);
const btn = actionRow.querySelector('.js-open-network-device-modal');
btn.addEventListener('click', () => {
fillModalFromRow(row);
openModal();
});
requestAnimationFrame(() => actionRow.classList.add('show'));
}
rows.forEach((row) => {
row.addEventListener('click', (e) => {
if (e.target.closest('button, input, select, textarea, a')) return;
showActionRow(row);
});
row.addEventListener('dblclick', (e) => {
if (e.target.closest('button, input, select, textarea, a')) return;
fillModalFromRow(row);
openModal();
});
});
})();
</script>
{% endblock %}