Files
Inv_web/source/templates/cartridges.html
2026-02-23 20:59:05 +03:00

447 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block body_class %}page-bg bg-cartridges{% endblock %}
{% block content %}
<style>
.table td,
.table th {
white-space: normal;
word-break: break-word;
}
.row-actions {
background: #f8f9fa;
}
.row-actions__inner {
max-height: 0;
opacity: 0;
transform: translateY(-6px);
transition: max-height 0.2s ease, opacity 0.2s ease, transform 0.2s ease;
overflow: hidden;
padding: 0 0.75rem;
}
.row-actions.show .row-actions__inner {
max-height: 60px;
opacity: 1;
transform: translateY(0);
}
.issues-table .row-actions__inner {
max-height: 0;
}
#issueCancelCard {
background: transparent;
border: none;
}
#issueCancelCard .card-body {
background: transparent;
padding-left: 0;
padding-right: 0;
}
.edit-modal {
position: fixed;
inset: 0;
background: rgba(15, 23, 32, 0.55);
display: none;
align-items: center;
justify-content: center;
z-index: 1050;
padding: 1rem;
}
.edit-modal.show {
display: flex;
}
.edit-modal__dialog {
background: #fff;
border-radius: 0.75rem;
width: min(700px, 100%);
max-height: 85vh;
overflow: auto;
padding: 1.25rem 1.5rem;
box-shadow: 0 24px 48px rgba(15, 23, 32, 0.25);
}
.edit-modal__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.edit-modal__title {
font-size: 1.1rem;
font-weight: 600;
margin: 0;
}
.edit-modal__footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1rem;
}
</style>
<div class="d-flex align-items-center justify-content-between mb-2">
<h3 class="mb-0">Картриджи</h3>
<div class="d-flex gap-2">
{% if session.get('role') == 'admin' %}
<form method="post" action="/orders/low_stock">
<button class="btn btn-warning" type="submit">Добавить в заказы (≤ 1)</button>
</form>
<button class="btn btn-outline-secondary" type="button" id="toggleIssueCancel">Отменить выдачу</button>
<button class="btn btn-outline-primary" type="button" id="toggleOrderForm">Заказать картридж</button>
{% endif %}
<a class="btn btn-success" href="/report/cartridges.xlsx">Экспорт в Excel</a>
</div>
</div>
{% if session.get('role') != 'admin' %}
<div class="alert alert-secondary">Режим просмотра: изменения доступны только администратору.</div>
{% endif %}
{% if session.get('role') == 'admin' %}
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title mb-2">Выдача по штрихкоду (сканер)</h5>
<form method="post" action="/issue" class="row g-2 align-items-center" id="issueByBarcodeForm">
<div class="col-md-3">
<input name="barcode" class="form-control" placeholder="Сканируй штрихкод" id="issue_barcode" autocomplete="off" autofocus>
<div class="form-text" id="issue_hint"></div>
</div>
<div class="col-md-3">
<input class="form-control" id="issue_model" placeholder="Модель" readonly>
</div>
<div class="col-md-2">
<input name="quantity" class="form-control" placeholder="Кол-во" value="1">
</div>
<div class="col-md-2">
<select name="cabinet" class="form-select">
<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">Выдать</button>
</div>
</form>
</div>
</div>
<div class="card mb-3 d-none" id="orderFormCard">
<div class="card-body">
<h5 class="card-title mb-2">Заказ картриджа</h5>
<form method="post" action="/order_by_model" class="row g-2 align-items-center">
<div class="col-md-5">
<input name="model" class="form-control" placeholder="Модель картриджа">
</div>
<div class="col-md-3">
<input name="quantity" class="form-control" placeholder="Кол-во" value="1">
</div>
<div class="col-md-2 d-grid">
<button class="btn btn-success">Заказать</button>
</div>
</form>
</div>
</div>
<div class="card mb-3 d-none" id="issueCancelCard">
<div class="card-body">
<h5 class="card-title mb-2">Отмена выдачи</h5>
<div class="text-muted mb-2">Выдачи за последние 7 дней (нажмите на строку, чтобы отменить)</div>
<div class="table-responsive">
<table class="excel-table issues-table">
<thead>
<tr>
<th>Дата</th><th>Штрихкод</th><th>Модель</th><th>Кол-во</th><th>Кабинет</th>
</tr>
</thead>
<tbody id="issuesTableBody">
<tr><td colspan="5">Загрузка...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<form method="post" action="/add" class="row g-2 mb-3">
<input name="barcode" class="form-control col" placeholder="Штрихкод">
<input name="model" class="form-control col" placeholder="Модель">
<input name="quantity" class="form-control col" placeholder="Кол-во">
<button class="btn btn-success col">Добавить</button>
</form>
<script>
// Автоподсказка: по введённому/отсканированному штрихкоду показываем модель и остаток.
(function() {
const barcodeEl = document.getElementById('issue_barcode');
const modelEl = document.getElementById('issue_model');
const hintEl = document.getElementById('issue_hint');
if (!barcodeEl || !modelEl || !hintEl) return;
let last = '';
async function lookup() {
const barcode = (barcodeEl.value || '').trim();
if (!barcode || barcode === last) return;
last = barcode;
modelEl.value = '';
hintEl.textContent = 'Ищу...';
try {
const res = await fetch(`/api/cartridge?barcode=${encodeURIComponent(barcode)}`);
if (!res.ok) {
hintEl.textContent = 'Ошибка запроса';
return;
}
const data = await res.json();
if (!data.found) {
hintEl.textContent = 'Не найдено (проверь штрихкод)';
return;
}
modelEl.value = data.model || '';
hintEl.textContent = `Остаток: ${data.quantity}`;
} catch (e) {
hintEl.textContent = 'Ошибка сети';
}
}
// Сканер обычно вводит строку очень быстро и жмёт Enter.
barcodeEl.addEventListener('input', () => {
// небольшой debounce
window.clearTimeout(window.__barcodeTimer);
window.__barcodeTimer = window.setTimeout(lookup, 120);
});
barcodeEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
// не мешаем сабмиту, но успеваем обновить подсказку
lookup();
}
});
})();
</script>
<script>
(function() {
const toggleBtn = document.getElementById('toggleOrderForm');
const card = document.getElementById('orderFormCard');
if (!toggleBtn || !card) return;
toggleBtn.addEventListener('click', () => {
card.classList.toggle('d-none');
});
})();
</script>
<script>
(function() {
const toggleBtn = document.getElementById('toggleIssueCancel');
const card = document.getElementById('issueCancelCard');
const tbody = document.getElementById('issuesTableBody');
if (!toggleBtn || !card || !tbody) return;
let loaded = false;
function clearActionRow() {
const existing = tbody.querySelector('.row-actions');
if (existing) existing.remove();
}
function renderRows(items) {
tbody.innerHTML = '';
if (!items.length) {
tbody.innerHTML = '<tr><td colspan="5">Нет выдач за последнюю неделю</td></tr>';
return;
}
items.forEach((item) => {
const tr = document.createElement('tr');
tr.className = 'js-issue-row';
tr.dataset.movementId = item.id;
tr.dataset.barcode = item.barcode || '';
tr.dataset.model = item.model || '';
tr.dataset.qty = item.quantity || 0;
tr.dataset.cabinet = item.cabinet || '';
tr.innerHTML = `<td>${item.date || ''}</td>
<td>${item.barcode || ''}</td>
<td>${item.model || ''}</td>
<td>${item.quantity || ''}</td>
<td>${item.cabinet || ''}</td>`;
tbody.appendChild(tr);
});
const rows = tbody.querySelectorAll('.js-issue-row');
rows.forEach((row) => {
row.addEventListener('click', (e) => {
if (e.target.closest('button, input, select, textarea, a')) return;
clearActionRow();
const movementId = row.dataset.movementId || '';
if (!movementId) return;
const actionRow = document.createElement('tr');
actionRow.className = 'row-actions show';
actionRow.innerHTML = `<td colspan="5">
<div class="row-actions__inner" style="max-height:60px;opacity:1;transform:translateY(0)">
<form method="post" action="/cartridges/issue/cancel" class="d-inline">
<input type="hidden" name="movement_id" value="${movementId}">
<button class="btn btn-sm btn-outline-danger" type="submit">Отменить выдачу</button>
</form>
</div>
</td>`;
row.insertAdjacentElement('afterend', actionRow);
});
});
}
async function loadIssues() {
tbody.innerHTML = '<tr><td colspan="5">Загрузка...</td></tr>';
try {
const res = await fetch('/api/cartridge/issues/recent');
if (!res.ok) throw new Error('bad status');
const data = await res.json();
renderRows((data && data.items) || []);
} catch (e) {
tbody.innerHTML = '<tr><td colspan="5">Ошибка загрузки</td></tr>';
}
}
toggleBtn.addEventListener('click', () => {
card.classList.toggle('d-none');
if (!card.classList.contains('d-none') && !loaded) {
loaded = true;
loadIssues();
}
});
tbody.addEventListener('submit', async (e) => {
const form = e.target.closest('form');
if (!form || !form.action.includes('/cartridges/issue/cancel')) return;
e.preventDefault();
const fd = new FormData(form);
try {
const res = await fetch(form.action, { method: 'POST', body: fd });
if (!res.ok) throw new Error('bad status');
await loadIssues();
} catch (err) {
alert('Не удалось отменить выдачу');
}
});
})();
</script>
{% endif %}
{% if session.get('role') == 'admin' %}
<script>
window.addEventListener('DOMContentLoaded', () => {
const rows = document.querySelectorAll('.js-cartridge-row');
if (!rows.length) return;
const modal = document.getElementById('cartridgeEditModal');
const form = document.getElementById('cartridgeEditForm');
if (!modal || !form) return;
const modalFields = {
barcode: document.getElementById('cartModalBarcode'),
model: document.getElementById('cartModalModel'),
quantity: document.getElementById('cartModalQuantity'),
};
function closeModal() {
modal.classList.remove('show');
modal.setAttribute('aria-hidden', 'true');
}
function openModal() {
modal.classList.add('show');
modal.setAttribute('aria-hidden', 'false');
}
function fillModalFromRow(row) {
const barcode = row.getAttribute('data-barcode') || '';
const model = row.getAttribute('data-model') || '';
const quantity = row.getAttribute('data-quantity') || '';
if (!barcode) return false;
modalFields.barcode.value = barcode;
modalFields.model.value = model;
modalFields.quantity.value = quantity;
return true;
}
modal.addEventListener('click', (e) => {
if (e.target === modal || e.target.hasAttribute('data-modal-close')) {
closeModal();
}
});
function showActionRow(row) {
const existing = row.parentElement.querySelector('.row-actions');
if (existing) existing.remove();
const barcode = row.getAttribute('data-barcode') || '';
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-cart-modal">Редактировать</button>
<form method="post" action="/cartridges/delete" class="d-inline ms-2">
<input type="hidden" name="barcode" value="${barcode}">
<button class="btn btn-sm btn-outline-danger" type="submit">Удалить</button>
</form>
</div>
</td>`;
row.insertAdjacentElement('afterend', actionRow);
const btn = actionRow.querySelector('.js-open-cart-modal');
btn.addEventListener('click', () => {
if (fillModalFromRow(row)) openModal();
});
requestAnimationFrame(() => actionRow.classList.add('show'));
}
rows.forEach((row) => {
row.addEventListener('click', (e) => {
if (e.target.closest('button, input, select, textarea, a')) return;
showActionRow(row);
});
row.addEventListener('dblclick', (e) => {
if (e.target.closest('button, input, select, textarea, a')) return;
if (fillModalFromRow(row)) openModal();
});
});
});
</script>
{% endif %}
<table class="table table-striped">
<tr>
<th>Штрихкод</th><th>Модель</th><th>Остаток</th>
</tr>
{% for b,m,q,minq in items %}
<tr class="js-cartridge-row {{ 'table-danger' if q < minq else '' }}"
data-barcode="{{ b }}"
data-model="{{ m or '' }}"
data-quantity="{{ q }}">
<td>{{ b }}</td><td>{{ m }}</td><td>{{ q }}</td>
</tr>
{% endfor %}
</table>
{% if session.get('role') == 'admin' %}
<div class="edit-modal" id="cartridgeEditModal" aria-hidden="true">
<div class="edit-modal__dialog">
<div class="edit-modal__header">
<h4 class="edit-modal__title">Редактирование картриджа</h4>
<button type="button" class="btn btn-sm btn-outline-secondary" data-modal-close>Закрыть</button>
</div>
<form method="post" action="/cartridges/edit" id="cartridgeEditForm">
<div class="row g-2">
<div class="col-md-4">
<input name="barcode" id="cartModalBarcode" class="form-control" placeholder="Штрихкод" readonly>
</div>
<div class="col-md-4">
<input name="model" id="cartModalModel" class="form-control" placeholder="Модель">
</div>
<div class="col-md-4">
<input name="quantity" id="cartModalQuantity" class="form-control" placeholder="Остаток">
</div>
</div>
<div class="edit-modal__footer">
<button type="button" class="btn btn-outline-secondary" data-modal-close>Отмена</button>
<button class="btn btn-primary">Сохранить</button>
</div>
</form>
</div>
</div>
{% endif %}
{% endblock %}