406 lines
15 KiB
HTML
406 lines
15 KiB
HTML
{% 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 %}
|