Files
Inv_web/templates/devices.html
2026-03-04 15:59:10 -08:00

578 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.
{% 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;
}
.devices-page .card {
background-color: #B3B3DA;
border: 1px solid #B3B3DA;
box-shadow: none;
}
.devices-page .card-body {
background-color: transparent;
}
.devices-page .form-control,
.devices-page .form-select {
background-color: #ffffff;
border: 1px solid #B3B3DA;
}
.devices-page .form-control:focus,
.devices-page .form-select:focus {
box-shadow: 0 0 0 0.1rem rgba(0, 0, 0, 0.15);
border-color: #B3B3DA;
}
.device-row .form-control:disabled,
.device-row .form-select:disabled {
background-color: transparent;
border-color: transparent;
color: inherit;
opacity: 1;
padding-left: 0;
}
.device-row .form-select:disabled {
appearance: none;
}
.table td,
.table th {
white-space: normal;
word-break: break-word;
}
.devices-table {
table-layout: fixed;
width: 1285px;
min-width: 1285px;
}
.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;
}
.device-add-card {
position: relative;
}
.device-search-btn {
position: absolute;
right: 12px;
top: 12px;
}
.devices-summary {
text-align: center;
margin-top: -2px;
margin-bottom: 8px;
color: #5f5f5f;
font-weight: 500;
}
</style>
<div class="devices-page">
<div class="d-flex align-items-center justify-content-between mb-2">
<h3 class="mb-0">МФУ / Принтеры</h3>
<a class="btn btn-success" href="/report/devices.xlsx">Экспорт в Excel</a>
</div>
<div class="card mb-3 d-none" id="deviceSearchCard">
<div class="card-body">
<form class="row g-2 align-items-center" id="deviceSearchForm">
<div class="col-md-6">
<input id="deviceSearchInput" class="form-control" placeholder="Модель устройства">
</div>
<div class="col-md-2 d-grid">
<button class="btn btn-primary" type="submit">Найти</button>
</div>
<div class="col-md-2 d-grid">
<button class="btn btn-outline-secondary" type="button" id="deviceSearchClear">Очистить</button>
</div>
</form>
</div>
</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 device-add-card">
<div class="card-body">
<h5 class="card-title mb-2">Добавить устройство</h5>
<button class="btn btn-outline-primary device-search-btn" type="button" id="toggleDeviceSearch" aria-label="Поиск">&#128269;</button>
<form method="post" action="/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="deviceBrandSelect">
<option value="">Бренд...</option>
{% for b in device_brands %}
<option value="{{ b }}">{{ b }}</option>
{% endfor %}
<option value="__custom__">+ Добавить бренд</option>
</select>
<input name="brand_custom" id="deviceBrandCustom" class="form-control mt-2 d-none" placeholder="Новый бренд">
<input type="hidden" name="brand" id="deviceBrandValue">
</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 device_types %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<select name="cabinet_id" class="form-select">
<option value="">Кабинет...</option>
{% for cid, cname in cabinets %}
<option value="{{ cid }}">{{ cname }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-8">
<input name="note" class="form-control" placeholder="Примечание">
</div>
<div class="col-md-2">
<input name="date_in_operation" type="date" class="form-control" placeholder="Дата ввода">
</div>
<div class="col-md-2 d-grid">
<button class="btn btn-success">Добавить</button>
</div>
</form>
</div>
</div>
{% endif %}
<div class="table-responsive">
<table class="table table-striped align-middle devices-table">
<colgroup>
<col style="width:155px">
<col style="width:100px">
<col style="width:200px">
<col style="width:150px">
<col style="width:70px">
<col style="width:260px">
<col style="width:150px">
<col style="width:100px">
<col style="width:100px">
</colgroup>
<tr class="device-row">
<th>Инв. №</th>
<th>Бренд</th>
<th>Модель</th>
<th>Серийный №</th>
<th>Тип</th>
<th>Кабинет</th>
<th>Примечание</th>
<th>Дата ввода</th>
<th>Добавлено</th>
</tr>
{% for id, inv, brand_value, model, sn, dtype, cartridge, cab_id, cab_name, note, date_in_operation, date_added in rows %}
<tr class="js-device-row"
data-id="{{ id }}"
data-inventory-number="{{ inv }}"
data-brand="{{ brand_value or '' }}"
data-model="{{ model or '' }}"
data-serial-number="{{ sn or '' }}"
data-type="{{ dtype }}"
data-cabinet-id="{{ cab_id or '' }}"
data-date-in-operation="{{ date_in_operation.strftime('%Y-%m-%d') if date_in_operation else '' }}"
data-note="{{ note or '' }}">
{% if false %}
<form method="post" action="/devices/edit">
<input type="hidden" name="id" value="{{ id }}">
<td><input name="inventory_number" value="{{ inv }}" class="form-control form-control-sm js-edit-field" disabled></td>
<td>
<select name="brand" class="form-select form-select-sm js-edit-field" disabled>
<option value=""></option>
{% for b in device_brands %}
<option value="{{ b }}" {% if brand_value == b %}selected{% endif %}>{{ b }}</option>
{% endfor %}
</select>
</td>
<td><input name="model" value="{{ model or '' }}" class="form-control form-control-sm js-edit-field" disabled></td>
<td><input name="serial_number" value="{{ sn or '' }}" class="form-control form-control-sm js-edit-field" disabled></td>
<td>
<select name="type" class="form-select form-select-sm js-edit-field" disabled>
{% for value, label in device_types %}
<option value="{{ value }}" {% if dtype == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</td>
<td>
<select name="cabinet_id" class="form-select form-select-sm js-edit-field" disabled>
<option value=""></option>
{% for cid, cname in cabinets %}
<option value="{{ cid }}" {% if cab_id == cid %}selected{% endif %}>{{ cname }}</option>
{% endfor %}
</select>
</td>
<td><input name="note" value="{{ note or '' }}" class="form-control form-control-sm js-edit-field" disabled></td>
<td>
<input name="date_in_operation" type="date" value="{{ date_in_operation.strftime('%Y-%m-%d') if date_in_operation else '' }}" class="form-control form-control-sm js-edit-field" disabled>
</td>
<td>{{ (date_added|string)[:10] if date_added else '' }}</td>
</form>
{% else %}
<td>{{ inv }}</td>
<td>{{ brand_value or '' }}</td>
<td>{{ model or '' }}</td>
<td>{{ sn or '' }}</td>
<td>{{ device_type_labels.get(dtype, dtype) }}</td>
<td>{{ cab_name or '' }}</td>
<td>{{ note or '' }}</td>
<td>{{ date_in_operation or '' }}</td>
<td>{{ (date_added|string)[:10] if date_added else '' }}</td>
{% endif %}
</tr>
{% endfor %}
</table>
</div>
<div class="devices-summary">
Всего: {{ total_devices }} |
Принтеры: {{ printers_count }} |
МФУ: {{ mfp_count }}
</div>
{% if session.get('role') in ('admin','storekeeper') %}
<div class="edit-modal" id="deviceEditModal" 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="/devices/edit" id="deviceEditForm">
<input type="hidden" name="id" id="deviceModalId">
<div class="row g-2">
<div class="col-md-3">
<input name="inventory_number" id="deviceModalInv" class="form-control" placeholder="Инв. №">
</div> <div class="col-md-3">
<select name="brand_select" id="deviceModalBrandSelect" class="form-select">
<option value="">Бренд...</option>
{% for b in device_brands %}
<option value="{{ b }}">{{ b }}</option>
{% endfor %}
<option value="__custom__">+ Добавить бренд</option>
</select>
<input name="brand_custom" id="deviceModalBrandCustom" class="form-control mt-2 d-none" placeholder="Новый бренд">
<input type="hidden" name="brand" id="deviceModalBrand">
</div>
<div class="col-md-3">
<input name="model" id="deviceModalModel" class="form-control" placeholder="Модель">
</div>
<div class="col-md-3">
<input name="serial_number" id="deviceModalSerial" class="form-control" placeholder="Серийный №">
</div>
<div class="col-md-3">
<select name="type" id="deviceModalType" class="form-select">
<option value="">Тип...</option>
{% for value, label in device_types %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<select name="cabinet_id" id="deviceModalCabinet" class="form-select">
<option value="">Кабинет...</option>
{% for cid, cname in cabinets %}
<option value="{{ cid }}">{{ cname }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<input name="date_in_operation" id="deviceModalDateIn" type="date" class="form-control">
</div>
<div class="col-md-12">
<input name="note" id="deviceModalNote" class="form-control" placeholder="Примечание">
</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 %}
<script>
(function() {
const select = document.getElementById('deviceBrandSelect');
const customInput = document.getElementById('deviceBrandCustom');
const valueInput = document.getElementById('deviceBrandValue');
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();
})();
(function() {
const select = document.getElementById('deviceModalBrandSelect');
const customInput = document.getElementById('deviceModalBrandCustom');
const valueInput = document.getElementById('deviceModalBrand');
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();
})();
(function() {
const buttons = document.querySelectorAll('.js-edit-toggle');
if (!buttons.length) return;
buttons.forEach((btn) => {
btn.addEventListener('click', () => {
const row = btn.closest('tr');
if (!row) return;
const fields = row.querySelectorAll('.js-edit-field');
if (!fields.length) return;
fields.forEach((field) => { field.disabled = false; });
if (fields[0] && fields[0].focus) {
fields[0].focus();
}
});
});
})();
(function() {
const rows = document.querySelectorAll('.js-device-row');
if (!rows.length) return;
const modal = document.getElementById('deviceEditModal');
const form = document.getElementById('deviceEditForm');
if (!modal || !form) return;
const modalFields = {
id: document.getElementById('deviceModalId'),
inventory_number: document.getElementById('deviceModalInv'),
brand: document.getElementById('deviceModalBrand'),
brand_select: document.getElementById('deviceModalBrandSelect'),
brand_custom: document.getElementById('deviceModalBrandCustom'),
model: document.getElementById('deviceModalModel'),
serial_number: document.getElementById('deviceModalSerial'),
type: document.getElementById('deviceModalType'),
cabinet_id: document.getElementById('deviceModalCabinet'),
date_in_operation: document.getElementById('deviceModalDateIn'),
note: document.getElementById('deviceModalNote'),
};
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",
"cabinet_id",
"date_in_operation",
"note",
];
let filled = false;
map.forEach((name) => {
if (!modalFields[name]) return;
const dataKey = name.replace(/_/g, '-');
const attrValue = row.getAttribute(`data-${dataKey}`);
if (attrValue !== null) {
modalFields[name].value = attrValue || '';
filled = true;
}
});
if (!filled) {
const formEl = row.querySelector('form');
if (!formEl) return false;
const idInput = formEl.querySelector('input[name="id"]');
if (idInput && modalFields.id) modalFields.id.value = idInput.value;
const mapFields = map.filter((item) => item !== "id");
mapFields.forEach((name) => {
const field = formEl.querySelector(`[name="${name}"]`);
if (field && modalFields[name]) {
modalFields[name].value = field.value || '';
}
});
}
const modalBrandSelect = document.getElementById('deviceModalBrandSelect');
const modalBrandCustom = document.getElementById('deviceModalBrandCustom');
const modalBrandValue = document.getElementById('deviceModalBrand');
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 if (modalBrandSelect) {
modalBrandSelect.value = current ? '__custom__' : '';
if (modalBrandCustom) {
modalBrandCustom.classList.toggle('d-none', !current);
modalBrandCustom.value = current;
}
}
}
return true;
}
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="9">
<div class="row-actions__inner">
<button type="button" class="btn btn-sm btn-outline-primary js-open-device-modal">Редактировать</button>
<form method="post" action="/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-device-modal');
btn.addEventListener('click', () => {
if (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;
if (fillModalFromRow(row)) openModal();
});
});
})();
</script>
<script>
window.addEventListener('DOMContentLoaded', () => {
const toggleBtn = document.getElementById('toggleDeviceSearch');
const card = document.getElementById('deviceSearchCard');
const form = document.getElementById('deviceSearchForm');
const input = document.getElementById('deviceSearchInput');
const clearBtn = document.getElementById('deviceSearchClear');
if (!toggleBtn || !card || !form || !input) return;
const rows = () => document.querySelectorAll('.js-device-row');
const applyFilter = () => {
const q = (input.value || '').trim().toLowerCase();
rows().forEach((row) => {
const model = (row.getAttribute('data-model') || '').toLowerCase();
const match = !q || model.includes(q);
row.classList.toggle('d-none', !match);
});
};
toggleBtn.addEventListener('click', () => {
card.classList.toggle('d-none');
if (!card.classList.contains('d-none')) input.focus();
});
form.addEventListener('submit', (e) => {
e.preventDefault();
applyFilter();
});
if (clearBtn) {
clearBtn.addEventListener('click', () => {
input.value = '';
applyFilter();
});
}
});
</script>
</div>
{% endblock %}