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

3883 lines
174 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 %}bg-home{% endblock %}
{% block content %}
<style>
.login-reveal {
position: fixed;
inset: 0;
z-index: 2000;
pointer-events: none;
background:
radial-gradient(800px 500px at 20% 20%, rgba(99, 102, 241, 0.22), transparent 60%),
radial-gradient(700px 460px at 85% 25%, rgba(14, 165, 233, 0.18), transparent 62%),
linear-gradient(135deg, rgba(10, 14, 20, 0.98), rgba(6, 10, 16, 0.98));
opacity: 0;
animation: revealFadeIn 140ms ease forwards;
}
.login-reveal__scanlines {
position: absolute;
inset: 0;
background-image: repeating-linear-gradient(
to bottom,
rgba(226, 232, 240, 0.06) 0 1px,
rgba(15, 23, 32, 0) 1px 4px
);
opacity: 0;
animation: scanlinesOn 1.1s ease-out forwards;
}
.login-reveal__vignette {
position: absolute;
inset: 0;
background: radial-gradient(circle at 50% 45%, transparent 55%, rgba(6, 10, 16, 0.65) 85%);
opacity: 0;
animation: vignetteIn 1s ease-out forwards;
}
.login-reveal__glint {
position: absolute;
left: -20%;
top: 0;
width: 140%;
height: 100%;
background: linear-gradient(110deg, transparent 0%, rgba(99, 102, 241, 0.12) 45%, rgba(125, 211, 252, 0.5) 50%, rgba(99, 102, 241, 0.12) 55%, transparent 100%);
transform: translateY(10%) translateX(-30%) rotate(-8deg);
animation: glintSweep 0.9s ease-out forwards;
}
.login-reveal__beam {
position: absolute;
left: 50%;
top: 50%;
width: 260px;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(125, 211, 252, 0.8), transparent);
transform: translate(-50%, -50%);
opacity: 0;
animation: beamPulse 1s ease-out forwards;
}
.login-reveal--out {
animation: revealFadeOut 320ms ease forwards;
}
.home-wrap .reveal-item {
opacity: 0;
transform: translateY(14px) scale(0.995);
animation: contentRise 480ms ease forwards;
}
.home-top-action {
position: fixed;
right: 8px;
top: 40px;
z-index: 1100;
display: flex;
justify-content: flex-start;
}
.home-top-action__stack {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.home-note {
position: fixed;
left: 8px;
top: 50%;
transform: translateY(-50%);
background: #fff6a8;
color: #111;
border: 1px solid #000;
padding: 6px 8px;
font-family: 'Chicago Regular', sans-serif;
font-size: 0.9rem;
box-shadow: inset 1px 1px 0 #fffbd8, inset -1px -1px 0 #b7ad62;
max-width: 220px;
}
.home-note--facts {
top: calc(50% + 110px);
max-width: 260px;
background: #efe6ff;
box-shadow: inset 1px 1px 0 #f7f1ff, inset -1px -1px 0 #9c93c4;
}
.home-note__title {
font-weight: 700;
margin-bottom: 4px;
}
.home-notes-layer {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1085;
}
.home-user-note {
position: fixed;
width: 240px;
min-height: 170px;
border: 1px solid #6e9a6e;
background: #CCFFCC;
box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.3);
pointer-events: auto;
overflow: hidden;
}
.home-user-note__head {
display: flex;
justify-content: flex-start;
align-items: center;
height: 24px;
padding: 2px 4px;
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
cursor: move;
user-select: none;
}
.home-user-note__head-actions {
display: flex;
align-items: center;
width: 100%;
gap: 4px;
}
.home-user-note__add {
width: 20px;
height: 20px;
border: 0;
background: transparent;
color: #112211;
font-size: 22px;
line-height: 14px;
cursor: pointer;
padding: 0;
}
.home-user-note.is-pinned .home-user-note__head {
cursor: default;
}
.home-user-note__add:disabled {
opacity: 0.5;
cursor: default;
}
.home-user-note__pin {
width: 20px;
height: 20px;
border: 0;
background: transparent;
font-size: 13px;
line-height: 1;
cursor: pointer;
margin-left: auto;
}
.home-user-note__pin.is-active {
filter: saturate(1.3);
}
.home-user-note__text {
width: 100%;
min-height: 126px;
border: 0;
background: transparent;
resize: both;
padding: 8px;
font-family: 'Chicago Regular', sans-serif;
font-size: 0.85rem;
color: #102010;
}
.home-user-note__text:focus {
outline: none;
}
.home-user-note__delete {
position: absolute;
left: 6px;
bottom: 4px;
width: 24px;
height: 24px;
border: 0;
background: transparent;
color: #0b190b;
font-size: 34px;
line-height: 20px;
cursor: pointer;
padding: 0;
}
.home-user-note__delete[disabled] {
opacity: 0.35;
cursor: default;
}
.home-barcode-btn {
border: 0;
background: transparent;
padding: 0;
line-height: 0;
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.home-barcode-btn img {
width: 36px;
height: auto;
display: block;
}
#homePcComponentsBtn img {
width: 54px;
}
.home-barcode-btn__label {
background: #ffffff;
color: #000;
padding: 2px 6px;
font-family: 'Chicago Regular', sans-serif;
font-size: 0.7rem;
line-height: 1.1;
border: 0;
}
.home-barcode-btn:active img,
.home-barcode-btn.is-active img {
filter: brightness(0.7);
}
.home-barcode-btn:active .home-barcode-btn__label,
.home-barcode-btn.is-active .home-barcode-btn__label {
background: #000000;
color: #ffffff;
}
.home-modal {
position: fixed;
top: 44px;
left: 12px;
width: 807px;
max-width: calc(100vw - 24px);
background: #dcdcdc;
border: 1px solid #000;
box-shadow: 1px 1px 0 #000, 2px 2px 0 #777;
z-index: 1200;
}
.home-modal.hidden {
display: none;
}
.home-modal__titlebar {
display: flex;
align-items: center;
gap: 4px;
padding: 2px 4px;
background: #c0c0c0;
border-bottom: 1px solid #000;
font-family: 'Chicago Regular', sans-serif;
font-size: 0.85rem;
line-height: 1;
cursor: move;
user-select: none;
}
.home-modal__title {
flex: 1;
text-align: center;
font-weight: 600;
letter-spacing: 0.2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.home-modal__spacer {
flex: 1;
}
.home-modal__btn {
width: 12px;
height: 12px;
border: 1px solid #000;
background: #e6e6e6;
display: inline-block;
}
.home-modal__close {
cursor: pointer;
position: relative;
}
.home-modal__close::before,
.home-modal__close::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: 9px;
height: 2px;
background: #000;
transform-origin: center;
}
.home-modal__close:active {
background: #bcbcbc;
box-shadow: inset 1px 1px 0 #9a9a9a, inset -1px -1px 0 #efefef;
}
.home-modal__close:active::before,
.home-modal__close:active::after {
background: #111;
}
.home-modal__close::before {
transform: translate(-50%, -50%) rotate(45deg);
}
.home-modal__close::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
.home-modal__content {
background: #efefef;
border-top: 1px solid #7f7f7f;
padding: 8px;
height: 486px;
overflow: auto;
font-family: 'Chicago Regular', sans-serif;
font-size: 0.82rem;
}
.home-modal__row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 6px;
}
.home-modal__icon {
display: grid;
justify-items: center;
gap: 4px;
color: #111;
}
.home-modal__icon-box {
width: 34px;
height: 26px;
background: #d8d0f0;
border: 1px solid #000;
box-shadow: inset 1px 1px 0 #f7f3ff, inset -1px -1px 0 #9a94b3;
}
.home-modal__footer {
border-top: 1px solid #7f7f7f;
padding: 4px 6px;
background: #dcdcdc;
font-family: 'Chicago Regular', sans-serif;
font-size: 0.78rem;
text-align: center;
}
.home-modal__embed {
width: 100%;
height: 100%;
min-height: 420px;
border: 1px solid #7f7f7f;
background: #fff;
}
.home-modal__section-title {
font-family: 'Chicago Regular', sans-serif;
font-size: 0.88rem;
margin-bottom: 6px;
}
.home-modal__form {
display: grid;
gap: 8px;
}
.home-modal__field label {
display: block;
font-size: 0.78rem;
margin-bottom: 4px;
}
.home-modal__input,
.home-modal__select {
width: 100%;
border: 1px solid #6f6f6f;
padding: 4px 6px;
background: #fff;
font-family: 'Chicago Regular', sans-serif;
font-size: 0.85rem;
}
.home-modal__row-2 {
display: grid;
grid-template-columns: 1fr 120px;
gap: 8px;
}
.home-modal__actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.home-modal__btn-primary {
border: 1px solid #000;
background: #d9d9d9;
padding: 4px 10px;
font-family: 'Chicago Regular', sans-serif;
font-size: 0.85rem;
}
.home-modal__status {
font-size: 0.78rem;
}
#homePcComponentsModal .home-modal__content {
display: grid;
gap: 10px;
align-content: start;
padding-top: 4px;
}
#homePcComponentsModal .home-modal__input,
#homePcComponentsModal .home-modal__select {
height: 26px;
padding: 4px 6px;
box-sizing: border-box;
}
#homePcComponentsModal .home-modal__section-title {
margin: 0 0 4px;
}
#homePcComponentsModal .home-modal__status {
margin: 0;
}
.pc-components-assign-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) 120px auto;
gap: 10px;
align-items: end;
}
.pc-components-add-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)) 120px auto;
gap: 10px;
align-items: end;
}
.pc-components-assign-grid .home-modal__field,
.pc-components-add-grid .home-modal__field {
min-width: 0;
}
.pc-components-assign-grid .home-modal__select,
.pc-components-add-grid .home-modal__select {
width: 100%;
min-width: 0;
justify-self: stretch;
}
#pcComponentsAddForm .pc-components-add-actions {
display: flex;
justify-content: flex-end;
align-self: end;
margin-top: 0;
}
@media (max-width: 980px) {
.pc-components-assign-grid {
grid-template-columns: 1fr 120px;
}
.pc-components-assign-grid .pc-components-action {
grid-column: 1 / -1;
justify-self: end;
}
.pc-components-add-grid {
grid-template-columns: 1fr 1fr;
}
#pcComponentsAddForm .pc-components-add-actions {
margin-top: 10px;
}
}
.home-modal__specs-list {
margin: 4px 0 0;
padding-left: 18px;
font-size: 0.78rem;
list-style: disc;
list-style-position: outside;
}
.home-modal__specs-list .home-modal__status {
display: list-item;
}
.home-modal__list {
margin: 4px 0 0;
padding-left: 16px;
font-size: 0.78rem;
}
@keyframes revealFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes revealFadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes scanlinesOn {
0% { opacity: 0; }
35% { opacity: 0.7; }
100% { opacity: 0.15; }
}
@keyframes vignetteIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes glintSweep {
0% { transform: translateY(18%) translateX(-35%) rotate(-8deg); opacity: 0; }
40% { opacity: 1; }
100% { transform: translateY(-8%) translateX(10%) rotate(-8deg); opacity: 0; }
}
@keyframes beamPulse {
0% { opacity: 0; transform: translate(-50%, -50%) scaleX(0.4); }
45% { opacity: 1; transform: translate(-50%, -50%) scaleX(1); }
100% { opacity: 0; transform: translate(-50%, -50%) scaleX(1.2); }
}
@keyframes contentRise {
from { opacity: 0; transform: translateY(18px) scale(0.99); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
.login-reveal { display: none; }
.home-wrap .reveal-item { animation: none; opacity: 1; transform: none; }
}
.users-modal-grid {
display: grid;
grid-template-columns: minmax(220px, 260px) 1fr;
gap: 1rem;
align-items: start;
}
.users-role-note {
border: 1px solid #000;
background: #f4f4f4;
padding: 0.6rem;
font-size: 0.85rem;
line-height: 1.35;
}
.users-list {
list-style: none;
padding: 0;
margin: 0 0 0.75rem 0;
}
.users-list li {
border: 1px solid #c7c7c7;
background: #fff;
padding: 0.5rem 0.6rem;
margin-bottom: 0.5rem;
}
.users-add-form {
border: 1px solid #c7c7c7;
padding: 0.75rem;
background: #fff;
}
</style>
<div class="home-top-action">
<div class="home-top-action__stack">
{% set role = session.get('role') %}
{% if role != 'viewer' %}
<button class="home-barcode-btn" id="homeBarcodeBtn" type="button" aria-label="Штрихкод">
<img src="{{ url_for('static', filename='Desktop_barcod.png') }}" alt="Штрихкод">
<span class="home-barcode-btn__label">Выдача картриджа</span>
</button>
<button class="home-barcode-btn" id="homePhotoBtn" type="button" aria-label="Расходные материалы">
<img src="{{ url_for('static', filename='Desktop_photoconductor.png') }}" alt="Фотобарабан">
<span class="home-barcode-btn__label">Расходные материалы</span>
</button>
{% endif %}
<button class="home-barcode-btn" id="homeSearchInvBtn" type="button" aria-label="Поиск оборудования">
<img src="{{ url_for('static', filename='Desktop_search.png') }}" alt="Поиск по инвентарному номеру">
<span class="home-barcode-btn__label">Поиск оборудования</span>
</button>
{% if role == 'admin' %}
<button class="home-barcode-btn" id="homeUsersBtn" type="button" aria-label="Пользователи">
<img src="{{ url_for('static', filename='Desktop_users.png') }}" alt="Пользователи">
<span class="home-barcode-btn__label">Пользователи</span>
</button>
{% endif %}
{% if role != 'viewer' %}
<button class="home-barcode-btn" id="homeAddDeviceBtn" type="button" aria-label="Добавить устройство">
<img src="{{ url_for('static', filename='Desktop_add.png') }}" alt="Добавить устройство">
<span class="home-barcode-btn__label">Добавить устройство</span>
</button>
{% endif %}
<button class="home-barcode-btn" id="homePcComponentsBtn" type="button" aria-label="Компьютерные комплектующие">
<img src="{{ url_for('static', filename='Desktop_PCcomponents.png') }}" alt="Компьютерные комплектующие">
<span class="home-barcode-btn__label">Компьютерные<br>комплектующие</span>
</button>
</div>
</div>
<div class="home-note">Welcome to IT-Inventory</div>
<div class="home-note home-note--facts">
<div class="home-note__title">Интересный факт</div>
<div id="homeFactText">Загрузка факта...</div>
</div>
<div class="home-notes-layer" id="homeNotesLayer" aria-live="polite"></div>
<div class="home-modal hidden" id="homeDesktopModal" role="dialog" aria-label="Выдача картриджей">
<div class="home-modal__titlebar">
<span class="home-modal__btn" aria-hidden="true"></span>
<div class="home-modal__title">Выдача картриджей</div>
<span class="home-modal__btn home-modal__close" id="homeDesktopClose" aria-label="Close"></span>
</div>
<div class="home-modal__content">
<div class="home-modal__section-title">Выдача по штрихкоду</div>
<form method="post" action="/issue" class="home-modal__form" id="modalIssueForm">
<div class="home-modal__field">
<label for="modalBarcode">Сканируйте штрихкод или введите вручную</label>
<input id="modalBarcode" name="barcode" class="home-modal__input" autocomplete="off">
</div>
<div class="home-modal__status" id="modalCartridgeName"></div>
<div class="home-modal__status" id="modalCartridgeStock"></div>
<div class="home-modal__field d-none" id="modalAddNotice">Картридж не найден. Добавить?</div>
<div class="home-modal__row-2 d-none" id="modalAddFields">
<div class="home-modal__field">
<label for="modalAddModel">Модель</label>
<input id="modalAddModel" name="model" class="home-modal__input">
</div>
<div class="home-modal__field">
<label for="modalAddQty">Количество</label>
<input id="modalAddQty" name="quantity" class="home-modal__input" value="1">
</div>
</div>
<div class="home-modal__actions d-none" id="modalAddActions">
<button class="home-modal__btn-primary" formaction="/add">Добавить</button>
</div>
<div class="home-modal__row-2">
<div class="home-modal__field">
<label for="modalCabinet">Кабинет</label>
<select id="modalCabinet" name="cabinet" class="home-modal__select">
<option value="">Выберите кабинет...</option>
{% for c in cabinets %}
<option value="{{ c }}">{{ c }}</option>
{% endfor %}
</select>
</div>
<div class="home-modal__field">
<label for="modalQuantity">Количество</label>
<input id="modalQuantity" name="quantity" class="home-modal__input" value="1">
</div>
</div>
<div class="home-modal__actions">
<button class="home-modal__btn-primary" id="modalIssueSubmit">Выдать</button>
</div>
</form>
</div>
<div class="home-modal__footer"></div>
</div>
<div class="home-modal hidden" id="homePhotoModal" role="dialog" aria-label="Расходные материалы">
<div class="home-modal__titlebar">
<span class="home-modal__btn" aria-hidden="true"></span>
<div class="home-modal__title">Расходные материалы</div>
<span class="home-modal__btn home-modal__close" id="homePhotoClose" aria-label="Close"></span>
</div>
<div class="home-modal__content">
<div class="home-modal__section-title">Расходный материал</div>
<form method="post" action="/consumables/issue" class="home-modal__form" id="photoIssueForm">
<div class="home-modal__field">
<label for="photoBarcode">Сканируйте штрихкод</label>
<input id="photoBarcode" name="barcode" class="home-modal__input" autocomplete="off">
</div>
<div class="home-modal__status d-none" id="photoModelText"></div>
<div class="home-modal__status d-none" id="photoStockText"></div>
<div class="home-modal__field d-none" id="photoDeviceWrap">
<label for="photoDevice">Принтер/МФУ</label>
<select id="photoDevice" name="device_id" class="home-modal__select">
<option value="">Выберите устройство...</option>
</select>
</div>
<div class="home-modal__actions">
<button class="home-modal__btn-primary">Выдать</button>
</div>
</form>
<div class="home-modal__section-title d-none" id="photoAddTitle">Добавить расходный материал</div>
<form method="post" action="/consumables/add" class="home-modal__form d-none" id="photoAddForm">
<div class="home-modal__field">
<label for="photoAddBarcode">Штрихкод</label>
<input id="photoAddBarcode" name="barcode" class="home-modal__input" readonly>
</div>
<div class="home-modal__field">
<label for="photoAddModel">Модель</label>
<input id="photoAddModel" name="model" class="home-modal__input">
</div>
<div class="home-modal__field">
<label for="photoAddTypeSelect">Тип</label>
<select name="type" id="photoAddTypeSelect" class="home-modal__select">
<option value="">Тип расходника...</option>
{% for t in consumable_types %}
<option value="{{ t }}">{{ t }}</option>
{% endfor %}
<option value="__custom__">Свой тип...</option>
</select>
<input name="type_custom" id="photoAddTypeCustom" class="home-modal__input d-none" placeholder="Свой тип">
</div>
<div class="home-modal__field">
<label for="photoAddQty">Количество</label>
<input id="photoAddQty" name="quantity" class="home-modal__input" value="1">
</div>
<div class="home-modal__actions">
<button class="home-modal__btn-primary" {% if session.get('role') not in ('admin','storekeeper') %}disabled{% endif %}>Добавить</button>
</div>
</form>
</div>
<div class="home-modal__footer"></div>
</div>
<div class="home-modal hidden" id="homeSearchInvModal" role="dialog" aria-label="Поиск оборудования">
<div class="home-modal__titlebar">
<span class="home-modal__btn" aria-hidden="true"></span>
<div class="home-modal__title">Поиск оборудования</div>
<span class="home-modal__btn home-modal__close" id="homeSearchInvClose" aria-label="Close"></span>
</div>
<div class="home-modal__content">
<div class="home-modal__section-title">Поиск оборудования</div>
<div class="home-modal__form">
<div class="home-modal__field">
<label for="searchCabinetViewSelect">Выберите кабинет</label>
<select id="searchCabinetViewSelect" class="home-modal__select">
<option value="">Выберите кабинет...</option>
{% for cid, cname in cabinets_full %}
<option value="{{ cid }}">{{ cname }}</option>
{% endfor %}
</select>
</div>
<div class="home-modal__status d-none" id="cabinetInfoTitle"></div>
<div class="home-modal__field d-none" id="cabinetDevicesWrap">
<label>Устройства (МФУ/Принтеры)</label>
<ul class="home-modal__list" id="cabinetDevicesList"></ul>
</div>
<div class="home-modal__field d-none" id="cabinetComputersWrap">
<label>Компьютеры/Ноутбуки</label>
<ul class="home-modal__list" id="cabinetComputersList"></ul>
</div>
<div class="home-modal__field d-none" id="cabinetProjectorsWrap">
<label>Презентационное оборудование</label>
<ul class="home-modal__list" id="cabinetProjectorsList"></ul>
</div>
<div class="home-modal__field d-none" id="cabinetDocsWrap">
<label>Документ-камеры</label>
<ul class="home-modal__list" id="cabinetDocsList"></ul>
</div>
<div class="home-modal__status d-none" id="cabinetEmptyNote">В этом кабинете нет оборудования.</div>
<div class="home-modal__section-title mt-2">Поиск по инвентарному номеру</div>
<div class="home-modal__field">
<label for="searchInvInput">Введите инвентарный номер</label>
<input id="searchInvInput" class="home-modal__input" autocomplete="off">
</div>
<div class="home-modal__status d-none" id="searchInfoInv"></div>
<div class="home-modal__status d-none" id="searchInfoType"></div>
<div class="home-modal__status d-none" id="searchInfoBrand"></div>
<div class="home-modal__status d-none" id="searchInfoSerial"></div>
<div class="home-modal__status d-none" id="searchInfoDate"></div>
<div class="home-modal__status d-none" id="searchInfoCabinet"></div>
<div class="home-modal__field d-none" id="searchConsumablesWrap">
<label>Привязанные материалы</label>
<ul class="home-modal__list" id="searchConsumablesList"></ul>
</div>
<div class="home-modal__field d-none" id="searchSpecsWrap">
<label>Характеристики</label>
<ul class="home-modal__specs-list">
<li class="home-modal__status" id="searchSpecCpu"></li>
<li class="home-modal__status" id="searchSpecGpu"></li>
<li class="home-modal__status" id="searchSpecRam"></li>
<li class="home-modal__status" id="searchSpecStorage"></li>
<li class="home-modal__status" id="searchSpecOs"></li>
<li class="home-modal__status d-none" id="searchSpecMotherboard"></li>
</ul>
</div>
<div class="home-modal__field d-none" id="searchKitWrap">
<label>Состав комплекта</label>
<div class="home-modal__status" id="searchKitProjectorTitle"></div>
<ul class="home-modal__specs-list" id="searchKitProjectorList"></ul>
<div class="home-modal__status" id="searchKitBoardTitle"></div>
<ul class="home-modal__specs-list" id="searchKitBoardList"></ul>
<div class="home-modal__status" id="searchKitComputerTitle"></div>
<ul class="home-modal__specs-list" id="searchKitComputerList"></ul>
</div>
</div>
</div>
<div class="home-modal__footer"></div>
</div>
<div class="home-modal hidden" id="homePrintersModal" role="dialog" aria-label="МФУ/Принтеры">
<div class="home-modal__titlebar">
<span class="home-modal__btn" aria-hidden="true"></span>
<div class="home-modal__title">МФУ/Принтеры</div>
<span class="home-modal__btn home-modal__close" id="homePrintersClose" aria-label="Close"></span>
</div>
<div class="home-modal__content">
<div class="home-modal__section-title">МФУ/Принтеры</div>
<div class="home-modal__form">
<div class="home-modal__field">
<label for="printersInv">Введите инвентарный номер</label>
<input id="printersInv" class="home-modal__input" autocomplete="off">
</div>
<div class="home-modal__status d-none" id="printersInfoInv"></div>
<div class="home-modal__status d-none" id="printersInfoBrand"></div>
<div class="home-modal__status d-none" id="printersInfoSerial"></div>
<div class="home-modal__status d-none" id="printersInfoDate"></div>
<div class="home-modal__status d-none" id="printersInfoCabinet"></div>
<form method="post" class="home-modal__form d-none" id="printersCabinetForm">
<input type="hidden" name="id" id="printersId">
<div class="home-modal__field">
<label for="printersCabinetSelect">Выбрать кабинет</label>
<select id="printersCabinetSelect" name="cabinet_id" class="home-modal__select">
<option value="">Выберите кабинет...</option>
{% for cid, cname in cabinets_full %}
<option value="{{ cid }}">{{ cname }}</option>
{% endfor %}
</select>
</div>
<div class="home-modal__actions">
<button class="home-modal__btn-primary">Сменить расположение</button>
</div>
</form>
<div class="home-modal__field d-none" id="printersConsumablesWrap">
<label>Привязанные материалы</label>
<ul class="home-modal__list" id="printersConsumablesList"></ul>
</div>
</div>
</div>
<div class="home-modal__footer"></div>
</div>
<div class="home-modal hidden" id="homeComputersModal" role="dialog" aria-label="Компьютеры/Ноутбуки">
<div class="home-modal__titlebar">
<span class="home-modal__btn" aria-hidden="true"></span>
<div class="home-modal__title">Компьютеры/Ноутбуки</div>
<span class="home-modal__btn home-modal__close" id="homeComputersClose" aria-label="Close"></span>
</div>
<div class="home-modal__content">
<div class="home-modal__section-title">Компьютеры/Ноутбуки</div>
<div class="home-modal__form">
<div class="home-modal__field">
<label for="computersInv">Введите инвентарный номер</label>
<input id="computersInv" class="home-modal__input" autocomplete="off">
</div>
<div class="home-modal__status d-none" id="computersInfoInv"></div>
<div class="home-modal__status d-none" id="computersInfoBrand"></div>
<div class="home-modal__status d-none" id="computersInfoSerial"></div>
<div class="home-modal__status d-none" id="computersInfoDate"></div>
<div class="home-modal__status d-none" id="computersInfoCabinet"></div>
<form method="post" class="home-modal__form d-none" id="computersCabinetForm">
<input type="hidden" name="id" id="computersId">
<div class="home-modal__field">
<label for="computersCabinetSelect">Выбрать кабинет</label>
<select id="computersCabinetSelect" name="cabinet_id" class="home-modal__select">
<option value="">Выберите кабинет...</option>
{% for cid, cname in cabinets_full %}
<option value="{{ cid }}">{{ cname }}</option>
{% endfor %}
</select>
</div>
<div class="home-modal__actions">
<button class="home-modal__btn-primary">Сменить расположение</button>
</div>
</form>
<div class="home-modal__field d-none" id="computersSpecsWrap">
<label>Характеристики</label>
<ul class="home-modal__specs-list">
<li class="home-modal__status" id="computersSpecCpu"></li>
<li class="home-modal__status" id="computersSpecGpu"></li>
<li class="home-modal__status" id="computersSpecRam"></li>
<li class="home-modal__status" id="computersSpecStorage"></li>
<li class="home-modal__status" id="computersSpecOs"></li>
<li class="home-modal__status d-none" id="computersSpecMotherboard"></li>
</ul>
</div>
</div>
</div>
<div class="home-modal__footer"></div>
</div>
<div class="home-modal hidden" id="homeProjectorsModal" role="dialog" aria-label="Проекторы">
<div class="home-modal__titlebar">
<span class="home-modal__btn" aria-hidden="true"></span>
<div class="home-modal__title">Проекторы</div>
<span class="home-modal__btn home-modal__close" id="homeProjectorsClose" aria-label="Close"></span>
</div>
<div class="home-modal__content">
<div class="home-modal__section-title">Проекторы</div>
<div class="home-modal__form">
<div class="home-modal__field">
<label for="projectorsInv">Введите инвентарный номер</label>
<input id="projectorsInv" class="home-modal__input" autocomplete="off">
</div>
<div class="home-modal__status d-none" id="projectorsInfoInv"></div>
<div class="home-modal__status d-none" id="projectorsInfoType"></div>
<div class="home-modal__status d-none" id="projectorsInfoBrand"></div>
<div class="home-modal__status d-none" id="projectorsInfoSerial"></div>
<div class="home-modal__status d-none" id="projectorsInfoDate"></div>
<div class="home-modal__status d-none" id="projectorsInfoCabinet"></div>
<form method="post" class="home-modal__form d-none" id="projectorsCabinetForm">
<input type="hidden" name="id" id="projectorsId">
<div class="home-modal__field">
<label for="projectorsCabinetSelect">Выбрать кабинет</label>
<select id="projectorsCabinetSelect" name="cabinet_id" class="home-modal__select">
<option value="">Выберите кабинет...</option>
{% for cid, cname in cabinets_full %}
<option value="{{ cid }}">{{ cname }}</option>
{% endfor %}
</select>
</div>
<div class="home-modal__actions">
<button class="home-modal__btn-primary">Сменить расположение</button>
</div>
</form>
<div class="home-modal__field d-none" id="projectorsKitWrap">
<label>Состав комплекта</label>
<div class="home-modal__status" id="kitProjectorTitle"></div>
<ul class="home-modal__specs-list" id="kitProjectorList"></ul>
<div class="home-modal__status" id="kitBoardTitle"></div>
<ul class="home-modal__specs-list" id="kitBoardList"></ul>
<div class="home-modal__status" id="kitComputerTitle"></div>
<ul class="home-modal__specs-list" id="kitComputerList"></ul>
</div>
</div>
</div>
<div class="home-modal__footer"></div>
</div>
<div class="home-modal hidden" id="homeDocCameraModal" role="dialog" aria-label="Документ-камеры">
<div class="home-modal__titlebar">
<span class="home-modal__btn" aria-hidden="true"></span>
<div class="home-modal__title">Документ-камеры</div>
<span class="home-modal__btn home-modal__close" id="homeDocCameraClose" aria-label="Close"></span>
</div>
<div class="home-modal__content">
<div class="home-modal__section-title">Документ-камеры</div>
<div class="home-modal__form">
<div class="home-modal__field">
<label for="docCameraInv">Введите инвентарный номер</label>
<input id="docCameraInv" class="home-modal__input" autocomplete="off">
</div>
<div class="home-modal__status d-none" id="docCameraInfoInv"></div>
<div class="home-modal__status d-none" id="docCameraInfoBrand"></div>
<div class="home-modal__status d-none" id="docCameraInfoSerial"></div>
<div class="home-modal__status d-none" id="docCameraInfoDate"></div>
<div class="home-modal__status d-none" id="docCameraInfoCabinet"></div>
<form method="post" class="home-modal__form d-none" id="docCameraCabinetForm">
<input type="hidden" name="id" id="docCameraId">
<div class="home-modal__field">
<label for="docCameraCabinetSelect">Выбрать кабинет</label>
<select id="docCameraCabinetSelect" name="cabinet_id" class="home-modal__select">
<option value="">Выберите кабинет...</option>
{% for cid, cname in cabinets_full %}
<option value="{{ cid }}">{{ cname }}</option>
{% endfor %}
</select>
</div>
<div class="home-modal__actions">
<button class="home-modal__btn-primary">Сменить расположение</button>
</div>
</form>
</div>
</div>
<div class="home-modal__footer"></div>
</div>
<div class="home-modal hidden" id="homeAddDeviceModal" role="dialog" aria-label="Добавить устройство">
<div class="home-modal__titlebar">
<span class="home-modal__btn" aria-hidden="true"></span>
<div class="home-modal__title">Добавить устройство</div>
<span class="home-modal__btn home-modal__close" id="homeAddDeviceClose" aria-label="Close"></span>
</div>
<div class="home-modal__content">
<div class="home-modal__section-title">Добавить устройство</div>
<div class="home-modal__stack">
<div class="home-modal__field">
<label for="addDeviceType">Выберите тип</label>
<select id="addDeviceType" class="home-modal__select">
<option value="">Выберите тип...</option>
<option value="device">МФУ/Принтер</option>
<option value="computer">Персональный компьютер/Ноутбук</option>
<option value="projector">Проектор</option>
<option value="document_camera">Документ-камера</option>
</select>
</div>
<form method="post" action="/devices/add" class="home-modal__form d-none" data-add-type="device">
<div class="home-modal__field">
<label>Введите инвентарный номер</label>
<input name="inventory_number" class="home-modal__input">
</div>
<div class="home-modal__row-3">
<div class="home-modal__field">
<label>Бренд</label>
<select name="brand_select" class="home-modal__select" id="homeDeviceBrandSelect">
<option value="">Бренд...</option>
{% for b in device_brands %}
<option value="{{ b }}">{{ b }}</option>
{% endfor %}
<option value="__custom__">+ Добавить бренд</option>
</select>
<input name="brand_custom" id="homeDeviceBrandCustom" class="home-modal__input mt-2 d-none" placeholder="Новый бренд">
<input type="hidden" name="brand" id="homeDeviceBrandValue">
</div>
<div class="home-modal__field">
<label>Модель</label>
<input name="model" class="home-modal__input">
</div>
<div class="home-modal__field">
<label>Серийный №</label>
<input name="serial_number" class="home-modal__input">
</div>
</div>
<div class="home-modal__row-3-wide">
<div class="home-modal__field">
<label>Тип</label>
<select name="type" class="home-modal__select">
<option value="">Тип...</option>
<option value="mfp">МФУ</option>
<option value="printer">Принтер</option>
</select>
</div>
<div class="home-modal__field">
<label>Расположение</label>
<select name="cabinet_id" class="home-modal__select">
<option value="">Кабинет...</option>
{% for cid, cname in cabinets_full %}
<option value="{{ cid }}">{{ cname }}</option>
{% endfor %}
</select>
</div>
<div class="home-modal__field">
<label>Дата добавления</label>
<input name="date_in_operation" type="date" class="home-modal__input">
</div>
</div>
<div class="home-modal__row-3-wide">
<div class="home-modal__field" style="grid-column: 1 / span 2;">
<label>Примечание</label>
<input name="note" class="home-modal__input">
</div>
<div class="home-modal__actions" style="align-self: end;">
<button class="home-modal__btn-primary">Добавить</button>
</div>
</div>
</form>
<form method="post" action="/computers/add" class="home-modal__form d-none" data-add-type="computer">
<div class="home-modal__field">
<label>Введите инвентарный номер</label>
<input name="inventory_number" class="home-modal__input">
</div>
<div class="home-modal__row-3">
<div class="home-modal__field">
<label>Бренд</label>
<select name="brand_select" class="home-modal__select" id="homeComputerBrandSelect">
<option value="">Бренд...</option>
{% for b in computer_brands %}
<option value="{{ b }}">{{ b }}</option>
{% endfor %}
<option value="__custom__">+ Добавить бренд</option>
</select>
<input name="brand_custom" id="homeComputerBrandCustom" class="home-modal__input mt-2 d-none" placeholder="Новый бренд">
<input type="hidden" name="brand" id="homeComputerBrandValue">
</div>
<div class="home-modal__field">
<label>Модель</label>
<input name="model" class="home-modal__input">
</div>
<div class="home-modal__field">
<label>Серийный №</label>
<input name="serial_number" class="home-modal__input">
</div>
</div>
<div class="home-modal__row-3-wide">
<div class="home-modal__field">
<label>Тип</label>
<select name="type" class="home-modal__select">
<option value="">Тип...</option>
<option value="pc">Персональный компьютер</option>
<option value="laptop">Ноутбук</option>
</select>
</div>
<div class="home-modal__field">
<label>Расположение</label>
<select name="cabinet_id" class="home-modal__select">
<option value="">Кабинет...</option>
{% for cid, cname in cabinets_full %}
<option value="{{ cid }}">{{ cname }}</option>
{% endfor %}
</select>
</div>
<div class="home-modal__field">
<label>Дата добавления</label>
<input name="date_in_operation" type="date" class="home-modal__input">
</div>
</div>
<div class="home-modal__row-3-wide">
<div class="home-modal__field" style="grid-column: 1 / span 2;">
<label>Примечание</label>
<input name="note" class="home-modal__input">
</div>
<div class="home-modal__actions" style="align-self: end;">
<button class="home-modal__btn-primary">Добавить</button>
</div>
</div>
<div class="home-modal__actions">
<button class="home-modal__btn-primary" type="button" id="homeComputerSpecsToggle">Характеристики</button>
</div>
<div class="home-modal__form d-none" id="homeComputerSpecs">
<div class="home-modal__row-3">
<div class="home-modal__field">
<label>Бренд CPU</label>
<select name="cpu_brand" class="home-modal__select">
<option value="">Бренд CPU...</option>
<option value="Intel">Intel</option>
<option value="AMD">AMD</option>
</select>
</div>
<div class="home-modal__field">
<label>Модель CPU</label>
<input name="cpu_model" class="home-modal__input">
</div>
<div class="home-modal__field">
<label>Модель GPU</label>
<input name="gpu_model" class="home-modal__input">
</div>
</div>
<div class="home-modal__row-3">
<div class="home-modal__field">
<label>Объём оперативной памяти</label>
<input name="memory_size" class="home-modal__input">
</div>
<div class="home-modal__field">
<label>Тип памяти</label>
<select name="memory_type" class="home-modal__select">
<option value="">Тип памяти...</option>
<option value="DDR">DDR</option>
<option value="DDR-2">DDR-2</option>
<option value="DDR-3">DDR-3</option>
<option value="DDR-4">DDR-4</option>
<option value="DDR-5">DDR-5</option>
</select>
</div>
<div class="home-modal__field">
<label>Объём накопителя</label>
<input name="storage_size" class="home-modal__input">
</div>
</div>
<div class="home-modal__row-3-wide">
<div class="home-modal__field">
<label>Операционная система</label>
<select name="os" class="home-modal__select">
<option value="">ОС...</option>
<option value="Windows 7 (x86)">Windows 7 (x86)</option>
<option value="Windows 7 (x64)">Windows 7 (x64)</option>
<option value="Windows 8 (x86)">Windows 8 (x86)</option>
<option value="Windows 8 (x64)">Windows 8 (x64)</option>
<option value="Windows 8.1 (x86)">Windows 8.1 (x86)</option>
<option value="Windows 8.1 (x64)">Windows 8.1 (x64)</option>
<option value="Windows 10 (x86)">Windows 10 (x86)</option>
<option value="Windows 10 (x64)">Windows 10 (x64)</option>
<option value="Windows 11">Windows 11</option>
</select>
</div>
<div class="home-modal__field" style="grid-column: 2 / span 1;">
<label>Примечание</label>
<input name="note" class="home-modal__input">
</div>
<div class="home-modal__actions" style="align-self: end;">
<button class="home-modal__btn-primary">Добавить</button>
</div>
</div>
</div>
</form>
<form method="post" action="/projectors/add" class="home-modal__form d-none" data-add-type="projector">
<div class="home-modal__field">
<label>Тип</label>
<select name="kit_type" class="home-modal__select" id="homeProjectorKitType">
<option value="">Тип...</option>
<option value="kit">Комплект</option>
<option value="board">Доска</option>
<option value="projector">Проектор</option>
<option value="display">Интерактивный экран</option>
<option value="tv">Телевизор</option>
</select>
</div>
<div class="home-modal__row-2" data-kit-types="kit,projector,board,display,tv">
<div class="home-modal__field">
<label>Введите инвентарный номер</label>
<input name="inventory_number" class="home-modal__input">
</div>
</div>
<div class="home-modal__row-2" data-kit-types="kit" id="homeProjectorBrandRowKit"></div>
<div class="home-modal__row-3" data-kit-types="projector,board,display,tv" id="homeProjectorBrandRowMain">
<div class="home-modal__field" id="homeProjectorBrandField">
<label>Бренд</label>
<select name="brand_select" class="home-modal__select" id="homeProjectorBrandSelect">
<option value="">Бренд...</option>
{% for b in projector_brands %}
<option value="{{ b }}">{{ b }}</option>
{% endfor %}
<option value="__custom__">+ Добавить бренд</option>
</select>
<input name="brand_custom" id="homeProjectorBrandCustom" class="home-modal__input mt-2 d-none" placeholder="Новый бренд">
<input type="hidden" name="brand" id="homeProjectorBrandValue">
</div>
<div class="home-modal__field">
<label id="homeProjectorModelLabel">Модель проектора</label>
<input name="projector_model" class="home-modal__input">
</div>
<div class="home-modal__field">
<label id="homeProjectorSerialLabel">Серийный № проектора</label>
<input name="projector_serial" class="home-modal__input">
</div>
</div>
<div class="home-modal__row-3" data-kit-types="kit">
<div class="home-modal__field">
<label>Инв. № проектора</label>
<input name="projector_inventory_number" class="home-modal__input">
</div>
<div class="home-modal__field">
<label>Модель проектора</label>
<input name="projector_model" class="home-modal__input">
</div>
<div class="home-modal__field">
<label>Серийный № проектора</label>
<input name="projector_serial" class="home-modal__input">
</div>
</div>
<div class="home-modal__row-3" data-kit-types="kit">
<div class="home-modal__field">
<label>Инв. № доски</label>
<input name="board_inventory_number" class="home-modal__input">
</div>
<div class="home-modal__field">
<label>Модель доски</label>
<input name="board_model" class="home-modal__input">
</div>
<div class="home-modal__field">
<label>Серийный № доски</label>
<input name="board_serial" class="home-modal__input">
</div>
</div>
<div class="home-modal__row-3" data-kit-types="kit,display">
<div class="home-modal__field">
<label>Ноутбук / ПК (модель)</label>
<select name="computer_id" class="home-modal__select" id="homeProjectorComputerSelect">
<option value="">Ноутбук / ПК (модель)...</option>
{% for cid, cinv, cbrand, cmodel, ctype in computers %}
{% set comp_text = [cbrand, cmodel]|select|join(' ') %}
<option value="{{ cid }}" data-inv="{{ cinv }}" data-label="{{ comp_text }}">{{ comp_text }}{% if cinv %} ({{ cinv }}){% endif %}</option>
{% endfor %}
</select>
</div>
<div class="home-modal__field">
<label>Инв. № ноутбука/ПК</label>
<input name="computer_inventory_number" class="home-modal__input" id="homeProjectorComputerInv">
</div>
<div class="home-modal__field"></div>
</div>
<div class="home-modal__row-3-wide" data-kit-types="kit,projector,board,display,tv">
<div class="home-modal__field">
<label>Расположение</label>
<select name="cabinet_id" class="home-modal__select">
<option value="">Кабинет...</option>
{% for cid, cname in cabinets_full %}
<option value="{{ cid }}">{{ cname }}</option>
{% endfor %}
</select>
</div>
<div class="home-modal__field">
<label>Дата добавления</label>
<input name="date_in_operation" type="date" class="home-modal__input">
</div>
<div class="home-modal__field"></div>
</div>
<div class="home-modal__row-3-wide" data-kit-types="kit,projector,board,display,tv">
<div class="home-modal__field" style="grid-column: 1 / span 2;">
<label>Примечание</label>
<input name="note" class="home-modal__input">
</div>
<div class="home-modal__actions" style="align-self: end;">
<button class="home-modal__btn-primary">Добавить</button>
</div>
</div>
</form>
<form method="post" action="/document-cameras/add" class="home-modal__form d-none" data-add-type="document_camera">
<div class="home-modal__field">
<label>Введите инвентарный номер</label>
<input name="inventory_number" class="home-modal__input">
</div>
<div class="home-modal__row-3">
<div class="home-modal__field">
<label>Бренд</label>
<select name="brand_select" class="home-modal__select" id="homeDocBrandSelect">
<option value="">Бренд...</option>
{% for b in document_camera_brands %}
<option value="{{ b }}">{{ b }}</option>
{% endfor %}
<option value="__custom__">+ Добавить бренд</option>
</select>
<input name="brand_custom" id="homeDocBrandCustom" class="home-modal__input mt-2 d-none" placeholder="Новый бренд">
<input type="hidden" name="brand" id="homeDocBrandValue">
</div>
<div class="home-modal__field">
<label>Модель</label>
<input name="model" class="home-modal__input">
</div>
<div class="home-modal__field">
<label>Серийный №</label>
<input name="serial_number" class="home-modal__input">
</div>
</div>
<div class="home-modal__row-3-wide">
<div class="home-modal__field">
<label>Расположение</label>
<select name="cabinet_id" class="home-modal__select">
<option value="">Кабинет...</option>
{% for cid, cname in cabinets_full %}
<option value="{{ cid }}">{{ cname }}</option>
{% endfor %}
</select>
</div>
<div class="home-modal__field">
<label>Дата добавления</label>
<input name="date_in_operation" type="date" class="home-modal__input">
</div>
<div class="home-modal__actions" style="align-self: end;">
<button class="home-modal__btn-primary">Добавить</button>
</div>
</div>
</form>
</div>
</div>
<div class="home-modal__footer"></div>
</div>
<div class="home-modal hidden" id="homePcComponentsModal" role="dialog" aria-label="Компьютерные комплектующие">
<div class="home-modal__titlebar">
<span class="home-modal__btn" aria-hidden="true"></span>
<div class="home-modal__title">Компьютерные комплектующие</div>
<span class="home-modal__btn home-modal__close" id="homePcComponentsClose" aria-label="Close"></span>
</div>
<div class="home-modal__content">
<div class="home-modal__section-title">Сканируйте или введите модель/штрихкод</div>
<div class="home-modal__form">
<div class="home-modal__field">
<input id="pcComponentsLookupInput" class="home-modal__input" autocomplete="off" placeholder="Модель или штрихкод">
</div>
<div class="home-modal__status" id="pcComponentsLookupStatus"></div>
</div>
<form method="post" action="/components/assign" class="home-modal__form d-none" id="pcComponentsAssignForm" style="margin-top: 10px;">
<input type="hidden" name="component_id" id="pcComponentsAssignId">
<div class="home-modal__status"><strong>Модель:</strong> <span id="pcComponentsFoundModel">-</span></div>
<div class="home-modal__status"><strong>Тип:</strong> <span id="pcComponentsFoundType">-</span></div>
<div class="home-modal__status"><strong>Остаток:</strong> <span id="pcComponentsFoundQty">0</span></div>
<div class="pc-components-assign-grid">
<div class="home-modal__field">
<label for="pcComponentsComputerSelect">Выберите устройство</label>
<select id="pcComponentsComputerSelect" name="computer_id" class="home-modal__select">
<option value="">Выберите устройство...</option>
{% for cid, inv, brand, model, ctype in computers %}
<option value="{{ cid }}">{{ inv or '?' }} - {{ (brand ~ ' ' if brand else '') ~ (model or '?') }} ({{ 'Компьютер' if ctype == 'pc' else 'Ноутбук' }})</option>
{% endfor %}
</select>
</div>
<div class="home-modal__field">
<label for="pcComponentsAssignQty">Кол-во</label>
<input id="pcComponentsAssignQty" name="quantity" class="home-modal__input" value="1">
</div>
<div class="home-modal__actions pc-components-action">
<button class="home-modal__btn-primary">Выбрать</button>
</div>
</div>
</form>
<form method="post" action="/components/add_quick" class="home-modal__form d-none" id="pcComponentsAddForm" style="margin-top: 10px;">
<div class="home-modal__status">Запись не найдена. Добавить?</div>
<div class="pc-components-add-grid">
<div class="home-modal__field">
<label for="pcComponentsAddBarcode">Штрихкод (необязательно)</label>
<input id="pcComponentsAddBarcode" name="barcode" class="home-modal__input">
</div>
<div class="home-modal__field">
<label for="pcComponentsAddModel">Модель</label>
<input id="pcComponentsAddModel" name="model" class="home-modal__input" autocomplete="off">
</div>
<div class="home-modal__field">
<label for="pcComponentsAddType">Тип</label>
<select id="pcComponentsAddType" name="component_type" class="home-modal__select">
<option value="">Выберите тип...</option>
{% for t in component_types %}
<option value="{{ t }}">{{ t }}</option>
{% endfor %}
</select>
</div>
<div class="home-modal__field">
<label for="pcComponentsAddQty">Кол-во</label>
<input id="pcComponentsAddQty" name="quantity" class="home-modal__input" value="1">
</div>
<div class="home-modal__actions pc-components-add-actions pc-components-action">
<button class="home-modal__btn-primary">Добавить</button>
</div>
</div>
</form>
</div>
<div class="home-modal__footer"></div>
</div>
{% if session.get('role') == 'admin' %}
<div class="home-modal hidden" id="homeUsersModal" role="dialog" aria-label="Пользователи">
<div class="home-modal__titlebar">
<span class="home-modal__btn" aria-hidden="true"></span>
<div class="home-modal__title">Пользователи</div>
<span class="home-modal__btn home-modal__close" id="homeUsersClose" aria-label="Close"></span>
</div>
<div class="home-modal__content">
<div class="users-modal-grid">
<div class="users-role-note">
<div><strong>Администратор</strong> — полный доступ.</div>
<div class="mt-2"><strong>Завхоз</strong> — ограниченный доступ, но есть доступ к передвижению техникой, добавлению картриджей и расходных материалов, а также добавлению и удалению оборудования.</div>
<div class="mt-2"><strong>Просмотр</strong> — доступ только для просмотра.</div>
</div>
<div>
<ul class="users-list" id="usersList">
{% for row in users_rows %}
<li>
<div><strong>{{ row.full_name }}</strong></div>
<div>Роль: {{ row.role }}</div>
<div>Логин: {{ row.login }}</div>
<div>Пароль: {{ row.password }}</div>
</li>
{% endfor %}
</ul>
<div class="d-flex justify-content-start mb-2">
<button class="home-modal__btn-primary" type="button" id="usersAddToggle">Добавить пользователя</button>
</div>
<form method="post" action="/users/add" class="users-add-form d-none" id="usersAddForm">
<div class="home-modal__row-3">
<div class="home-modal__field">
<label>Фамилия</label>
<input name="last_name" class="home-modal__input">
</div>
<div class="home-modal__field">
<label>Имя</label>
<input name="first_name" class="home-modal__input">
</div>
<div class="home-modal__field">
<label>Отчество</label>
<input name="patronymic" class="home-modal__input">
</div>
</div>
<div class="home-modal__row-3">
<div class="home-modal__field">
<label>Роль</label>
<select name="role" class="home-modal__select">
<option value="">Выберите роль...</option>
<option value="admin">Администратор</option>
<option value="storekeeper">Завхоз</option>
<option value="viewer">Режим просмотра</option>
</select>
</div>
<div class="home-modal__field">
<label>Логин</label>
<input name="login" class="home-modal__input">
</div>
<div class="home-modal__field">
<label>Пароль</label>
<input name="password" type="password" class="home-modal__input">
</div>
</div>
<div class="home-modal__row-2">
<div class="home-modal__field">
<label>Подтвердите пароль</label>
<input name="password_confirm" type="password" class="home-modal__input">
</div>
</div>
<div class="home-modal__actions">
<button class="home-modal__btn-primary">Сохранить</button>
</div>
</form>
</div>
</div>
</div>
<div class="home-modal__footer"></div>
</div>
{% endif %}
<div class="d-flex justify-content-center">
<div class="w-100 home-wrap" style="max-width: 860px;">
<!-- removed title -->
{% if session.get('role') not in ('admin','storekeeper') %}
<div class="alert alert-secondary">Режим просмотра: выдача доступна только администратору и завхозу.</div>
{% endif %}
<div class="card mb-3 d-none" id="addCard">
<div class="card-body">
<h5 class="card-title mb-2">Штрихкод не найден — добавить в базу</h5>
<div class="row g-2 align-items-center mb-2">
<div class="col-md-4">
<select id="addKindSelect" class="form-select">
<option value="">Что добавляем...</option>
<option value="cartridge">Картридж</option>
<option value="consumable">Расходный материал</option>
</select>
</div>
</div>
<form method="post" action="/add" class="row g-2 align-items-center d-none" id="addCartridgeForm">
<div class="col-md-3">
<input name="barcode" id="addCartridgeBarcode" class="form-control" placeholder="Штрихкод" readonly>
</div>
<div class="col-md-4">
<input name="model" class="form-control" placeholder="Модель">
</div>
<div class="col-md-2">
<input name="quantity" class="form-control" value="1">
</div>
<div class="col-md-2 d-grid">
<button class="btn btn-primary" {% if session.get('role') not in ('admin','storekeeper') %}disabled{% endif %}>Добавить</button>
</div>
</form>
<form method="post" action="/consumables/add" class="row g-2 align-items-center d-none" id="addConsumableForm">
<div class="col-md-3">
<input name="barcode" id="addConsumableBarcode" class="form-control" placeholder="Штрихкод" readonly>
</div>
<div class="col-md-3">
<input name="model" class="form-control" placeholder="Модель">
</div>
<div class="col-md-3">
<select name="type" id="consumableTypeSelect" class="form-select">
<option value="">Тип расходника...</option>
{% for t in consumable_types %}
<option value="{{ t }}">{{ t }}</option>
{% endfor %}
<option value="__custom__">Свой тип...</option>
</select>
<input name="type_custom" id="consumableTypeCustom" class="form-control mt-2 d-none" placeholder="Свой тип">
</div>
<div class="col-md-2">
<input name="quantity" class="form-control" value="1">
</div>
<div class="col-md-1 d-grid">
<button class="btn btn-primary" {% if session.get('role') not in ('admin','storekeeper') %}disabled{% endif %}>Добавить</button>
</div>
</form>
</div>
</div>
<div class="card mb-3 d-none" id="deviceCard">
<div class="card-body">
<h5 class="card-title mb-2" id="deviceCardTitle">Устройство</h5>
<div class="row g-2">
<div class="col-md-4">
<div class="form-text">Инвентарный номер</div>
<div id="deviceInventory" class="fw-semibold"></div>
</div>
<div class="col-md-4" id="deviceBrandWrap">
<div class="form-text">Бренд</div>
<div id="deviceBrand"></div>
</div>
<div class="col-md-4">
<div class="form-text">Модель</div>
<div id="deviceModel"></div>
</div>
<div class="col-md-4" id="deviceSerialWrap">
<div class="form-text">Серийный номер</div>
<div id="deviceSerial"></div>
</div>
<div class="col-md-3">
<div class="form-text">Кабинет</div>
<div id="deviceCabinet"></div>
</div>
<div class="col-md-9">
<div class="form-text">Дата ввода</div>
<div id="deviceDate"></div>
</div>
</div>
<div class="d-none" id="deviceConsumablesSection">
<hr class="my-3">
<div>
<div class="form-text">Привязанные расходные материалы</div>
<ul class="mb-0" id="deviceConsumablesList"></ul>
</div>
</div>
<div class="d-none" id="computerSpecsSection">
<hr class="my-3">
<div class="form-text">Характеристики</div>
<div class="border rounded-3 p-2 small">
<div class="row g-1">
<div class="col-12">CPU: <span id="compCpu"></span></div>
<div class="col-12">GPU: <span id="compGpu"></span></div>
<div class="col-12">RAM: <span id="compMemory"></span></div>
<div class="col-12">Накопитель: <span id="compStorage"></span></div>
<div class="col-12" id="compMotherboardRow">Материнская плата: <span id="compMotherboard"></span></div>
<div class="col-12">ОС: <span id="compOs"></span></div>
</div>
</div>
</div>
<div class="d-none" id="projectorKitSection">
<hr class="my-3">
<div class="form-text">Комплект</div>
<div class="border rounded-3 p-2 small">
<div class="row g-2">
<div class="col-md-4">
<div class="fw-semibold mb-1">Проектор</div>
<div>Инв. №: <span id="projInv"></span></div>
<div>Бренд: <span id="projBrand"></span></div>
<div>Серийный №: <span id="projSerial"></span></div>
</div>
<div class="col-md-4">
<div class="fw-semibold mb-1">Доска</div>
<div>Инв. №: <span id="boardInv"></span></div>
<div>Бренд: <span id="boardBrand"></span></div>
<div>Модель: <span id="boardModel"></span></div>
<div>Серийный №: <span id="boardSerial"></span></div>
</div>
<div class="col-md-4">
<div class="fw-semibold mb-1">Компьютер/ноутбук</div>
<div>Инв. №: <span id="kitCompInv"></span></div>
<div>Бренд: <span id="kitCompBrand"></span></div>
<div>Модель: <span id="kitCompModel"></span></div>
<div>Серийный №: <span id="kitCompSerial"></span></div>
</div>
</div>
</div>
</div>
<div class="d-none" id="projectorComputerSection">
<hr class="my-3">
<div class="form-text">Компьютер/ноутбук</div>
<div class="border rounded-3 p-2 small">
<div class="row g-1">
<div class="col-12">Модель: <span id="projCompModel"></span></div>
<div class="col-12">Инв. №: <span id="projCompInv"></span></div>
</div>
</div>
</div>
<hr class="my-3">
<form method="post" id="equipmentCabinetForm" class="row g-2 align-items-end">
<input type="hidden" name="id" id="equipmentId">
<div class="col-md-4">
<label class="form-label mb-1">Сменить кабинет</label>
<select name="cabinet_id" class="form-select" id="equipmentCabinetSelect" {% if session.get('role') not in ('admin','storekeeper') %}disabled{% endif %}>
<option value=""></option>
{% for cid, cname in cabinets_full %}
<option value="{{ cid }}">{{ cname }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3 d-grid">
<button class="btn btn-primary" {% if session.get('role') not in ('admin','storekeeper') %}disabled{% endif %}>Сменить расположение</button>
</div>
</form>
</div>
</div>
<div class="card mb-3 d-none" id="cartridgeCard">
<div class="card-body">
<h5 class="card-title mb-2">Картридж</h5>
<form method="post" action="/issue" class="row g-2 align-items-center">
<input type="hidden" name="barcode" id="cartridgeBarcode">
<div class="col-md-4">
<input class="form-control" id="cartridgeModel" placeholder="Модель" readonly>
</div>
<div class="col-md-2">
<input name="quantity" class="form-control" value="1">
</div>
<div class="col-md-3">
<select name="cabinet" class="form-select" id="cartridgeCabinet">
<option value="">Выберите кабинет...</option>
{% for c in cabinets %}
<option value="{{ c }}">{{ c }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2 d-grid">
<button class="btn btn-danger" id="cartridgeIssueSubmit" {% if session.get('role') not in ('admin','storekeeper') %}disabled{% endif %}>Выдать</button>
</div>
</form>
</div>
</div>
<div class="card mb-3 d-none" id="consumableCard">
<div class="card-body">
<h5 class="card-title mb-2">Расходный материал</h5>
<form method="post" action="/consumables/issue" class="row g-2 align-items-center">
<input type="hidden" name="barcode" id="consumableBarcode">
<div class="col-md-4">
<input class="form-control" id="consumableModel" placeholder="Модель" readonly>
</div>
<div class="col-md-4">
<select name="device_id" class="form-select">
<option value="">Выберите устройство...</option>
{% for did, inv, model, dtype in devices %}
<option value="{{ did }}">{{ inv }} — {{ model }} ({{ 'Принтер' if dtype == 'printer' else 'МФУ' }})</option>
{% endfor %}
</select>
</div>
<div class="col-md-2 d-grid">
<button class="btn btn-danger" {% if session.get('role') not in ('admin','storekeeper') %}disabled{% endif %}>Выдать</button>
</div>
</form>
</div>
</div>
<script>
(function() {
const input = document.getElementById('barcodeInput');
const hint = document.getElementById('barcodeHint');
const cartCard = document.getElementById('cartridgeCard');
const consCard = document.getElementById('consumableCard');
const addCard = document.getElementById('addCard');
const deviceCard = document.getElementById('deviceCard');
const deviceCardTitle = document.getElementById('deviceCardTitle');
const deviceInventory = document.getElementById('deviceInventory');
const deviceBrand = document.getElementById('deviceBrand');
const deviceBrandWrap = document.getElementById('deviceBrandWrap');
const deviceSerialWrap = document.getElementById('deviceSerialWrap');
const deviceSerial = document.getElementById('deviceSerial');
const deviceModel = document.getElementById('deviceModel');
const deviceCabinet = document.getElementById('deviceCabinet');
const deviceDate = document.getElementById('deviceDate');
const deviceConsumablesList = document.getElementById('deviceConsumablesList');
const deviceConsumablesSection = document.getElementById('deviceConsumablesSection');
const computerSpecsSection = document.getElementById('computerSpecsSection');
const compCpu = document.getElementById('compCpu');
const compGpu = document.getElementById('compGpu');
const compMemory = document.getElementById('compMemory');
const compStorage = document.getElementById('compStorage');
const compMotherboardRow = document.getElementById('compMotherboardRow');
const compMotherboard = document.getElementById('compMotherboard');
const compOs = document.getElementById('compOs');
const projectorKitSection = document.getElementById('projectorKitSection');
const projInv = document.getElementById('projInv');
const projBrand = document.getElementById('projBrand');
const projSerial = document.getElementById('projSerial');
const boardModel = document.getElementById('boardModel');
const boardInv = document.getElementById('boardInv');
const boardBrand = document.getElementById('boardBrand');
const boardSerial = document.getElementById('boardSerial');
const kitCompInv = document.getElementById('kitCompInv');
const kitCompBrand = document.getElementById('kitCompBrand');
const kitCompModel = document.getElementById('kitCompModel');
const kitCompSerial = document.getElementById('kitCompSerial');
const projectorComputerSection = document.getElementById('projectorComputerSection');
const projCompModel = document.getElementById('projCompModel');
const projCompInv = document.getElementById('projCompInv');
const equipmentCabinetForm = document.getElementById('equipmentCabinetForm');
const equipmentId = document.getElementById('equipmentId');
const equipmentCabinetSelect = document.getElementById('equipmentCabinetSelect');
const addKindSelect = document.getElementById('addKindSelect');
const addCartridgeForm = document.getElementById('addCartridgeForm');
const addConsumableForm = document.getElementById('addConsumableForm');
const addCartridgeBarcode = document.getElementById('addCartridgeBarcode');
const addConsumableBarcode = document.getElementById('addConsumableBarcode');
const consumableTypeSelect = document.getElementById('consumableTypeSelect');
const consumableTypeCustom = document.getElementById('consumableTypeCustom');
const cartModel = document.getElementById('cartridgeModel');
const cartBarcode = document.getElementById('cartridgeBarcode');
const consModel = document.getElementById('consumableModel');
const consBarcode = document.getElementById('consumableBarcode');
const desktopBtn = document.getElementById('homeBarcodeBtn');
const desktopModal = document.getElementById('homeDesktopModal');
const desktopClose = document.getElementById('homeDesktopClose');
const desktopTitlebar = desktopModal ? desktopModal.querySelector('.home-modal__titlebar') : null;
const photoBtn = document.getElementById('homePhotoBtn');
const photoModal = document.getElementById('homePhotoModal');
const photoClose = document.getElementById('homePhotoClose');
const photoTitlebar = photoModal ? photoModal.querySelector('.home-modal__titlebar') : null;
const searchInvBtn = document.getElementById('homeSearchInvBtn');
const searchInvModal = document.getElementById('homeSearchInvModal');
const searchInvClose = document.getElementById('homeSearchInvClose');
const searchInvTitlebar = searchInvModal ? searchInvModal.querySelector('.home-modal__titlebar') : null;
const printersBtn = document.getElementById('homePrintersBtn');
const printersModal = document.getElementById('homePrintersModal');
const printersClose = document.getElementById('homePrintersClose');
const printersTitlebar = printersModal ? printersModal.querySelector('.home-modal__titlebar') : null;
const computersBtn = document.getElementById('homeComputersBtn');
const computersModal = document.getElementById('homeComputersModal');
const computersClose = document.getElementById('homeComputersClose');
const computersTitlebar = computersModal ? computersModal.querySelector('.home-modal__titlebar') : null;
const projectorsBtn = document.getElementById('homeProjectBtn');
const projectorsModal = document.getElementById('homeProjectorsModal');
const projectorsClose = document.getElementById('homeProjectorsClose');
const projectorsTitlebar = projectorsModal ? projectorsModal.querySelector('.home-modal__titlebar') : null;
const docCameraBtn = document.getElementById('homeDocCameraBtn');
const docCameraModal = document.getElementById('homeDocCameraModal');
const docCameraClose = document.getElementById('homeDocCameraClose');
const docCameraTitlebar = docCameraModal ? docCameraModal.querySelector('.home-modal__titlebar') : null;
const usersBtn = document.getElementById('homeUsersBtn');
const usersModal = document.getElementById('homeUsersModal');
const usersClose = document.getElementById('homeUsersClose');
const usersTitlebar = usersModal ? usersModal.querySelector('.home-modal__titlebar') : null;
const usersAddToggle = document.getElementById('usersAddToggle');
const usersAddForm = document.getElementById('usersAddForm');
const addDeviceBtn = document.getElementById('homeAddDeviceBtn');
const addDeviceModal = document.getElementById('homeAddDeviceModal');
const addDeviceClose = document.getElementById('homeAddDeviceClose');
const addDeviceTitlebar = addDeviceModal ? addDeviceModal.querySelector('.home-modal__titlebar') : null;
const pcComponentsBtn = document.getElementById('homePcComponentsBtn');
const pcComponentsModal = document.getElementById('homePcComponentsModal');
const pcComponentsClose = document.getElementById('homePcComponentsClose');
const pcComponentsTitlebar = pcComponentsModal ? pcComponentsModal.querySelector('.home-modal__titlebar') : null;
const pcComponentsLookupInput = document.getElementById('pcComponentsLookupInput');
const pcComponentsLookupStatus = document.getElementById('pcComponentsLookupStatus');
const pcComponentsAssignForm = document.getElementById('pcComponentsAssignForm');
const pcComponentsAssignId = document.getElementById('pcComponentsAssignId');
const pcComponentsFoundModel = document.getElementById('pcComponentsFoundModel');
const pcComponentsFoundType = document.getElementById('pcComponentsFoundType');
const pcComponentsFoundQty = document.getElementById('pcComponentsFoundQty');
const pcComponentsAssignQty = document.getElementById('pcComponentsAssignQty');
const pcComponentsAddForm = document.getElementById('pcComponentsAddForm');
const pcComponentsAddBarcode = document.getElementById('pcComponentsAddBarcode');
const pcComponentsAddModel = document.getElementById('pcComponentsAddModel');
const pcComponentsAddQty = document.getElementById('pcComponentsAddQty');
const addDeviceType = document.getElementById('addDeviceType');
const addDeviceForms = addDeviceModal ? addDeviceModal.querySelectorAll('[data-add-type]') : [];
const homeComputerSpecsToggle = document.getElementById('homeComputerSpecsToggle');
const homeComputerSpecs = document.getElementById('homeComputerSpecs');
const homeDeviceBrandSelect = document.getElementById('homeDeviceBrandSelect');
const homeDeviceBrandCustom = document.getElementById('homeDeviceBrandCustom');
const homeDeviceBrandValue = document.getElementById('homeDeviceBrandValue');
const homeComputerBrandSelect = document.getElementById('homeComputerBrandSelect');
const homeComputerBrandCustom = document.getElementById('homeComputerBrandCustom');
const homeComputerBrandValue = document.getElementById('homeComputerBrandValue');
const homeProjectorBrandSelect = document.getElementById('homeProjectorBrandSelect');
const homeProjectorBrandCustom = document.getElementById('homeProjectorBrandCustom');
const homeProjectorBrandValue = document.getElementById('homeProjectorBrandValue');
const homeDocBrandSelect = document.getElementById('homeDocBrandSelect');
const homeDocBrandCustom = document.getElementById('homeDocBrandCustom');
const homeDocBrandValue = document.getElementById('homeDocBrandValue');
const homeProjectorComputerSelect = document.getElementById('homeProjectorComputerSelect');
const homeProjectorComputerInv = document.getElementById('homeProjectorComputerInv');
const homeNotesLayer = document.getElementById('homeNotesLayer');
const initialHomeNotes = {{ home_notes|tojson }};
let homeNotesMaxZ = initialHomeNotes.reduce((maxValue, note) => Math.max(maxValue, Number(note.z) || 1), 1);
const docCameraInv = document.getElementById('docCameraInv');
const docCameraInfoInv = document.getElementById('docCameraInfoInv');
const docCameraInfoBrand = document.getElementById('docCameraInfoBrand');
const docCameraInfoSerial = document.getElementById('docCameraInfoSerial');
const docCameraInfoDate = document.getElementById('docCameraInfoDate');
const docCameraInfoCabinet = document.getElementById('docCameraInfoCabinet');
const docCameraCabinetForm = document.getElementById('docCameraCabinetForm');
const docCameraCabinetSelect = document.getElementById('docCameraCabinetSelect');
const docCameraId = document.getElementById('docCameraId');
const searchCabinetViewSelect = document.getElementById('searchCabinetViewSelect');
const cabinetInfoTitle = document.getElementById('cabinetInfoTitle');
const cabinetDevicesWrap = document.getElementById('cabinetDevicesWrap');
const cabinetDevicesList = document.getElementById('cabinetDevicesList');
const cabinetComputersWrap = document.getElementById('cabinetComputersWrap');
const cabinetComputersList = document.getElementById('cabinetComputersList');
const cabinetProjectorsWrap = document.getElementById('cabinetProjectorsWrap');
const cabinetProjectorsList = document.getElementById('cabinetProjectorsList');
const cabinetDocsWrap = document.getElementById('cabinetDocsWrap');
const cabinetDocsList = document.getElementById('cabinetDocsList');
const cabinetEmptyNote = document.getElementById('cabinetEmptyNote');
const searchInvInput = document.getElementById('searchInvInput');
const searchInfoInv = document.getElementById('searchInfoInv');
const searchInfoType = document.getElementById('searchInfoType');
const searchInfoBrand = document.getElementById('searchInfoBrand');
const searchInfoSerial = document.getElementById('searchInfoSerial');
const searchInfoDate = document.getElementById('searchInfoDate');
const searchInfoCabinet = document.getElementById('searchInfoCabinet');
const searchConsumablesWrap = document.getElementById('searchConsumablesWrap');
const searchConsumablesList = document.getElementById('searchConsumablesList');
const searchSpecsWrap = document.getElementById('searchSpecsWrap');
const searchSpecCpu = document.getElementById('searchSpecCpu');
const searchSpecGpu = document.getElementById('searchSpecGpu');
const searchSpecRam = document.getElementById('searchSpecRam');
const searchSpecStorage = document.getElementById('searchSpecStorage');
const searchSpecOs = document.getElementById('searchSpecOs');
const searchSpecMotherboard = document.getElementById('searchSpecMotherboard');
const searchKitWrap = document.getElementById('searchKitWrap');
const searchKitProjectorTitle = document.getElementById('searchKitProjectorTitle');
const searchKitProjectorList = document.getElementById('searchKitProjectorList');
const searchKitBoardTitle = document.getElementById('searchKitBoardTitle');
const searchKitBoardList = document.getElementById('searchKitBoardList');
const searchKitComputerTitle = document.getElementById('searchKitComputerTitle');
const searchKitComputerList = document.getElementById('searchKitComputerList');
const projectorsInv = document.getElementById('projectorsInv');
const projectorsInfoInv = document.getElementById('projectorsInfoInv');
const projectorsInfoType = document.getElementById('projectorsInfoType');
const projectorsInfoBrand = document.getElementById('projectorsInfoBrand');
const projectorsInfoSerial = document.getElementById('projectorsInfoSerial');
const projectorsInfoDate = document.getElementById('projectorsInfoDate');
const projectorsInfoCabinet = document.getElementById('projectorsInfoCabinet');
const projectorsCabinetForm = document.getElementById('projectorsCabinetForm');
const projectorsCabinetSelect = document.getElementById('projectorsCabinetSelect');
const projectorsId = document.getElementById('projectorsId');
const projectorsKitWrap = document.getElementById('projectorsKitWrap');
const kitProjectorTitle = document.getElementById('kitProjectorTitle');
const kitProjectorList = document.getElementById('kitProjectorList');
const kitBoardTitle = document.getElementById('kitBoardTitle');
const kitBoardList = document.getElementById('kitBoardList');
const kitComputerTitle = document.getElementById('kitComputerTitle');
const kitComputerList = document.getElementById('kitComputerList');
const computersInv = document.getElementById('computersInv');
const computersInfoInv = document.getElementById('computersInfoInv');
const computersInfoBrand = document.getElementById('computersInfoBrand');
const computersInfoSerial = document.getElementById('computersInfoSerial');
const computersInfoDate = document.getElementById('computersInfoDate');
const computersInfoCabinet = document.getElementById('computersInfoCabinet');
const computersCabinetForm = document.getElementById('computersCabinetForm');
const computersCabinetSelect = document.getElementById('computersCabinetSelect');
const computersId = document.getElementById('computersId');
const computersSpecsWrap = document.getElementById('computersSpecsWrap');
const computersSpecsList = computersSpecsWrap ? computersSpecsWrap.querySelector('.home-modal__specs-list') : null;
const computersSpecCpu = document.getElementById('computersSpecCpu');
const computersSpecGpu = document.getElementById('computersSpecGpu');
const computersSpecRam = document.getElementById('computersSpecRam');
const computersSpecStorage = document.getElementById('computersSpecStorage');
const computersSpecOs = document.getElementById('computersSpecOs');
const computersSpecMotherboard = document.getElementById('computersSpecMotherboard');
const printersInv = document.getElementById('printersInv');
const printersInfoInv = document.getElementById('printersInfoInv');
const printersInfoBrand = document.getElementById('printersInfoBrand');
const printersInfoSerial = document.getElementById('printersInfoSerial');
const printersInfoDate = document.getElementById('printersInfoDate');
const printersInfoCabinet = document.getElementById('printersInfoCabinet');
const printersCabinetForm = document.getElementById('printersCabinetForm');
const printersCabinetSelect = document.getElementById('printersCabinetSelect');
const printersId = document.getElementById('printersId');
const printersConsumablesWrap = document.getElementById('printersConsumablesWrap');
const printersConsumablesList = document.getElementById('printersConsumablesList');
const photoBarcode = document.getElementById('photoBarcode');
const photoModelText = document.getElementById('photoModelText');
const photoStockText = document.getElementById('photoStockText');
const photoDeviceWrap = document.getElementById('photoDeviceWrap');
const photoDevice = document.getElementById('photoDevice');
const photoAddTitle = document.getElementById('photoAddTitle');
const photoAddForm = document.getElementById('photoAddForm');
const photoAddBarcode = document.getElementById('photoAddBarcode');
const photoAddModel = document.getElementById('photoAddModel');
const photoAddTypeSelect = document.getElementById('photoAddTypeSelect');
const photoAddTypeCustom = document.getElementById('photoAddTypeCustom');
const photoAddQty = document.getElementById('photoAddQty');
const modalBarcode = document.getElementById('modalBarcode');
const modalCartridgeName = document.getElementById('modalCartridgeName');
const modalCartridgeStock = document.getElementById('modalCartridgeStock');
const modalCabinet = document.getElementById('modalCabinet');
const modalQuantity = document.getElementById('modalQuantity');
const modalIssueSubmit = document.getElementById('modalIssueSubmit');
const modalAddNotice = document.getElementById('modalAddNotice');
const modalAddFields = document.getElementById('modalAddFields');
const modalAddModel = document.getElementById('modalAddModel');
const modalAddQty = document.getElementById('modalAddQty');
const modalAddActions = document.getElementById('modalAddActions');
const cartridgeCabinet = document.getElementById('cartridgeCabinet');
const cartridgeIssueSubmit = document.getElementById('cartridgeIssueSubmit');
const defaultModalCabinetOptions = modalCabinet ? modalCabinet.innerHTML : '';
const defaultCartridgeCabinetOptions = cartridgeCabinet ? cartridgeCabinet.innerHTML : '';
function togglePhotoAdd(show, barcode) {
if (!photoAddTitle || !photoAddForm) return;
const action = show ? 'remove' : 'add';
photoAddTitle.classList[action]('d-none');
photoAddForm.classList[action]('d-none');
if (show) {
if (photoAddBarcode) photoAddBarcode.value = barcode || '';
if (photoAddQty && !photoAddQty.value) photoAddQty.value = '1';
}
}
function syncPhotoAddType() {
if (!photoAddTypeSelect || !photoAddTypeCustom) return;
if (photoAddTypeSelect.value === '__custom__') {
photoAddTypeCustom.classList.remove('d-none');
photoAddTypeCustom.focus();
} else {
photoAddTypeCustom.classList.add('d-none');
photoAddTypeCustom.value = '';
}
}
function hideAll() {
cartCard.classList.add('d-none');
consCard.classList.add('d-none');
addCard.classList.add('d-none');
deviceCard.classList.add('d-none');
addCartridgeForm.classList.add('d-none');
addConsumableForm.classList.add('d-none');
}
function setCabinetOptions(selectEl, submitBtn, cabinets) {
if (!selectEl) return;
const values = Array.isArray(cabinets) ? cabinets.filter(Boolean) : [];
if (!values.length) {
selectEl.innerHTML = '<option value="">Нет привязанных кабинетов</option>';
selectEl.value = '';
selectEl.disabled = true;
if (submitBtn) submitBtn.disabled = true;
return;
}
selectEl.innerHTML = '';
const placeholder = document.createElement('option');
placeholder.value = '';
placeholder.textContent = 'Выберите кабинет...';
selectEl.appendChild(placeholder);
values.forEach((name) => {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
selectEl.appendChild(option);
});
selectEl.value = '';
selectEl.disabled = false;
if (submitBtn) submitBtn.disabled = false;
}
function resetCabinetOptions() {
if (modalCabinet) {
modalCabinet.innerHTML = defaultModalCabinetOptions;
modalCabinet.disabled = false;
}
if (cartridgeCabinet) {
cartridgeCabinet.innerHTML = defaultCartridgeCabinetOptions;
cartridgeCabinet.disabled = false;
}
if (modalIssueSubmit) modalIssueSubmit.disabled = false;
if (cartridgeIssueSubmit) cartridgeIssueSubmit.disabled = false;
}
function applyAllowedCabinets(cabinets) {
setCabinetOptions(modalCabinet, modalIssueSubmit, cabinets);
setCabinetOptions(cartridgeCabinet, cartridgeIssueSubmit, cabinets);
}
function showAdd(kind, barcode) {
addCard.classList.remove('d-none');
addKindSelect.value = kind || '';
addCartridgeBarcode.value = barcode || '';
addConsumableBarcode.value = barcode || '';
if (kind === 'cartridge') {
addCartridgeForm.classList.remove('d-none');
addConsumableForm.classList.add('d-none');
} else if (kind === 'consumable') {
addConsumableForm.classList.remove('d-none');
addCartridgeForm.classList.add('d-none');
} else {
addCartridgeForm.classList.add('d-none');
addConsumableForm.classList.add('d-none');
}
}
async function lookup() {
const barcode = (input.value || '').trim();
if (!barcode) {
hint.textContent = '';
hideAll();
resetCabinetOptions();
return;
}
hint.textContent = 'Ищу...';
try {
const res = await fetch(`/api/barcode?barcode=${encodeURIComponent(barcode)}`);
if (!res.ok) {
hint.textContent = 'Ошибка запроса';
hideAll();
return;
}
const data = await res.json();
if (!data.found) {
hint.textContent = 'Расходный материал не найден. Добавить?';
cartCard.classList.add('d-none');
consCard.classList.add('d-none');
deviceCard.classList.add('d-none');
showAdd('consumable', barcode);
resetCabinetOptions();
return;
}
if (data.kind === 'device' || data.kind === 'computer' || data.kind === 'projector' || data.kind === 'board') {
const kind = data.kind;
const titleMap = {
device: 'МФУ / Принтер',
computer: 'Компьютер / Ноутбук',
projector: 'Проектор',
board: 'Доска',
};
const typeLabel = data.type_label || data.kit_type_label || data.type || '';
deviceCardTitle.textContent = typeLabel || titleMap[kind] || 'Устройство';
deviceInventory.textContent = data.inventory_number || '';
deviceModel.textContent = data.model || data.projector_model || '';
deviceBrand.textContent = data.brand || '—';
deviceSerial.textContent = data.serial_number || data.projector_serial || '—';
deviceCabinet.textContent = data.cabinet_name || '—';
if (data.date_in_operation) {
const parsed = new Date(data.date_in_operation);
deviceDate.textContent = Number.isNaN(parsed.getTime())
? data.date_in_operation
: parsed.toLocaleDateString('ru-RU');
} else {
deviceDate.textContent = '—';
}
equipmentId.value = data.id || '';
equipmentCabinetSelect.value = data.cabinet_id || '';
if (kind === 'device') {
equipmentCabinetForm.action = '/devices/update_cabinet';
} else if (kind === 'computer') {
equipmentCabinetForm.action = '/computers/update_cabinet';
} else if (kind === 'projector') {
equipmentCabinetForm.action = '/projectors/update_cabinet';
} else if (kind === 'board') {
equipmentCabinetForm.action = '/projectors/update_cabinet';
}
if (kind === 'projector') {
deviceBrandWrap.classList.remove('d-none');
deviceSerialWrap.classList.remove('d-none');
} else if (kind === 'board') {
deviceBrandWrap.classList.add('d-none');
deviceSerialWrap.classList.remove('d-none');
} else {
deviceBrandWrap.classList.remove('d-none');
deviceSerialWrap.classList.remove('d-none');
}
deviceConsumablesList.innerHTML = '';
const items = Array.isArray(data.consumables) ? data.consumables : [];
if (kind === 'device' && items.length) {
deviceConsumablesSection.classList.remove('d-none');
items.forEach(item => {
const li = document.createElement('li');
const label = item.type ? `${item.type}: ` : '';
li.textContent = `${label}${item.model || ''}`.trim();
deviceConsumablesList.appendChild(li);
});
} else {
deviceConsumablesSection.classList.add('d-none');
}
if (kind === 'computer') {
const cpuLabel = [data.cpu_brand, data.cpu_model].filter(Boolean).join(' ');
function formatGB(value) {
const raw = (value || '').toString().trim();
if (!raw) return '—';
const lower = raw.toLowerCase();
const num = parseFloat(raw.replace(',', '.'));
if (Number.isNaN(num)) return raw;
if (lower.includes('mb') || lower.includes('мб')) {
const gb = num / 1024;
const label = gb % 1 === 0 ? gb.toFixed(0) : gb.toFixed(1);
return `${label}GB`;
}
if (lower.includes('gb') || lower.includes('гб')) {
const label = num % 1 === 0 ? num.toFixed(0) : num.toFixed(1);
return `${label}GB`;
}
const label = num % 1 === 0 ? num.toFixed(0) : num.toFixed(1);
return `${label}GB`;
}
const memoryLabel = [formatGB(data.memory_size), data.memory_type].filter(v => v && v !== '—').join(' ');
compCpu.textContent = cpuLabel || '—';
compGpu.textContent = data.gpu_model || '—';
compMemory.textContent = memoryLabel || '—';
compStorage.textContent = formatGB(data.storage_size);
compMotherboard.textContent = data.motherboard || '—';
compOs.textContent = data.os || '—';
if (data.type === 'laptop') {
compMotherboardRow.classList.add('d-none');
} else {
compMotherboardRow.classList.remove('d-none');
}
computerSpecsSection.classList.remove('d-none');
} else {
computerSpecsSection.classList.add('d-none');
}
if (kind === 'projector' && data.kit_type === 'kit') {
projInv.textContent = data.projector_inventory_number || '—';
projBrand.textContent = data.projector_brand || data.brand || '—';
projSerial.textContent = data.projector_serial || '—';
boardModel.textContent = data.board_model || '—';
boardInv.textContent = data.board_inventory_number || '—';
boardBrand.textContent = data.board_brand || data.brand || '—';
boardSerial.textContent = data.board_serial || '—';
kitCompInv.textContent = data.computer_inventory_number || '—';
kitCompBrand.textContent = data.computer_brand || '—';
kitCompModel.textContent = data.computer_model || '—';
kitCompSerial.textContent = data.computer_serial || '—';
projectorKitSection.classList.remove('d-none');
} else {
projectorKitSection.classList.add('d-none');
}
if (kind === 'projector' && data.kit_type === 'display') {
projCompModel.textContent = data.computer_model || '—';
projCompInv.textContent = data.computer_inventory_number || '—';
projectorComputerSection.classList.remove('d-none');
} else {
projectorComputerSection.classList.add('d-none');
}
cartCard.classList.add('d-none');
consCard.classList.add('d-none');
addCard.classList.add('d-none');
deviceCard.classList.remove('d-none');
resetCabinetOptions();
hint.textContent = `Найдено: ${data.inventory_number || ''}`;
} else if (data.kind === 'cartridge') {
cartModel.value = data.model || '';
cartBarcode.value = data.barcode || barcode;
consBarcode.value = '';
consModel.value = '';
applyAllowedCabinets(data.allowed_cabinets || []);
deviceCard.classList.add('d-none');
consCard.classList.add('d-none');
addCard.classList.add('d-none');
cartCard.classList.remove('d-none');
hint.textContent = `Картридж: ${data.model || ''}`;
} else if (data.kind === 'consumable') {
consModel.value = data.model || '';
consBarcode.value = data.barcode || barcode;
cartBarcode.value = '';
cartModel.value = '';
resetCabinetOptions();
deviceCard.classList.add('d-none');
cartCard.classList.add('d-none');
addCard.classList.add('d-none');
consCard.classList.remove('d-none');
hint.textContent = `Расходник: ${data.model || ''}`;
} else {
hint.textContent = 'Неизвестный тип';
hideAll();
resetCabinetOptions();
}
} catch (e) {
hint.textContent = 'Ошибка сети';
hideAll();
resetCabinetOptions();
}
}
addKindSelect.addEventListener('change', () => {
const kind = addKindSelect.value;
const barcode = (input.value || '').trim();
showAdd(kind, barcode);
});
consumableTypeSelect.addEventListener('change', () => {
if (consumableTypeSelect.value === '__custom__') {
consumableTypeCustom.classList.remove('d-none');
consumableTypeCustom.focus();
} else {
consumableTypeCustom.classList.add('d-none');
consumableTypeCustom.value = '';
}
});
if (input) {
let timer = null;
input.addEventListener('input', () => {
if (timer) window.clearTimeout(timer);
timer = window.setTimeout(lookup, 120);
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
lookup();
}
});
}
function resetPcComponentsUi() {
if (pcComponentsLookupStatus) pcComponentsLookupStatus.textContent = '';
if (pcComponentsAssignForm) pcComponentsAssignForm.classList.add('d-none');
if (pcComponentsAddForm) pcComponentsAddForm.classList.add('d-none');
if (pcComponentsAssignId) pcComponentsAssignId.value = '';
if (pcComponentsFoundModel) pcComponentsFoundModel.textContent = '—';
if (pcComponentsFoundType) pcComponentsFoundType.textContent = '—';
if (pcComponentsFoundQty) pcComponentsFoundQty.textContent = '0';
if (pcComponentsAssignQty) pcComponentsAssignQty.value = '1';
if (pcComponentsAddQty) pcComponentsAddQty.value = '1';
}
async function lookupPcComponent() {
if (!pcComponentsLookupInput || !pcComponentsLookupStatus) return;
const q = (pcComponentsLookupInput.value || '').trim();
if (!q) {
resetPcComponentsUi();
return;
}
pcComponentsLookupStatus.textContent = 'Ищу...';
try {
const res = await fetch(`/api/component_lookup?q=${encodeURIComponent(q)}`);
if (!res.ok) {
resetPcComponentsUi();
pcComponentsLookupStatus.textContent = 'Ошибка поиска';
return;
}
const data = await res.json();
if (data && data.found && data.component) {
const comp = data.component;
if (pcComponentsAssignId) pcComponentsAssignId.value = comp.id || '';
if (pcComponentsFoundModel) pcComponentsFoundModel.textContent = comp.model || '—';
if (pcComponentsFoundType) pcComponentsFoundType.textContent = comp.component_type || '—';
if (pcComponentsFoundQty) pcComponentsFoundQty.textContent = String(comp.quantity ?? 0);
if (pcComponentsLookupStatus) pcComponentsLookupStatus.textContent = '';
if (pcComponentsAssignForm) pcComponentsAssignForm.classList.remove('d-none');
if (pcComponentsAddForm) pcComponentsAddForm.classList.add('d-none');
} else {
if (pcComponentsLookupStatus) pcComponentsLookupStatus.textContent = 'Неизвестный штрихкод/модель';
if (pcComponentsAssignForm) pcComponentsAssignForm.classList.add('d-none');
if (pcComponentsAddForm) pcComponentsAddForm.classList.remove('d-none');
if (pcComponentsAddBarcode) pcComponentsAddBarcode.value = q;
}
} catch (e) {
resetPcComponentsUi();
if (pcComponentsLookupStatus) pcComponentsLookupStatus.textContent = 'Ошибка сети';
}
}
if (pcComponentsLookupInput) {
let pcLookupTimer = null;
pcComponentsLookupInput.addEventListener('input', () => {
resetPcComponentsUi();
if (pcLookupTimer) window.clearTimeout(pcLookupTimer);
const q = (pcComponentsLookupInput.value || '').trim();
if (q.length < 3) return;
pcLookupTimer = window.setTimeout(lookupPcComponent, 600);
});
pcComponentsLookupInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
lookupPcComponent();
}
});
}
function toggleDesktop(show) {
if (!desktopModal) return;
const shouldShow = typeof show === 'boolean' ? show : desktopModal.classList.contains('hidden');
desktopModal.classList.toggle('hidden', !shouldShow);
}
function togglePhoto(show) {
if (!photoModal) return;
const shouldShow = typeof show === 'boolean' ? show : photoModal.classList.contains('hidden');
photoModal.classList.toggle('hidden', !shouldShow);
}
function toggleSearchInv(show) {
if (!searchInvModal) return;
const shouldShow = typeof show === 'boolean' ? show : searchInvModal.classList.contains('hidden');
searchInvModal.classList.toggle('hidden', !shouldShow);
}
function togglePrinters(show) {
if (!printersModal) return;
const shouldShow = typeof show === 'boolean' ? show : printersModal.classList.contains('hidden');
printersModal.classList.toggle('hidden', !shouldShow);
}
function toggleComputers(show) {
if (!computersModal) return;
const shouldShow = typeof show === 'boolean' ? show : computersModal.classList.contains('hidden');
computersModal.classList.toggle('hidden', !shouldShow);
}
function toggleProjectors(show) {
if (!projectorsModal) return;
const shouldShow = typeof show === 'boolean' ? show : projectorsModal.classList.contains('hidden');
projectorsModal.classList.toggle('hidden', !shouldShow);
}
function toggleDocCamera(show) {
if (!docCameraModal) return;
const shouldShow = typeof show === 'boolean' ? show : docCameraModal.classList.contains('hidden');
docCameraModal.classList.toggle('hidden', !shouldShow);
}
function toggleAddDevice(show) {
if (!addDeviceModal) return;
const shouldShow = typeof show === 'boolean' ? show : addDeviceModal.classList.contains('hidden');
addDeviceModal.classList.toggle('hidden', !shouldShow);
}
function togglePcComponents(show) {
if (!pcComponentsModal) return;
const shouldShow = typeof show === 'boolean' ? show : pcComponentsModal.classList.contains('hidden');
pcComponentsModal.classList.toggle('hidden', !shouldShow);
if (shouldShow) {
resetPcComponentsUi();
if (pcComponentsLookupInput) {
pcComponentsLookupInput.value = '';
pcComponentsLookupInput.focus();
}
}
}
function toggleUsers(show) {
if (!usersModal) return;
const shouldShow = typeof show === 'boolean' ? show : usersModal.classList.contains('hidden');
usersModal.classList.toggle('hidden', !shouldShow);
}
if (desktopBtn) {
desktopBtn.addEventListener('click', () => toggleDesktop(true));
}
if (desktopClose) {
desktopClose.addEventListener('click', () => toggleDesktop(false));
}
if (photoBtn) {
photoBtn.addEventListener('click', () => togglePhoto(true));
}
if (photoClose) {
photoClose.addEventListener('click', () => togglePhoto(false));
}
if (searchInvBtn) {
searchInvBtn.addEventListener('click', () => toggleSearchInv(true));
}
if (searchInvClose) {
searchInvClose.addEventListener('click', () => toggleSearchInv(false));
}
if (printersBtn) {
printersBtn.addEventListener('click', () => togglePrinters(true));
}
if (printersClose) {
printersClose.addEventListener('click', () => togglePrinters(false));
}
if (computersBtn) {
computersBtn.addEventListener('click', () => toggleComputers(true));
}
if (computersClose) {
computersClose.addEventListener('click', () => toggleComputers(false));
}
if (projectorsBtn) {
projectorsBtn.addEventListener('click', () => toggleProjectors(true));
}
if (projectorsClose) {
projectorsClose.addEventListener('click', () => toggleProjectors(false));
}
if (docCameraBtn) {
docCameraBtn.addEventListener('click', () => toggleDocCamera(true));
}
if (docCameraClose) {
docCameraClose.addEventListener('click', () => toggleDocCamera(false));
}
if (usersBtn) {
usersBtn.addEventListener('click', () => toggleUsers(true));
}
if (usersClose) {
usersClose.addEventListener('click', () => toggleUsers(false));
}
if (usersAddToggle && usersAddForm) {
usersAddToggle.addEventListener('click', () => {
usersAddForm.classList.toggle('d-none');
if (!usersAddForm.classList.contains('d-none')) {
const input = usersAddForm.querySelector('input[name="last_name"]');
if (input) input.focus();
}
});
}
if (addDeviceBtn) {
addDeviceBtn.addEventListener('click', () => toggleAddDevice(true));
}
if (addDeviceClose) {
addDeviceClose.addEventListener('click', () => toggleAddDevice(false));
}
if (pcComponentsBtn) {
pcComponentsBtn.addEventListener('click', () => togglePcComponents(true));
}
if (pcComponentsClose) {
pcComponentsClose.addEventListener('click', () => togglePcComponents(false));
}
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
toggleDesktop(false);
togglePhoto(false);
toggleSearchInv(false);
togglePrinters(false);
toggleComputers(false);
toggleProjectors(false);
toggleDocCamera(false);
toggleUsers(false);
toggleAddDevice(false);
togglePcComponents(false);
}
});
async function lookupModalBarcode() {
if (!modalBarcode || !modalCartridgeName) return;
const barcode = (modalBarcode.value || '').trim();
if (!barcode) {
modalCartridgeName.textContent = '';
if (modalCartridgeStock) modalCartridgeStock.textContent = '';
resetCabinetOptions();
if (modalAddNotice) modalAddNotice.classList.add('d-none');
if (modalAddFields) modalAddFields.classList.add('d-none');
if (modalAddActions) modalAddActions.classList.add('d-none');
if (modalAddModel) modalAddModel.value = '';
return;
}
modalCartridgeName.textContent = 'Ищу...';
if (modalCartridgeStock) modalCartridgeStock.textContent = '';
if (modalAddNotice) modalAddNotice.classList.add('d-none');
if (modalAddFields) modalAddFields.classList.add('d-none');
if (modalAddActions) modalAddActions.classList.add('d-none');
try {
const res = await fetch(`/api/barcode?barcode=${encodeURIComponent(barcode)}`);
if (!res.ok) {
modalCartridgeName.textContent = 'Ошибка запроса';
if (modalCartridgeStock) modalCartridgeStock.textContent = '';
return;
}
const data = await res.json();
if (!data.found || data.kind !== 'cartridge') {
modalCartridgeName.textContent = 'Картридж не найден';
resetCabinetOptions();
if (modalAddNotice) modalAddNotice.classList.remove('d-none');
if (modalAddFields) modalAddFields.classList.remove('d-none');
if (modalAddActions) modalAddActions.classList.remove('d-none');
if (modalAddQty && !modalAddQty.value) modalAddQty.value = '1';
if (modalCartridgeStock) modalCartridgeStock.textContent = '';
return;
}
modalCartridgeName.textContent = `Картридж: ${data.model || ''}`.trim();
applyAllowedCabinets(data.allowed_cabinets || []);
if (modalCartridgeStock) {
const qty = typeof data.quantity === 'number' ? data.quantity : '';
modalCartridgeStock.textContent = qty !== '' ? `Остаток: ${qty}` : '';
}
if (modalQuantity && !modalQuantity.value) {
modalQuantity.value = '1';
}
} catch (e) {
modalCartridgeName.textContent = 'Ошибка сети';
resetCabinetOptions();
}
}
if (modalBarcode) {
let modalTimer = null;
modalBarcode.addEventListener('input', () => {
if (modalTimer) window.clearTimeout(modalTimer);
modalTimer = window.setTimeout(lookupModalBarcode, 120);
});
modalBarcode.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
lookupModalBarcode();
}
});
}
async function loadConsumableDevices(barcode) {
if (!photoDevice) return;
photoDevice.innerHTML = '<option value="">Выберите устройство...</option>';
if (!barcode) return;
try {
const res = await fetch(`/api/consumable_devices?barcode=${encodeURIComponent(barcode)}`);
if (!res.ok) return;
const data = await res.json();
const items = Array.isArray(data.devices) ? data.devices : [];
if (!items.length) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'Нет привязанных устройств';
photoDevice.appendChild(opt);
return;
}
items.forEach(item => {
const opt = document.createElement('option');
const typeLabel = item.type === 'printer' ? 'Принтер' : 'МФУ';
opt.value = item.id;
opt.textContent = `${item.inventory_number || ''}${item.model || ''} (${typeLabel})`.trim();
photoDevice.appendChild(opt);
});
} catch (e) {
// ignore
}
}
async function lookupPhotoBarcode() {
if (!photoBarcode || !photoDeviceWrap) return;
const barcode = (photoBarcode.value || '').trim();
if (!barcode) {
photoDeviceWrap.classList.add('d-none');
if (photoModelText) {
photoModelText.textContent = '';
photoModelText.classList.add('d-none');
}
if (photoStockText) {
photoStockText.textContent = '';
photoStockText.classList.add('d-none');
}
if (photoDevice) photoDevice.value = '';
togglePhotoAdd(false);
return;
}
try {
const res = await fetch(`/api/barcode?barcode=${encodeURIComponent(barcode)}`);
if (!res.ok) {
if (photoModelText) {
photoModelText.textContent = 'Ошибка запроса';
photoModelText.classList.remove('d-none');
}
photoDeviceWrap.classList.add('d-none');
if (photoStockText) {
photoStockText.textContent = '';
photoStockText.classList.add('d-none');
}
togglePhotoAdd(false);
return;
}
const data = await res.json();
if (!data.found || data.kind !== 'consumable') {
photoDeviceWrap.classList.add('d-none');
if (photoModelText) {
photoModelText.textContent = 'Расходный материал не найден. Добавить?';
photoModelText.classList.remove('d-none');
}
if (photoStockText) {
photoStockText.textContent = '';
photoStockText.classList.add('d-none');
}
if (photoDevice) photoDevice.value = '';
togglePhotoAdd(true, barcode);
return;
}
if (photoModelText) {
photoModelText.textContent = `Модель: ${data.model || ''}`.trim();
photoModelText.classList.remove('d-none');
}
if (photoStockText) {
const qty = typeof data.quantity === 'number' ? data.quantity : '';
photoStockText.textContent = qty !== '' ? `Остаток: ${qty}` : '';
photoStockText.classList.toggle('d-none', qty === '');
}
photoDeviceWrap.classList.remove('d-none');
togglePhotoAdd(false);
loadConsumableDevices(data.barcode || barcode);
} catch (e) {
photoDeviceWrap.classList.add('d-none');
if (photoModelText) {
photoModelText.textContent = 'Ошибка сети';
photoModelText.classList.remove('d-none');
}
if (photoStockText) {
photoStockText.textContent = '';
photoStockText.classList.add('d-none');
}
togglePhotoAdd(false);
}
}
if (photoBarcode) {
let photoTimer = null;
photoBarcode.addEventListener('input', () => {
if (photoTimer) window.clearTimeout(photoTimer);
photoTimer = window.setTimeout(lookupPhotoBarcode, 120);
});
photoBarcode.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
lookupPhotoBarcode();
}
});
}
if (photoAddTypeSelect && photoAddTypeCustom) {
photoAddTypeSelect.addEventListener('change', syncPhotoAddType);
syncPhotoAddType();
}
function setPrintersInfoVisible(visible) {
const action = visible ? 'remove' : 'add';
printersInfoInv.classList[action]('d-none');
printersInfoBrand.classList[action]('d-none');
printersInfoSerial.classList[action]('d-none');
printersInfoDate.classList[action]('d-none');
printersInfoCabinet.classList[action]('d-none');
printersCabinetForm.classList[action]('d-none');
printersConsumablesWrap.classList[action]('d-none');
}
async function lookupPrintersInv() {
if (!printersInv) return;
const inv = (printersInv.value || '').trim();
if (!inv) {
setPrintersInfoVisible(false);
printersInfoInv.textContent = '';
printersInfoBrand.textContent = '';
printersInfoSerial.textContent = '';
printersInfoDate.textContent = '';
printersInfoCabinet.textContent = '';
printersConsumablesList.innerHTML = '';
if (printersId) printersId.value = '';
if (printersCabinetSelect) printersCabinetSelect.value = '';
return;
}
try {
const res = await fetch(`/api/barcode?barcode=${encodeURIComponent(inv)}`);
if (!res.ok) return;
const data = await res.json();
const isPrintDevice = data.kind === 'device' && (data.type === 'printer' || data.type === 'mfp');
if (!data.found || !isPrintDevice) {
printersInfoInv.textContent = '';
printersInfoBrand.textContent = 'МФУ/Принтер не найден';
printersInfoSerial.textContent = '';
printersInfoDate.textContent = '';
printersInfoCabinet.textContent = '';
printersConsumablesList.innerHTML = '';
if (printersId) printersId.value = '';
if (printersCabinetSelect) printersCabinetSelect.value = '';
setPrintersInfoVisible(true);
return;
}
const brandModel = [data.brand, data.model].filter(Boolean).join(' ');
printersInfoInv.textContent = `Инвентарный номер: ${data.inventory_number || inv}`;
printersInfoBrand.textContent = `Бренд/Модель: ${brandModel || ''}`.trim();
printersInfoSerial.textContent = `Серийный номер: ${data.serial_number || '—'}`;
if (data.date_in_operation) {
const parsed = new Date(data.date_in_operation);
const label = Number.isNaN(parsed.getTime())
? data.date_in_operation
: parsed.toLocaleDateString('ru-RU');
printersInfoDate.textContent = `Дата ввода в эксплуатацию: ${label}`;
} else {
printersInfoDate.textContent = 'Дата ввода в эксплуатацию: —';
}
printersInfoCabinet.textContent = `Расположение: ${data.cabinet_name || '—'}`;
printersConsumablesList.innerHTML = '';
const items = Array.isArray(data.consumables) ? data.consumables : [];
if (items.length) {
items.forEach((item) => {
const li = document.createElement('li');
const label = item.type ? `${item.type}: ` : '';
li.textContent = `${label}${item.model || ''}`.trim();
printersConsumablesList.appendChild(li);
});
} else {
const li = document.createElement('li');
li.textContent = 'Нет привязанных материалов';
printersConsumablesList.appendChild(li);
}
if (printersId) printersId.value = data.id || '';
if (printersCabinetSelect) printersCabinetSelect.value = data.cabinet_id || '';
if (printersCabinetForm) {
printersCabinetForm.action = data.kind === 'device' ? '/devices/update_cabinet' : '/computers/update_cabinet';
}
setPrintersInfoVisible(true);
} catch (e) {
// ignore
}
}
if (printersInv) {
let printersTimer = null;
printersInv.addEventListener('input', () => {
if (printersTimer) window.clearTimeout(printersTimer);
printersTimer = window.setTimeout(lookupPrintersInv, 160);
});
printersInv.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
lookupPrintersInv();
}
});
}
function setComputersInfoVisible(visible) {
const action = visible ? 'remove' : 'add';
computersInfoInv.classList[action]('d-none');
computersInfoBrand.classList[action]('d-none');
computersInfoSerial.classList[action]('d-none');
computersInfoDate.classList[action]('d-none');
computersInfoCabinet.classList[action]('d-none');
computersCabinetForm.classList[action]('d-none');
computersSpecsWrap.classList[action]('d-none');
}
function formatGB(value) {
const raw = (value || '').toString().trim();
if (!raw) return '—';
const lower = raw.toLowerCase();
if (lower.includes('gb') || lower.includes('гб')) return raw;
const num = raw.match(/\\d+/);
if (num) return `${num[0]} ГБ`;
return raw;
}
async function lookupComputersInv() {
if (!computersInv) return;
const inv = (computersInv.value || '').trim();
if (!inv) {
setComputersInfoVisible(false);
computersInfoInv.textContent = '';
computersInfoBrand.textContent = '';
computersInfoSerial.textContent = '';
computersInfoDate.textContent = '';
computersInfoCabinet.textContent = '';
computersSpecCpu.textContent = '';
computersSpecGpu.textContent = '';
computersSpecRam.textContent = '';
computersSpecStorage.textContent = '';
computersSpecOs.textContent = '';
computersSpecMotherboard.textContent = '';
computersSpecMotherboard.classList.add('d-none');
if (computersId) computersId.value = '';
if (computersCabinetSelect) computersCabinetSelect.value = '';
return;
}
try {
const res = await fetch(`/api/barcode?barcode=${encodeURIComponent(inv)}`);
if (!res.ok) return;
const data = await res.json();
if (!data.found || data.kind !== 'computer') {
computersInfoInv.textContent = '';
computersInfoBrand.textContent = 'Компьютер/Ноутбук не найден';
computersInfoSerial.textContent = '';
computersInfoDate.textContent = '';
computersInfoCabinet.textContent = '';
computersSpecCpu.textContent = '';
computersSpecGpu.textContent = '';
computersSpecRam.textContent = '';
computersSpecStorage.textContent = '';
computersSpecOs.textContent = '';
computersSpecMotherboard.textContent = '';
computersSpecMotherboard.classList.add('d-none');
if (computersId) computersId.value = '';
if (computersCabinetSelect) computersCabinetSelect.value = '';
setComputersInfoVisible(true);
return;
}
const brandModel = [data.brand, data.model].filter(Boolean).join(' ');
computersInfoInv.textContent = `Инвентарный номер: ${data.inventory_number || inv}`;
computersInfoBrand.textContent = `Бренд/Модель: ${brandModel || ''}`.trim();
computersInfoSerial.textContent = `Серийный номер: ${data.serial_number || '—'}`;
if (data.date_in_operation) {
const parsed = new Date(data.date_in_operation);
const label = Number.isNaN(parsed.getTime())
? data.date_in_operation
: parsed.toLocaleDateString('ru-RU');
computersInfoDate.textContent = `Дата ввода в эксплуатацию: ${label}`;
} else {
computersInfoDate.textContent = 'Дата ввода в эксплуатацию: —';
}
computersInfoCabinet.textContent = `Расположение: ${data.cabinet_name || '—'}`;
if (computersId) computersId.value = data.id || '';
if (computersCabinetSelect) computersCabinetSelect.value = data.cabinet_id || '';
if (computersCabinetForm) computersCabinetForm.action = '/computers/update_cabinet';
const cpuLabel = [data.cpu_brand, data.cpu_model].filter(Boolean).join(' ');
if (data.type === 'pc') {
computersSpecMotherboard.textContent = `Материнская плата: ${data.motherboard || '—'}`;
computersSpecMotherboard.classList.remove('d-none');
if (computersSpecsList && computersSpecMotherboard) {
if (computersSpecMotherboard.parentElement !== computersSpecsList) {
computersSpecsList.insertBefore(computersSpecMotherboard, computersSpecsList.firstElementChild);
} else if (computersSpecsList.firstElementChild !== computersSpecMotherboard) {
computersSpecsList.insertBefore(computersSpecMotherboard, computersSpecsList.firstElementChild);
}
}
} else {
computersSpecMotherboard.textContent = '';
computersSpecMotherboard.classList.add('d-none');
}
computersSpecCpu.textContent = `CPU: ${cpuLabel || '—'}`;
computersSpecGpu.textContent = `GPU: ${data.gpu_model || '—'}`;
const memoryLabel = [formatGB(data.memory_size), data.memory_type].filter(v => v && v !== '—').join(' ');
computersSpecRam.textContent = `RAM: ${memoryLabel || '—'}`;
computersSpecStorage.textContent = `Накопитель: ${formatGB(data.storage_size)}`;
computersSpecOs.textContent = `ОС: ${data.os || '—'}`;
setComputersInfoVisible(true);
} catch (e) {
// ignore
}
}
if (computersInv) {
let computersTimer = null;
computersInv.addEventListener('input', () => {
if (computersTimer) window.clearTimeout(computersTimer);
computersTimer = window.setTimeout(lookupComputersInv, 160);
});
computersInv.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
lookupComputersInv();
}
});
}
function setProjectorsInfoVisible(visible) {
const action = visible ? 'remove' : 'add';
projectorsInfoInv.classList[action]('d-none');
projectorsInfoType.classList[action]('d-none');
projectorsInfoBrand.classList[action]('d-none');
projectorsInfoSerial.classList[action]('d-none');
projectorsInfoDate.classList[action]('d-none');
projectorsInfoCabinet.classList[action]('d-none');
projectorsCabinetForm.classList[action]('d-none');
projectorsKitWrap.classList[action]('d-none');
}
function formatBrandModel(brand, model) {
return [brand, model].filter(Boolean).join(' ');
}
function clearCabinetResults() {
if (cabinetInfoTitle) {
cabinetInfoTitle.textContent = '';
cabinetInfoTitle.classList.add('d-none');
}
if (cabinetDevicesWrap) cabinetDevicesWrap.classList.add('d-none');
if (cabinetDevicesList) cabinetDevicesList.innerHTML = '';
if (cabinetComputersWrap) cabinetComputersWrap.classList.add('d-none');
if (cabinetComputersList) cabinetComputersList.innerHTML = '';
if (cabinetProjectorsWrap) cabinetProjectorsWrap.classList.add('d-none');
if (cabinetProjectorsList) cabinetProjectorsList.innerHTML = '';
if (cabinetDocsWrap) cabinetDocsWrap.classList.add('d-none');
if (cabinetDocsList) cabinetDocsList.innerHTML = '';
if (cabinetEmptyNote) cabinetEmptyNote.classList.add('d-none');
}
function formatDate(value) {
if (!value) return '—';
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) return value;
return parsed.toLocaleDateString('ru-RU');
}
function setSearchInfoVisible(visible) {
const action = visible ? 'remove' : 'add';
searchInfoInv.classList[action]('d-none');
searchInfoType.classList[action]('d-none');
searchInfoBrand.classList[action]('d-none');
searchInfoSerial.classList[action]('d-none');
searchInfoDate.classList[action]('d-none');
searchInfoCabinet.classList[action]('d-none');
}
function resetSearchExtras() {
if (searchConsumablesWrap) searchConsumablesWrap.classList.add('d-none');
if (searchConsumablesList) searchConsumablesList.innerHTML = '';
if (searchSpecsWrap) searchSpecsWrap.classList.add('d-none');
if (searchSpecCpu) searchSpecCpu.textContent = '';
if (searchSpecGpu) searchSpecGpu.textContent = '';
if (searchSpecRam) searchSpecRam.textContent = '';
if (searchSpecStorage) searchSpecStorage.textContent = '';
if (searchSpecOs) searchSpecOs.textContent = '';
if (searchSpecMotherboard) {
searchSpecMotherboard.textContent = '';
searchSpecMotherboard.classList.add('d-none');
}
if (searchKitWrap) searchKitWrap.classList.add('d-none');
if (searchKitProjectorTitle) searchKitProjectorTitle.textContent = '';
if (searchKitProjectorList) searchKitProjectorList.innerHTML = '';
if (searchKitBoardTitle) searchKitBoardTitle.textContent = '';
if (searchKitBoardList) searchKitBoardList.innerHTML = '';
if (searchKitComputerTitle) searchKitComputerTitle.textContent = '';
if (searchKitComputerList) searchKitComputerList.innerHTML = '';
}
async function lookupSearchInv() {
if (!searchInvInput) return;
const inv = (searchInvInput.value || '').trim();
if (!inv) {
setSearchInfoVisible(false);
if (searchInfoInv) searchInfoInv.textContent = '';
if (searchInfoType) searchInfoType.textContent = '';
if (searchInfoBrand) searchInfoBrand.textContent = '';
if (searchInfoSerial) searchInfoSerial.textContent = '';
if (searchInfoDate) searchInfoDate.textContent = '';
if (searchInfoCabinet) searchInfoCabinet.textContent = '';
resetSearchExtras();
return;
}
try {
const res = await fetch(`/api/barcode?barcode=${encodeURIComponent(inv)}`);
if (!res.ok) return;
const data = await res.json();
const allowedKinds = new Set(['device', 'computer', 'projector', 'board', 'document_camera']);
if (!data.found || !allowedKinds.has(data.kind)) {
if (searchInfoInv) searchInfoInv.textContent = 'Инвентарный номер не найден';
if (searchInfoType) searchInfoType.textContent = '';
if (searchInfoBrand) searchInfoBrand.textContent = '';
if (searchInfoSerial) searchInfoSerial.textContent = '';
if (searchInfoDate) searchInfoDate.textContent = '';
if (searchInfoCabinet) searchInfoCabinet.textContent = '';
resetSearchExtras();
searchInfoInv.classList.remove('d-none');
searchInfoType.classList.add('d-none');
searchInfoBrand.classList.add('d-none');
searchInfoSerial.classList.add('d-none');
searchInfoDate.classList.add('d-none');
searchInfoCabinet.classList.add('d-none');
return;
}
const kindLabelMap = {
device: 'Устройство',
computer: 'Компьютер/Ноутбук',
projector: 'Презентационное оборудование',
board: 'Доска',
document_camera: 'Документ-камера',
};
let typeLabel = kindLabelMap[data.kind] || 'Устройство';
if (data.kind === 'device') {
typeLabel = data.type_label || (data.type === 'printer' ? 'Принтер' : data.type === 'mfp' ? 'МФУ' : data.type || typeLabel);
} else if (data.kind === 'projector') {
typeLabel = data.kit_type_label || data.type_label || data.kit_type || typeLabel;
} else if (data.kind === 'board') {
typeLabel = data.type_label || typeLabel;
}
const modelValue = data.model || data.projector_model || data.board_model || '';
const brandModel = formatBrandModel(data.brand, modelValue);
const serialValue = data.serial_number || data.projector_serial || data.board_serial || '—';
searchInfoInv.textContent = `Инвентарный номер: ${data.inventory_number || inv}`;
searchInfoType.textContent = `Тип: ${typeLabel}`;
searchInfoBrand.textContent = `Бренд/Модель: ${brandModel || ''}`.trim();
searchInfoSerial.textContent = `Серийный номер: ${serialValue}`;
if (data.date_in_operation) {
const parsed = new Date(data.date_in_operation);
const label = Number.isNaN(parsed.getTime())
? data.date_in_operation
: parsed.toLocaleDateString('ru-RU');
searchInfoDate.textContent = `Дата ввода в эксплуатацию: ${label}`;
} else {
searchInfoDate.textContent = 'Дата ввода в эксплуатацию: —';
}
searchInfoCabinet.textContent = `Расположение: ${data.cabinet_name || '—'}`;
resetSearchExtras();
if (data.kind === 'device') {
const items = Array.isArray(data.consumables) ? data.consumables : [];
if (searchConsumablesWrap && searchConsumablesList) {
searchConsumablesWrap.classList.remove('d-none');
if (items.length) {
items.forEach((item) => {
const li = document.createElement('li');
const label = item.type ? `${item.type}: ` : '';
li.textContent = `${label}${item.model || ''}`.trim();
searchConsumablesList.appendChild(li);
});
} else {
const li = document.createElement('li');
li.textContent = 'Нет привязанных материалов';
searchConsumablesList.appendChild(li);
}
}
}
if (data.kind === 'computer') {
const cpuLabel = [data.cpu_brand, data.cpu_model].filter(Boolean).join(' ');
if (searchSpecMotherboard) {
if (data.type === 'pc') {
searchSpecMotherboard.textContent = `Материнская плата: ${data.motherboard || '—'}`;
searchSpecMotherboard.classList.remove('d-none');
} else {
searchSpecMotherboard.textContent = '';
searchSpecMotherboard.classList.add('d-none');
}
}
if (searchSpecCpu) searchSpecCpu.textContent = `CPU: ${cpuLabel || '—'}`;
if (searchSpecGpu) searchSpecGpu.textContent = `GPU: ${data.gpu_model || '—'}`;
const memoryLabel = [formatGB(data.memory_size), data.memory_type].filter(v => v && v !== '—').join(' ');
if (searchSpecRam) searchSpecRam.textContent = `RAM: ${memoryLabel || '—'}`;
if (searchSpecStorage) searchSpecStorage.textContent = `Накопитель: ${formatGB(data.storage_size)}`;
if (searchSpecOs) searchSpecOs.textContent = `ОС: ${data.os || '—'}`;
if (searchSpecsWrap) searchSpecsWrap.classList.remove('d-none');
}
const showKit = data.kind === 'projector'
&& data.kit_type === 'kit'
&& data.inventory_number === inv;
if (showKit && searchKitWrap) {
const projModel = formatBrandModel(data.projector_brand || data.brand, data.projector_model);
const boardModel = formatBrandModel(data.board_brand || data.brand, data.board_model);
const compModel = formatBrandModel(data.computer_brand, data.computer_model);
searchKitWrap.classList.remove('d-none');
if (searchKitProjectorTitle) searchKitProjectorTitle.textContent = 'Проектор:';
if (searchKitProjectorList) {
searchKitProjectorList.innerHTML = `
<li class="home-modal__status">Инвентарный номер: ${data.projector_inventory_number || '—'}</li>
<li class="home-modal__status">Бренд/Модель: ${projModel || '—'}</li>
<li class="home-modal__status">Серийный номер: ${data.projector_serial || '—'}</li>
`;
}
if (searchKitBoardTitle) searchKitBoardTitle.textContent = 'Доска:';
if (searchKitBoardList) {
searchKitBoardList.innerHTML = `
<li class="home-modal__status">Инвентарный номер: ${data.board_inventory_number || '—'}</li>
<li class="home-modal__status">Бренд/Модель: ${boardModel || '—'}</li>
<li class="home-modal__status">Серийный номер: ${data.board_serial || '—'}</li>
`;
}
if (searchKitComputerTitle) searchKitComputerTitle.textContent = 'Компьютер:';
if (searchKitComputerList) {
searchKitComputerList.innerHTML = `
<li class="home-modal__status">Инвентарный номер: ${data.computer_inventory_number || '—'}</li>
<li class="home-modal__status">Бренд/Модель: ${compModel || '—'}</li>
<li class="home-modal__status">Серийный номер: ${data.computer_serial || '—'}</li>
`;
}
}
setSearchInfoVisible(true);
} catch (e) {
// ignore
}
}
function appendListItem(list, lines) {
const li = document.createElement('li');
li.className = 'home-modal__status';
li.innerHTML = lines.map(line => `<div>${line}</div>`).join('');
list.appendChild(li);
}
async function lookupCabinetInfo() {
if (!searchCabinetViewSelect) return;
const cabId = (searchCabinetViewSelect.value || '').trim();
if (!cabId) {
clearCabinetResults();
return;
}
try {
const res = await fetch(`/api/cabinet_info?cabinet_id=${encodeURIComponent(cabId)}`);
if (!res.ok) return;
const data = await res.json();
clearCabinetResults();
if (cabinetInfoTitle) {
const title = data.cabinet_name ? `Кабинет: ${data.cabinet_name}` : 'Кабинет';
cabinetInfoTitle.textContent = title;
cabinetInfoTitle.classList.remove('d-none');
}
const devices = Array.isArray(data.devices) ? data.devices : [];
if (devices.length && cabinetDevicesWrap && cabinetDevicesList) {
devices.forEach((item) => {
const brandModel = formatBrandModel(item.brand, item.model);
const typeLabel = item.type_label || item.type || 'Устройство';
const consumables = Array.isArray(item.consumables) ? item.consumables : [];
const consLabel = consumables.length
? consumables.map(c => (c.type ? `${c.type}: ${c.model || ''}` : c.model || '')).filter(Boolean).join('; ')
: 'Нет привязанных материалов';
appendListItem(cabinetDevicesList, [
`Инв. №: ${item.inventory_number || '—'}`,
`Тип: ${typeLabel}`,
`Бренд/Модель: ${brandModel || '—'}`,
`Серийный №: ${item.serial_number || '—'}`,
`Дата ввода: ${formatDate(item.date_in_operation)}`,
`Расходные материалы: ${consLabel}`,
]);
});
cabinetDevicesWrap.classList.remove('d-none');
}
const computers = Array.isArray(data.computers) ? data.computers : [];
if (computers.length && cabinetComputersWrap && cabinetComputersList) {
computers.forEach((item) => {
const brandModel = formatBrandModel(item.brand, item.model);
const cpuLabel = [item.cpu_brand, item.cpu_model].filter(Boolean).join(' ');
const memoryLabel = [formatGB(item.memory_size), item.memory_type].filter(v => v && v !== '—').join(' ');
const lines = [
`Инв. №: ${item.inventory_number || '—'}`,
`Тип: ${item.type_label || item.type || '—'}`,
`Бренд/Модель: ${brandModel || '—'}`,
`Серийный №: ${item.serial_number || '—'}`,
`CPU: ${cpuLabel || '—'}`,
`GPU: ${item.gpu_model || '—'}`,
`RAM: ${memoryLabel || '—'}`,
`Накопитель: ${formatGB(item.storage_size)}`,
`ОС: ${item.os || '—'}`,
];
if (item.type === 'pc') {
lines.push(`Материнская плата: ${item.motherboard || '—'}`);
}
appendListItem(cabinetComputersList, lines);
});
cabinetComputersWrap.classList.remove('d-none');
}
const projectors = Array.isArray(data.projectors) ? data.projectors : [];
if (projectors.length && cabinetProjectorsWrap && cabinetProjectorsList) {
projectors.forEach((item) => {
const typeLabel = item.kit_type_label || item.kit_type || 'Проектор';
const projModel = formatBrandModel(item.projector_brand || item.brand, item.projector_model);
const boardModel = formatBrandModel(item.board_brand || item.brand, item.board_model);
const compModel = formatBrandModel(item.computer_brand, item.computer_model);
const lines = [
`Инв. №: ${item.inventory_number || '—'}`,
`Тип: ${typeLabel}`,
`Проектор: ${projModel || '—'}`,
`Серийный № проектора: ${item.projector_serial || '—'}`,
];
if (item.projector_inventory_number) {
lines.push(`Инв. № проектора: ${item.projector_inventory_number}`);
}
if (item.board_model || item.board_serial || item.board_inventory_number) {
lines.push(`Доска: ${boardModel || '—'}`);
lines.push(`Серийный № доски: ${item.board_serial || '—'}`);
if (item.board_inventory_number) {
lines.push(`Инв. № доски: ${item.board_inventory_number}`);
}
}
if (item.computer_inventory_number || compModel) {
lines.push(`Компьютер: ${compModel || '—'}`);
if (item.computer_inventory_number) {
lines.push(`Инв. № компьютера: ${item.computer_inventory_number}`);
}
}
appendListItem(cabinetProjectorsList, lines);
});
cabinetProjectorsWrap.classList.remove('d-none');
}
const docs = Array.isArray(data.document_cameras) ? data.document_cameras : [];
if (docs.length && cabinetDocsWrap && cabinetDocsList) {
docs.forEach((item) => {
const brandModel = formatBrandModel(item.brand, item.model);
appendListItem(cabinetDocsList, [
`Инв. №: ${item.inventory_number || '—'}`,
`Бренд/Модель: ${brandModel || '—'}`,
`Серийный №: ${item.serial_number || '—'}`,
`Дата ввода: ${formatDate(item.date_in_operation)}`,
]);
});
cabinetDocsWrap.classList.remove('d-none');
}
const totalItems = devices.length + computers.length + projectors.length + docs.length;
if (!totalItems && cabinetEmptyNote) {
cabinetEmptyNote.classList.remove('d-none');
}
} catch (e) {
// ignore
}
}
async function lookupProjectorsInv() {
if (!projectorsInv) return;
const inv = (projectorsInv.value || '').trim();
if (!inv) {
setProjectorsInfoVisible(false);
projectorsInfoInv.textContent = '';
projectorsInfoType.textContent = '';
projectorsInfoBrand.textContent = '';
projectorsInfoSerial.textContent = '';
projectorsInfoDate.textContent = '';
projectorsInfoCabinet.textContent = '';
projectorsInfoInv.classList.add('d-none');
kitProjectorTitle.textContent = '';
kitProjectorList.innerHTML = '';
kitBoardTitle.textContent = '';
kitBoardList.innerHTML = '';
kitComputerTitle.textContent = '';
kitComputerList.innerHTML = '';
if (projectorsId) projectorsId.value = '';
if (projectorsCabinetSelect) projectorsCabinetSelect.value = '';
return;
}
try {
const res = await fetch(`/api/barcode?barcode=${encodeURIComponent(inv)}`);
if (!res.ok) return;
const data = await res.json();
const isProjectorKind = data.kind === 'projector' || data.kind === 'board';
if (!data.found || !isProjectorKind) {
projectorsInfoInv.textContent = 'Инвентарный номер не найден';
projectorsInfoInv.classList.remove('d-none');
projectorsInfoType.textContent = '';
projectorsInfoBrand.textContent = '';
projectorsInfoSerial.textContent = '';
projectorsInfoDate.textContent = '';
projectorsInfoCabinet.textContent = '';
kitProjectorTitle.textContent = '';
kitProjectorList.innerHTML = '';
kitBoardTitle.textContent = '';
kitBoardList.innerHTML = '';
kitComputerTitle.textContent = '';
kitComputerList.innerHTML = '';
if (projectorsId) projectorsId.value = '';
if (projectorsCabinetSelect) projectorsCabinetSelect.value = '';
setProjectorsInfoVisible(true);
return;
}
projectorsInfoInv.textContent = `Инвентарный номер: ${data.inventory_number || inv}`;
projectorsInfoInv.classList.remove('d-none');
const typeLabel = data.kind === 'board'
? (data.type_label || 'Доска')
: (data.kit_type_label || data.type_label || data.kit_type || 'Проектор');
projectorsInfoType.textContent = `Тип: ${typeLabel}`;
const brandModel = formatBrandModel(data.brand, data.model || data.projector_model || data.board_model);
projectorsInfoBrand.textContent = `Бренд/Модель: ${brandModel || ''}`.trim();
projectorsInfoSerial.textContent = `Серийный номер: ${data.serial_number || data.projector_serial || data.board_serial || '—'}`;
if (data.date_in_operation) {
const parsed = new Date(data.date_in_operation);
const label = Number.isNaN(parsed.getTime())
? data.date_in_operation
: parsed.toLocaleDateString('ru-RU');
projectorsInfoDate.textContent = `Дата ввода в эксплуатацию: ${label}`;
} else {
projectorsInfoDate.textContent = 'Дата ввода в эксплуатацию: —';
}
projectorsInfoCabinet.textContent = `Кабинет: ${data.cabinet_name || '—'}`;
if (projectorsId) projectorsId.value = data.id || '';
if (projectorsCabinetSelect) projectorsCabinetSelect.value = data.cabinet_id || '';
if (projectorsCabinetForm) projectorsCabinetForm.action = '/projectors/update_cabinet';
const showKit = data.kind === 'projector'
&& data.kit_type === 'kit'
&& data.inventory_number === inv;
if (showKit) {
projectorsKitWrap.classList.remove('d-none');
const projModel = formatBrandModel(data.projector_brand || data.brand, data.projector_model);
const boardModel = formatBrandModel(data.board_brand || data.brand, data.board_model);
const compModel = formatBrandModel(data.computer_brand, data.computer_model);
kitProjectorTitle.textContent = 'Проектор:';
kitProjectorList.innerHTML = '';
kitProjectorList.innerHTML = `
<li class="home-modal__status">Инвентарный номер: ${data.projector_inventory_number || '—'}</li>
<li class="home-modal__status">Бренд/Модель: ${projModel || '—'}</li>
<li class="home-modal__status">Серийный номер: ${data.projector_serial || '—'}</li>
`;
kitBoardTitle.textContent = 'Доска:';
kitBoardList.innerHTML = `
<li class="home-modal__status">Инвентарный номер: ${data.board_inventory_number || '—'}</li>
<li class="home-modal__status">Бренд/Модель: ${boardModel || '—'}</li>
<li class="home-modal__status">Серийный номер: ${data.board_serial || '—'}</li>
`;
kitComputerTitle.textContent = 'Компьютер:';
kitComputerList.innerHTML = `
<li class="home-modal__status">Инвентарный номер: ${data.computer_inventory_number || '—'}</li>
<li class="home-modal__status">Бренд/Модель: ${compModel || '—'}</li>
<li class="home-modal__status">Серийный номер: ${data.computer_serial || '—'}</li>
`;
} else {
projectorsKitWrap.classList.add('d-none');
kitProjectorTitle.textContent = '';
kitProjectorList.innerHTML = '';
kitBoardTitle.textContent = '';
kitBoardList.innerHTML = '';
kitComputerTitle.textContent = '';
kitComputerList.innerHTML = '';
}
setProjectorsInfoVisible(true);
} catch (e) {
// ignore
}
}
if (projectorsInv) {
let projectorsTimer = null;
projectorsInv.addEventListener('input', () => {
if (projectorsTimer) window.clearTimeout(projectorsTimer);
projectorsTimer = window.setTimeout(lookupProjectorsInv, 160);
});
projectorsInv.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
lookupProjectorsInv();
}
});
}
function setDocCameraInfoVisible(visible) {
const action = visible ? 'remove' : 'add';
docCameraInfoInv.classList[action]('d-none');
docCameraInfoBrand.classList[action]('d-none');
docCameraInfoSerial.classList[action]('d-none');
docCameraInfoDate.classList[action]('d-none');
docCameraInfoCabinet.classList[action]('d-none');
docCameraCabinetForm.classList[action]('d-none');
}
async function lookupDocCameraInv() {
if (!docCameraInv) return;
const inv = (docCameraInv.value || '').trim();
if (!inv) {
setDocCameraInfoVisible(false);
docCameraInfoInv.textContent = '';
docCameraInfoBrand.textContent = '';
docCameraInfoSerial.textContent = '';
docCameraInfoDate.textContent = '';
docCameraInfoCabinet.textContent = '';
if (docCameraId) docCameraId.value = '';
if (docCameraCabinetSelect) docCameraCabinetSelect.value = '';
return;
}
try {
const res = await fetch(`/api/barcode?barcode=${encodeURIComponent(inv)}`);
if (!res.ok) return;
const data = await res.json();
if (!data.found || data.kind !== 'document_camera') {
docCameraInfoInv.textContent = 'Документ-камера не найдена';
docCameraInfoBrand.textContent = '';
docCameraInfoSerial.textContent = '';
docCameraInfoDate.textContent = '';
docCameraInfoCabinet.textContent = '';
if (docCameraId) docCameraId.value = '';
if (docCameraCabinetSelect) docCameraCabinetSelect.value = '';
setDocCameraInfoVisible(true);
return;
}
const brandModel = [data.brand, data.model].filter(Boolean).join(' ');
docCameraInfoInv.textContent = `Инвентарный номер: ${data.inventory_number || inv}`;
docCameraInfoBrand.textContent = `Бренд/Модель: ${brandModel || ''}`.trim();
docCameraInfoSerial.textContent = `Серийный номер: ${data.serial_number || '—'}`;
if (data.date_in_operation) {
const parsed = new Date(data.date_in_operation);
const label = Number.isNaN(parsed.getTime())
? data.date_in_operation
: parsed.toLocaleDateString('ru-RU');
docCameraInfoDate.textContent = `Дата ввода в эксплуатацию: ${label}`;
} else {
docCameraInfoDate.textContent = 'Дата ввода в эксплуатацию: —';
}
docCameraInfoCabinet.textContent = `Кабинет: ${data.cabinet_name || '—'}`;
if (docCameraId) docCameraId.value = data.id || '';
if (docCameraCabinetSelect) docCameraCabinetSelect.value = data.cabinet_id || '';
if (docCameraCabinetForm) docCameraCabinetForm.action = '/document_cameras/update_cabinet';
setDocCameraInfoVisible(true);
} catch (e) {
// ignore
}
}
if (docCameraInv) {
let docTimer = null;
docCameraInv.addEventListener('input', () => {
if (docTimer) window.clearTimeout(docTimer);
docTimer = window.setTimeout(lookupDocCameraInv, 160);
});
docCameraInv.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
lookupDocCameraInv();
}
});
}
if (searchCabinetViewSelect) {
searchCabinetViewSelect.addEventListener('change', lookupCabinetInfo);
}
if (searchInvInput) {
let searchTimer = null;
searchInvInput.addEventListener('input', () => {
if (searchTimer) window.clearTimeout(searchTimer);
searchTimer = window.setTimeout(lookupSearchInv, 160);
});
searchInvInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
lookupSearchInv();
}
});
}
function syncBrand(selectEl, customEl, valueEl) {
if (!selectEl || !customEl || !valueEl) return;
const sel = selectEl.value;
if (sel === '__custom__') {
customEl.classList.remove('d-none');
customEl.disabled = false;
valueEl.value = customEl.value || '';
} else {
customEl.classList.add('d-none');
customEl.disabled = true;
customEl.value = '';
valueEl.value = sel;
}
}
function attachBrandSync(selectEl, customEl, valueEl) {
if (!selectEl || !customEl || !valueEl) return;
selectEl.addEventListener('change', () => syncBrand(selectEl, customEl, valueEl));
customEl.addEventListener('input', () => syncBrand(selectEl, customEl, valueEl));
syncBrand(selectEl, customEl, valueEl);
}
attachBrandSync(homeDeviceBrandSelect, homeDeviceBrandCustom, homeDeviceBrandValue);
attachBrandSync(homeComputerBrandSelect, homeComputerBrandCustom, homeComputerBrandValue);
attachBrandSync(homeProjectorBrandSelect, homeProjectorBrandCustom, homeProjectorBrandValue);
attachBrandSync(homeDocBrandSelect, homeDocBrandCustom, homeDocBrandValue);
if (homeProjectorComputerSelect && homeProjectorComputerInv) {
const syncFromSelect = () => {
const opt = homeProjectorComputerSelect.options[homeProjectorComputerSelect.selectedIndex];
const inv = opt ? (opt.getAttribute('data-inv') || '') : '';
homeProjectorComputerInv.value = inv;
};
const syncFromInv = () => {
const inv = (homeProjectorComputerInv.value || '').trim();
if (!inv) return;
const opts = Array.from(homeProjectorComputerSelect.options);
const match = opts.find(o => (o.getAttribute('data-inv') || '') === inv);
if (match) {
homeProjectorComputerSelect.value = match.value;
}
};
homeProjectorComputerSelect.addEventListener('change', syncFromSelect);
homeProjectorComputerInv.addEventListener('input', syncFromInv);
syncFromSelect();
}
if (addDeviceType && addDeviceForms.length) {
const showForm = (value) => {
addDeviceForms.forEach((form) => {
form.classList.toggle('d-none', form.getAttribute('data-add-type') !== value);
});
};
addDeviceType.addEventListener('change', () => showForm(addDeviceType.value));
}
if (homeComputerSpecsToggle && homeComputerSpecs) {
homeComputerSpecsToggle.addEventListener('click', () => {
homeComputerSpecs.classList.toggle('d-none');
});
}
const homeProjectorKitType = document.getElementById('homeProjectorKitType');
if (homeProjectorKitType) {
const projectorForm = homeProjectorKitType.closest('form');
const brandField = document.getElementById('homeProjectorBrandField');
const brandRowKit = document.getElementById('homeProjectorBrandRowKit');
const brandRowMain = document.getElementById('homeProjectorBrandRowMain');
const projectorModelLabelEl = document.getElementById('homeProjectorModelLabel');
const projectorSerialLabelEl = document.getElementById('homeProjectorSerialLabel');
const applyKitType = () => {
const val = homeProjectorKitType.value;
if (brandField && brandRowKit && brandRowMain) {
if (val === 'kit') {
if (brandField.parentElement !== brandRowKit) {
brandRowKit.appendChild(brandField);
}
} else {
if (brandField.parentElement !== brandRowMain) {
brandRowMain.insertBefore(brandField, brandRowMain.firstChild);
}
}
}
const fields = projectorForm.querySelectorAll('[data-kit-types]');
fields.forEach((field) => {
const types = (field.dataset.kitTypes || '').split(',').map(t => t.trim()).filter(Boolean);
const show = !!val && (!types.length || types.includes(val));
field.classList.toggle('d-none', !show);
field.querySelectorAll('input, select, textarea, button').forEach((el) => {
el.disabled = !show;
});
});
if (projectorModelLabelEl && projectorSerialLabelEl) {
if (val === 'display') {
projectorModelLabelEl.textContent = 'Модель интерактивного экрана';
projectorSerialLabelEl.textContent = 'Серийный № интерактивного экрана';
} else if (val === 'tv') {
projectorModelLabelEl.textContent = 'Модель телевизора';
projectorSerialLabelEl.textContent = 'Серийный № телевизора';
} else if (val === 'board') {
projectorModelLabelEl.textContent = 'Модель доски';
projectorSerialLabelEl.textContent = 'Серийный № доски';
} else {
projectorModelLabelEl.textContent = 'Модель проектора';
projectorSerialLabelEl.textContent = 'Серийный № проектора';
}
}
};
homeProjectorKitType.addEventListener('change', applyKitType);
applyKitType();
}
const factEl = document.getElementById('homeFactText');
if (factEl) {
const facts = [
'Первый жёсткий диск IBM 350 в 1956 году вмещал всего 5 МБ и весил почти тонну.',
'Лазерная печать появилась в 1970-х: технология Xerox стала основой современных лазерных принтеров.',
'Термин «баг» в программировании закрепился после реального мотылька в реле компьютера Mark II.',
'В первых принтерах использовались ударные механизмы, похожие на печатные машинки.',
'Первые ЭВМ занимали целые комнаты, а сегодня мощности смартфона хватает для задач того уровня.',
'Матричные принтеры до сих пор ценят за возможность печати на копиях и самокопирующихся бланках.',
'Первая коммерческая мышь была деревянной и имела два колеса.',
'Твердотельные накопители (SSD) работают без механики, поэтому быстрее и тише HDD.',
'Цветная печать требует точного совмещения слоёв, поэтому принтеры регулярно выполняют калибровку.',
'Первые графические интерфейсы вдохновили современные окна и меню, которые мы используем каждый день.',
'Скорость печати часто измеряют в страницах в минуту (PPM), но качество зависит и от режима печати.',
'Картриджи с тонером используют порошок, который закрепляется на бумаге при нагреве.',
'Первая версия USB появилась в 1996 году и резко упростила подключение периферии.',
'Оптические сенсоры мыши вытеснили шариковые благодаря большей точности и надёжности.',
'Лазерный принтер формирует изображение электростатикой на барабане и переносит тонер на бумагу.',
'Сканеры CIS тоньше и дешевле CCD, но обычно хуже передают глубину и цвет.',
'Чем выше DPI, тем больше деталей принтер может воспроизвести, но это не всегда улучшает текст.',
'Жизненный цикл картриджа зависит от покрытия страницы: 5% заполнение — стандарт отрасли.',
'Термо-принтеры печатают без чернил — изображение появляется на термобумаге.',
'Жёсткие диски HDD хранят данные на вращающихся пластинах с магнитным покрытием.',
'Сетевые принтеры часто используют протоколы IPP и LPR/LPD для печати по сети.',
'Первая коммерческая ЭВМ UNIVAC I поставлялась в 1951 году и весила более 7 тонн.',
'Память DDR повышает скорость, передавая данные дважды за такт.',
'Современные МФУ объединяют печать, сканирование и копирование в одном устройстве.',
'Видеопроцессор (GPU) ускоряет работу графики и параллельных вычислений.',
'Для увеличения ресурса лазерного картриджа важно использовать качественную бумагу.',
'Шум от вентиляторов — один из основных источников звука в системном блоке.',
'Печать в черновом режиме может снизить расход тонера.',
'USB-C может передавать данные, видео и питание через один кабель.',
'В первых ноутбуках аккумуляторы обеспечивали работу всего на 12 часа.',
'Механические клавиатуры ценят за ресурс и чёткую тактильную отдачу.',
];
let idx = Math.floor(Math.random() * facts.length);
const showFact = () => {
factEl.textContent = facts[idx % facts.length];
idx += 1;
};
showFact();
window.setInterval(showFact, 5 * 60 * 1000);
}
function noteRequest(url, payload) {
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload || {}),
});
}
function collectNotePayload(noteEl) {
const textarea = noteEl.querySelector('.home-user-note__text');
return {
text: textarea ? textarea.value : '',
x: Math.round(parseFloat(noteEl.style.left) || 0),
y: Math.round(parseFloat(noteEl.style.top) || 0),
pinned: noteEl.classList.contains('is-pinned'),
z: parseInt(noteEl.style.zIndex || '1', 10) || 1,
};
}
function ensureNoteInViewport(noteEl) {
const width = noteEl.offsetWidth || 240;
const height = noteEl.offsetHeight || 170;
const maxLeft = Math.max(0, window.innerWidth - width);
const maxTop = Math.max(44, window.innerHeight - height);
const left = parseFloat(noteEl.style.left) || 8;
const top = parseFloat(noteEl.style.top) || 90;
noteEl.style.left = `${Math.max(0, Math.min(maxLeft, left))}px`;
noteEl.style.top = `${Math.max(44, Math.min(maxTop, top))}px`;
}
function applyNotePinnedState(noteEl, pinned) {
const pinBtn = noteEl.querySelector('.home-user-note__pin');
noteEl.classList.toggle('is-pinned', !!pinned);
if (pinBtn) {
pinBtn.classList.toggle('is-active', !!pinned);
pinBtn.setAttribute('aria-pressed', pinned ? 'true' : 'false');
pinBtn.title = pinned ? '\u041E\u0442\u043A\u0440\u0435\u043F\u0438\u0442\u044C' : '\u0417\u0430\u043A\u0440\u0435\u043F\u0438\u0442\u044C';
}
}
function refreshDeleteButtonsState() {
if (!homeNotesLayer) return;
const notes = Array.from(homeNotesLayer.querySelectorAll('.home-user-note'));
const oneNoteLeft = notes.length <= 1;
notes.forEach((noteEl) => {
const delBtn = noteEl.querySelector('.home-user-note__delete');
if (!delBtn) return;
delBtn.disabled = oneNoteLeft;
delBtn.title = oneNoteLeft ? '\u041D\u0435\u043B\u044C\u0437\u044F \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0435\u0434\u0438\u043D\u0441\u0442\u0432\u0435\u043D\u043D\u0443\u044E \u0437\u0430\u043C\u0435\u0442\u043A\u0443' : '\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0437\u0430\u043C\u0435\u0442\u043A\u0443';
});
}
function bringNoteToFront(noteEl) {
homeNotesMaxZ += 1;
noteEl.style.zIndex = String(homeNotesMaxZ);
}
async function createNoteAndMount(focusText) {
try {
const res = await noteRequest('/api/home_notes/create', {});
if (!res.ok) return null;
const data = await res.json();
if (!data || !data.ok || !data.note) return null;
const noteEl = mountNote(data.note);
if (noteEl) {
bringNoteToFront(noteEl);
refreshDeleteButtonsState();
if (focusText) {
const textarea = noteEl.querySelector('.home-user-note__text');
if (textarea) textarea.focus();
}
}
return noteEl;
} catch (e) {
return null;
}
}
const noteSaveTimers = new Map();
function scheduleNoteSave(noteEl, delayMs) {
const id = noteEl.getAttribute('data-note-id');
if (!id) return;
const prev = noteSaveTimers.get(id);
if (prev) window.clearTimeout(prev);
const timerId = window.setTimeout(async () => {
noteSaveTimers.delete(id);
const payload = collectNotePayload(noteEl);
try {
await noteRequest(`/api/home_notes/${encodeURIComponent(id)}/update`, payload);
} catch (e) {
// ignore network errors for now
}
}, delayMs);
noteSaveTimers.set(id, timerId);
}
function mountNote(note) {
if (!homeNotesLayer) return null;
const noteEl = document.createElement('div');
noteEl.className = 'home-user-note';
noteEl.setAttribute('data-note-id', String(note.id));
noteEl.style.left = `${Number(note.x) || 8}px`;
noteEl.style.top = `${Number(note.y) || 90}px`;
noteEl.style.zIndex = String(Number(note.z) || 1);
homeNotesMaxZ = Math.max(homeNotesMaxZ, Number(note.z) || 1);
const head = document.createElement('div');
head.className = 'home-user-note__head';
const actions = document.createElement('div');
actions.className = 'home-user-note__head-actions';
const addBtn = document.createElement('button');
addBtn.type = 'button';
addBtn.className = 'home-user-note__add';
addBtn.textContent = '+';
addBtn.setAttribute('aria-label', '\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0437\u0430\u043C\u0435\u0442\u043A\u0443');
addBtn.title = '\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0437\u0430\u043C\u0435\u0442\u043A\u0443';
const pinBtn = document.createElement('button');
pinBtn.type = 'button';
pinBtn.className = 'home-user-note__pin';
pinBtn.textContent = '\uD83D\uDCCC';
pinBtn.setAttribute('aria-label', '\u0417\u0430\u043A\u0440\u0435\u043F\u0438\u0442\u044C \u0437\u0430\u043C\u0435\u0442\u043A\u0443');
pinBtn.title = '\u0417\u0430\u043A\u0440\u0435\u043F\u0438\u0442\u044C';
actions.appendChild(addBtn);
actions.appendChild(pinBtn);
head.appendChild(actions);
const textarea = document.createElement('textarea');
textarea.className = 'home-user-note__text';
textarea.value = note.text || '';
textarea.placeholder = '\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442...';
const deleteBtn = document.createElement('button');
deleteBtn.type = 'button';
deleteBtn.className = 'home-user-note__delete';
deleteBtn.textContent = '\u00D7';
deleteBtn.setAttribute('aria-label', '\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0437\u0430\u043C\u0435\u0442\u043A\u0443');
noteEl.appendChild(head);
noteEl.appendChild(textarea);
noteEl.appendChild(deleteBtn);
applyNotePinnedState(noteEl, !!note.pinned);
homeNotesLayer.appendChild(noteEl);
ensureNoteInViewport(noteEl);
noteEl.addEventListener('mousedown', () => {
bringNoteToFront(noteEl);
scheduleNoteSave(noteEl, 120);
});
addBtn.addEventListener('click', async () => {
await createNoteAndMount(true);
});
deleteBtn.addEventListener('click', async () => {
if (deleteBtn.disabled) return;
const id = noteEl.getAttribute('data-note-id');
if (!id) return;
try {
const res = await noteRequest(`/api/home_notes/${encodeURIComponent(id)}/delete`, {});
if (!res.ok) return;
const data = await res.json();
if (!data || !data.ok) return;
noteEl.remove();
refreshDeleteButtonsState();
} catch (e) {
// ignore delete errors
}
});
pinBtn.addEventListener('click', () => {
const nextPinned = !noteEl.classList.contains('is-pinned');
applyNotePinnedState(noteEl, nextPinned);
scheduleNoteSave(noteEl, 80);
});
textarea.addEventListener('input', () => scheduleNoteSave(noteEl, 350));
textarea.addEventListener('blur', () => scheduleNoteSave(noteEl, 40));
let isDragging = false;
let dragStartX = 0;
let dragStartY = 0;
let startLeft = 0;
let startTop = 0;
let pointerId = null;
const onPointerMove = (e) => {
if (!isDragging) return;
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
noteEl.style.left = `${startLeft + dx}px`;
noteEl.style.top = `${startTop + dy}px`;
ensureNoteInViewport(noteEl);
};
const onPointerUp = () => {
if (!isDragging) return;
isDragging = false;
if (pointerId !== null && head.releasePointerCapture) {
try { head.releasePointerCapture(pointerId); } catch (e) { /* ignore */ }
}
head.removeEventListener('pointermove', onPointerMove);
head.removeEventListener('pointerup', onPointerUp);
head.removeEventListener('pointercancel', onPointerUp);
scheduleNoteSave(noteEl, 30);
};
head.addEventListener('pointerdown', (e) => {
if (e.target === pinBtn || e.target === addBtn) return;
if (noteEl.classList.contains('is-pinned')) return;
isDragging = true;
pointerId = e.pointerId;
dragStartX = e.clientX;
dragStartY = e.clientY;
startLeft = parseFloat(noteEl.style.left) || 0;
startTop = parseFloat(noteEl.style.top) || 0;
bringNoteToFront(noteEl);
if (head.setPointerCapture) {
try { head.setPointerCapture(pointerId); } catch (err) { /* ignore */ }
}
head.addEventListener('pointermove', onPointerMove);
head.addEventListener('pointerup', onPointerUp);
head.addEventListener('pointercancel', onPointerUp);
});
return noteEl;
}
if (homeNotesLayer) {
initialHomeNotes.forEach((note) => mountNote(note));
refreshDeleteButtonsState();
window.addEventListener('resize', () => {
homeNotesLayer.querySelectorAll('.home-user-note').forEach((noteEl) => ensureNoteInViewport(noteEl));
});
}
function attachDrag(modal, titlebar, closeBtn) {
if (!modal || !titlebar) return;
let isDragging = false;
let startX = 0;
let startY = 0;
let startLeft = 0;
let startTop = 0;
const onMove = (e) => {
if (!isDragging) return;
const nextLeft = startLeft + (e.clientX - startX);
const nextTop = startTop + (e.clientY - startY);
modal.style.left = `${nextLeft}px`;
modal.style.top = `${nextTop}px`;
};
const onUp = () => {
isDragging = false;
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
titlebar.addEventListener('mousedown', (e) => {
if (closeBtn && e.target === closeBtn) return;
isDragging = true;
const rect = modal.getBoundingClientRect();
startX = e.clientX;
startY = e.clientY;
startLeft = rect.left;
startTop = rect.top;
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
}
attachDrag(desktopModal, desktopTitlebar, desktopClose);
attachDrag(photoModal, photoTitlebar, photoClose);
attachDrag(searchInvModal, searchInvTitlebar, searchInvClose);
attachDrag(printersModal, printersTitlebar, printersClose);
attachDrag(computersModal, computersTitlebar, computersClose);
attachDrag(projectorsModal, projectorsTitlebar, projectorsClose);
attachDrag(docCameraModal, docCameraTitlebar, docCameraClose);
attachDrag(addDeviceModal, addDeviceTitlebar, addDeviceClose);
attachDrag(pcComponentsModal, pcComponentsTitlebar, pcComponentsClose);
attachDrag(usersModal, usersTitlebar, usersClose);
})();
</script>
</div>
</div>
{% endblock %}