Files
Inv_web/templates/cabinets.html
2026-03-11 17:13:42 +03:00

594 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block body_class %}page-bg bg-cabinets{% endblock %}
{% block content %}
<style>
body.bg-cabinets {
background-color: #B3B3DA;
background-image:
linear-gradient(rgba(179, 179, 218, 0.75), rgba(179, 179, 218, 0.75)),
url('{{ url_for("static", filename="bg/bg_zdanie.png", v=1) }}');
background-size: auto 40vh;
}
.row-actions {
background: #f8f9fa;
}
.row-actions__inner {
max-height: 0;
opacity: 0;
transform: translateY(-6px);
transition: max-height 0.2s ease, opacity 0.2s ease, transform 0.2s ease;
overflow: hidden;
padding: 0 0.75rem;
}
.row-actions.show .row-actions__inner {
max-height: 60px;
opacity: 1;
transform: translateY(0);
}
.overlay-modal {
position: fixed;
inset: 0;
background: rgba(15, 23, 32, 0.55);
display: none;
align-items: center;
justify-content: center;
z-index: 1050;
padding: 1rem;
}
.overlay-modal.show {
display: flex;
}
.overlay-modal__dialog {
background: #fff;
border-radius: 0.75rem;
width: min(720px, 100%);
max-height: 85vh;
overflow: auto;
}
.overlay-modal__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border-bottom: 1px solid #e2e2e2;
}
.overlay-modal__title {
margin: 0;
font-size: 1.05rem;
}
.overlay-modal__footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
padding: 0.75rem 1rem;
border-top: 1px solid #e2e2e2;
}
.floor-combo {
position: relative;
}
.floor-combo__trigger {
width: 100%;
text-align: left;
display: flex;
justify-content: space-between;
align-items: center;
}
.floor-combo__trigger::after {
content: "▾";
color: #5a6570;
margin-left: 0.5rem;
}
.floor-combo.open .floor-combo__trigger::after {
content: "▴";
}
.floor-combo__menu {
position: absolute;
left: 0;
right: 0;
top: calc(100% + 0.25rem);
background: #fff;
border: 1px solid #d5dbe1;
border-radius: 0.5rem;
box-shadow: 0 8px 18px rgba(25, 32, 38, 0.14);
z-index: 40;
display: none;
max-height: 260px;
overflow: auto;
padding: 0.35rem;
}
.floor-combo.open .floor-combo__menu {
display: block;
}
.floor-combo__option {
width: 100%;
border: 0;
background: transparent;
text-align: left;
padding: 0.35rem 0.5rem;
border-radius: 0.35rem;
}
.floor-combo__option:hover {
background: #f2f5f8;
}
.floor-combo__option-row {
display: flex;
gap: 0.35rem;
align-items: center;
}
.floor-combo__option-row .floor-combo__option {
flex: 1;
}
.floor-combo__delete {
white-space: nowrap;
}
.floor-combo__empty {
color: #687483;
font-size: 0.9rem;
padding: 0.35rem 0.5rem;
}
.floor-combo__add {
border-top: 1px solid #e4e9ee;
margin-top: 0.35rem;
padding-top: 0.45rem;
}
.floor-combo__add-row {
display: flex;
gap: 0.35rem;
align-items: center;
}
</style>
<h3>Здания и кабинеты</h3>
{% if session.get('role') not in ('admin','storekeeper') %}
<div class="alert alert-secondary">Режим просмотра: изменения доступны только администратору.</div>
{% endif %}
{% if grouped %}
{% for building, rows in grouped.items() %}
<h5 class="mt-4 mb-2">Здание: {{ building }}</h5>
<table class="table table-striped">
<tr>
<th>Кабинет</th>
<th>Этаж</th>
<th>Здание</th>
</tr>
{% for row in rows %}
<tr class="js-cabinet-row"
data-id="{{ row.id }}"
data-building="{{ row.building }}"
data-floor="{{ row.floor }}"
data-cabinet="{{ row.cabinet }}">
<td>{{ row.cabinet }}</td>
<td>{{ row.floor or '—' }}</td>
<td>{{ row.building or '—' }}</td>
</tr>
{% endfor %}
</table>
{% if session.get('role') in ('admin','storekeeper') and building != 'Без здания' %}
<form method="post" action="/buildings/delete" class="mb-4 d-flex justify-content-end" onsubmit="return confirm('Удалить организацию/здание и все кабинеты в нем?')">
<input type="hidden" name="building" value="{{ building }}">
<button class="btn btn-sm btn-outline-secondary text-danger" type="submit">Удалить организацию/здание</button>
</form>
{% endif %}
{% endfor %}
{% else %}
<div class="alert alert-secondary">Кабинеты не найдены.</div>
{% endif %}
{% if session.get('role') in ('admin','storekeeper') %}
<div class="overlay-modal" id="addBuildingModal" aria-hidden="true">
<div class="overlay-modal__dialog">
<div class="overlay-modal__header">
<h4 class="overlay-modal__title">Добавить здание</h4>
<button type="button" class="btn btn-sm btn-outline-secondary" data-modal-close>Закрыть</button>
</div>
<form method="post" action="/buildings/add">
<div class="row g-2 px-3 py-3">
<div class="col-12">
<label class="form-label">Здание</label>
<input name="building" class="form-control" placeholder="Например: 1, А, Корпус 2" required>
</div>
</div>
<div class="overlay-modal__footer">
<button type="button" class="btn btn-outline-secondary" data-modal-close>Отмена</button>
<button class="btn btn-success">Добавить</button>
</div>
</form>
</div>
</div>
<div class="overlay-modal" id="addCabinetModal" aria-hidden="true">
<div class="overlay-modal__dialog">
<div class="overlay-modal__header">
<h4 class="overlay-modal__title">Добавить кабинет</h4>
<button type="button" class="btn btn-sm btn-outline-secondary" data-modal-close>Закрыть</button>
</div>
<form method="post" action="/cabinets/add" id="cabinetAddForm">
<div class="row g-2 px-3 py-3">
<div class="col-md-6">
<label class="form-label">Здание</label>
<select name="building" class="form-select" id="cabinetBuilding" required>
<option value="">Выберите здание...</option>
{% for building in buildings %}
<option value="{{ building }}" {% if selected_building == building %}selected{% endif %}>{{ building }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Кабинет</label>
<input name="cabinet" class="form-control" placeholder="Например: 101" required>
</div>
<div class="col-md-6">
<label class="form-label">Этаж</label>
<div class="floor-combo" id="cabinetFloorCombo">
<input type="hidden" name="floor" id="cabinetFloor">
<button type="button" class="btn btn-outline-secondary floor-combo__trigger" id="cabinetFloorTrigger">Выберите этаж</button>
<div class="floor-combo__menu" id="cabinetFloorMenu">
<div id="cabinetFloorOptions"></div>
<div class="floor-combo__add">
<div class="floor-combo__add-row">
<input type="text" class="form-control form-control-sm" id="cabinetFloorNew" placeholder="Новый этаж">
<button type="button" class="btn btn-sm btn-outline-primary" id="cabinetFloorAddBtn">Добавить этаж</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="overlay-modal__footer">
<button type="button" class="btn btn-outline-secondary" data-modal-close>Отмена</button>
<button class="btn btn-success">Добавить</button>
</div>
</form>
</div>
</div>
<div class="overlay-modal" id="cabinetEditModal" aria-hidden="true">
<div class="overlay-modal__dialog">
<div class="overlay-modal__header">
<h4 class="overlay-modal__title">Редактирование кабинета</h4>
<button type="button" class="btn btn-sm btn-outline-secondary" data-modal-close>Закрыть</button>
</div>
<form method="post" action="/cabinets/edit" id="cabinetEditForm">
<input type="hidden" name="id" id="cabinetModalId">
<div class="row g-2 px-3 py-3">
<div class="col-md-6">
<label class="form-label">Здание</label>
<select name="building" id="cabinetModalBuilding" class="form-select" required>
<option value="">Выберите здание...</option>
{% for building in buildings %}
<option value="{{ building }}">{{ building }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Кабинет</label>
<input name="cabinet" id="cabinetModalCabinet" class="form-control" placeholder="Кабинет" required>
</div>
<div class="col-md-6">
<label class="form-label">Этаж</label>
<div class="floor-combo" id="cabinetModalFloorCombo">
<input type="hidden" name="floor" id="cabinetModalFloor">
<button type="button" class="btn btn-outline-secondary floor-combo__trigger" id="cabinetModalFloorTrigger">Выберите этаж</button>
<div class="floor-combo__menu" id="cabinetModalFloorMenu">
<div id="cabinetModalFloorOptions"></div>
<div class="floor-combo__add">
<div class="floor-combo__add-row">
<input type="text" class="form-control form-control-sm" id="cabinetModalFloorNew" placeholder="Новый этаж">
<button type="button" class="btn btn-sm btn-outline-primary" id="cabinetModalFloorAddBtn">Добавить этаж</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="overlay-modal__footer">
<button type="button" class="btn btn-outline-secondary" data-modal-close>Отмена</button>
<button class="btn btn-primary">Сохранить</button>
</div>
</form>
</div>
</div>
{% endif %}
<script>
(function() {
const floorMap = {{ floor_map | tojson }};
const openModalName = {{ open_modal | tojson }};
function getFloorOptions(building) {
return floorMap[(building || '').trim()] || [];
}
function upsertFloor(building, floorValue) {
const bld = (building || '').trim();
const floor = (floorValue || '').trim();
if (!bld || !floor) return;
const current = floorMap[bld] || [];
if (!current.includes(floor)) {
current.push(floor);
current.sort();
floorMap[bld] = current;
}
}
function createFloorCombo(config) {
const root = config.root;
const hidden = config.hidden;
const trigger = config.trigger;
const optionsEl = config.options;
const addInput = config.addInput;
const addBtn = config.addBtn;
const resolveBuilding = config.resolveBuilding;
const onAdd = config.onAdd;
const onDelete = config.onDelete;
if (!root || !hidden || !trigger || !optionsEl || !addInput || !addBtn) {
return { refresh: () => {}, setValue: () => {}, close: () => {} };
}
const close = () => root.classList.remove('open');
const open = () => root.classList.add('open');
const setTriggerText = () => {
const value = (hidden.value || '').trim();
trigger.textContent = value || 'Выберите этаж';
};
const setValue = (value) => {
hidden.value = (value || '').trim();
setTriggerText();
};
const renderOptions = () => {
const building = resolveBuilding();
const options = getFloorOptions(building);
optionsEl.innerHTML = '';
if (!options.length) {
const empty = document.createElement('div');
empty.className = 'floor-combo__empty';
empty.textContent = 'Этажей пока нет';
optionsEl.appendChild(empty);
return;
}
options.forEach((item) => {
const row = document.createElement('div');
row.className = 'floor-combo__option-row';
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'floor-combo__option';
btn.textContent = item;
btn.addEventListener('click', () => {
setValue(item);
close();
});
row.appendChild(btn);
if (typeof onDelete === 'function') {
const del = document.createElement('button');
del.type = 'button';
del.className = 'btn btn-sm btn-outline-danger floor-combo__delete';
del.textContent = 'Удалить';
del.addEventListener('click', (e) => {
e.stopPropagation();
const building = resolveBuilding();
if (!building) return;
if (!confirm(`Удалить этаж ${item} в здании ${building}?`)) return;
onDelete(building, item);
});
row.appendChild(del);
}
optionsEl.appendChild(row);
});
};
trigger.addEventListener('click', () => {
if (root.classList.contains('open')) {
close();
return;
}
renderOptions();
open();
});
addBtn.addEventListener('click', () => {
const building = resolveBuilding();
const newFloor = (addInput.value || '').trim();
if (!building || !newFloor) return;
upsertFloor(building, newFloor);
setValue(newFloor);
addInput.value = '';
renderOptions();
if (typeof onAdd === 'function') {
onAdd(building, newFloor);
}
});
document.addEventListener('click', (e) => {
if (!root.contains(e.target)) close();
});
setTriggerText();
return { refresh: renderOptions, setValue, close };
}
function bindOverlay(modalEl) {
if (!modalEl) return { open: () => {}, close: () => {} };
const close = () => {
modalEl.classList.remove('show');
modalEl.setAttribute('aria-hidden', 'true');
};
const open = () => {
modalEl.classList.add('show');
modalEl.setAttribute('aria-hidden', 'false');
};
modalEl.addEventListener('click', (e) => {
if (e.target === modalEl || e.target.hasAttribute('data-modal-close')) close();
});
return { open, close };
}
function submitFloorDelete(building, floor) {
const form = document.createElement('form');
form.method = 'post';
form.action = '/floors/delete';
const b = document.createElement('input');
b.type = 'hidden';
b.name = 'building';
b.value = building;
form.appendChild(b);
const f = document.createElement('input');
f.type = 'hidden';
f.name = 'floor';
f.value = floor;
form.appendChild(f);
document.body.appendChild(form);
form.submit();
}
function submitFloorAdd(building, floor) {
const form = document.createElement('form');
form.method = 'post';
form.action = '/floors/add';
const b = document.createElement('input');
b.type = 'hidden';
b.name = 'building';
b.value = building;
form.appendChild(b);
const f = document.createElement('input');
f.type = 'hidden';
f.name = 'floor';
f.value = floor;
form.appendChild(f);
document.body.appendChild(form);
form.submit();
}
const canEdit = {{ 'true' if session.get('role') in ('admin','storekeeper') else 'false' }};
if (!canEdit) return;
const addBuildingModal = document.getElementById('addBuildingModal');
const addCabinetModal = document.getElementById('addCabinetModal');
const editModal = document.getElementById('cabinetEditModal');
const addBuildingCtl = bindOverlay(addBuildingModal);
const addCabinetCtl = bindOverlay(addCabinetModal);
const editCtl = bindOverlay(editModal);
const addBuildingSelect = document.getElementById('cabinetBuilding');
const addFloorCombo = createFloorCombo({
root: document.getElementById('cabinetFloorCombo'),
hidden: document.getElementById('cabinetFloor'),
trigger: document.getElementById('cabinetFloorTrigger'),
options: document.getElementById('cabinetFloorOptions'),
addInput: document.getElementById('cabinetFloorNew'),
addBtn: document.getElementById('cabinetFloorAddBtn'),
resolveBuilding: () => addBuildingSelect ? addBuildingSelect.value : '',
onAdd: submitFloorAdd,
onDelete: submitFloorDelete,
});
if (addBuildingSelect) {
addBuildingSelect.addEventListener('change', () => {
addFloorCombo.setValue('');
addFloorCombo.refresh();
});
}
const fields = {
id: document.getElementById('cabinetModalId'),
building: document.getElementById('cabinetModalBuilding'),
floor: document.getElementById('cabinetModalFloor'),
cabinet: document.getElementById('cabinetModalCabinet'),
};
const modalFloorCombo = createFloorCombo({
root: document.getElementById('cabinetModalFloorCombo'),
hidden: document.getElementById('cabinetModalFloor'),
trigger: document.getElementById('cabinetModalFloorTrigger'),
options: document.getElementById('cabinetModalFloorOptions'),
addInput: document.getElementById('cabinetModalFloorNew'),
addBtn: document.getElementById('cabinetModalFloorAddBtn'),
resolveBuilding: () => fields.building ? fields.building.value : '',
onAdd: submitFloorAdd,
onDelete: submitFloorDelete,
});
if (fields.building) {
fields.building.addEventListener('change', () => {
modalFloorCombo.setValue('');
modalFloorCombo.refresh();
});
}
function fillModalFromRow(row) {
const rowBuilding = row.getAttribute('data-building') || '';
if (fields.id) fields.id.value = row.getAttribute('data-id') || '';
if (fields.building) {
const exists = Array.from(fields.building.options || []).some((o) => o.value === rowBuilding);
if (!exists && rowBuilding) {
const opt = document.createElement('option');
opt.value = rowBuilding;
opt.textContent = rowBuilding;
fields.building.appendChild(opt);
}
fields.building.value = rowBuilding;
}
modalFloorCombo.setValue(row.getAttribute('data-floor') || '');
if (fields.cabinet) fields.cabinet.value = row.getAttribute('data-cabinet') || '';
modalFloorCombo.refresh();
return true;
}
const rows = document.querySelectorAll('.js-cabinet-row');
function showActionRow(row) {
const existing = row.parentElement.querySelector('.row-actions');
if (existing) existing.remove();
const rowId = row.getAttribute('data-id') || '';
const actionRow = document.createElement('tr');
actionRow.className = 'row-actions';
actionRow.innerHTML = `<td colspan="3">
<div class="row-actions__inner">
<button type="button" class="btn btn-sm btn-outline-primary js-open-cabinet-modal">Редактировать</button>
<form method="post" action="/cabinets/delete" class="d-inline ms-2" onsubmit="return confirm('Удалить кабинет?')">
<input type="hidden" name="id" value="${rowId}">
<button class="btn btn-sm btn-outline-danger" type="submit">Удалить</button>
</form>
</div>
</td>`;
row.insertAdjacentElement('afterend', actionRow);
const btn = actionRow.querySelector('.js-open-cabinet-modal');
btn.addEventListener('click', () => {
if (fillModalFromRow(row)) editCtl.open();
});
requestAnimationFrame(() => actionRow.classList.add('show'));
}
rows.forEach((row) => {
row.addEventListener('click', (e) => {
if (e.target.closest('button, input, select, textarea, a')) return;
showActionRow(row);
});
row.addEventListener('dblclick', (e) => {
if (e.target.closest('button, input, select, textarea, a')) return;
if (fillModalFromRow(row)) editCtl.open();
});
});
if (openModalName === 'add-building') {
addBuildingCtl.open();
} else if (openModalName === 'add-cabinet') {
addCabinetCtl.open();
}
})();
</script>
{% endblock %}