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

557 lines
18 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);
}
.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;
}
.cartridges-page .btn,
.cartridges-page .btn:hover,
.cartridges-page .btn:focus {
background: #d9d9d9;
border: 1px solid #000;
color: #000;
padding: 2px 14px;
font-family: 'Chicago Regular', sans-serif;
font-size: 0.85rem;
line-height: 1.2;
border-radius: 6px;
box-shadow: inset 1px 1px 0 #f7f7f7, inset -1px -1px 0 #8a8a8a;
text-decoration: none;
min-height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.cartridges-page .btn:active {
box-shadow: inset -1px -1px 0 #f7f7f7, inset 1px 1px 0 #8a8a8a;
}
.cartridges-page .excel-table {
width: 100%;
border-collapse: collapse;
background: #f8f8f8;
font-family: 'Chicago Regular', sans-serif;
font-size: 0.88rem;
}
.cartridges-page .excel-table th,
.cartridges-page .excel-table td {
border: 1px solid #7a7a7a;
padding: 6px 8px;
background: #ffffff;
}
.cartridges-page .excel-table th {
background: linear-gradient(#d9d9d9, #c0c0c0);
color: #111;
font-weight: 600;
text-align: left;
box-shadow: inset 1px 1px 0 #f7f7f7, inset -1px -1px 0 #9a9a9a;
}
.cartridges-page .excel-table tr.table-danger td {
background: #f5c2c7;
}
.cartridges-page .form-control,
.cartridges-page .form-select {
border: 1px solid #7a7a7a;
border-radius: 6px;
background: #ffffff;
font-family: 'Chicago Regular', sans-serif;
font-size: 0.88rem;
padding: 4px 6px;
box-shadow: inset 1px 1px 0 #f2f2f2, inset -1px -1px 0 #b3b3b3;
}
.cartridges-page .add-form {
column-gap: 10px;
}
.cartridges-page .add-form .qty-col {
flex: 0 0 120px;
max-width: 120px;
}
#issueByBarcodeForm .form-control,
#issueByBarcodeForm .form-select,
#issueByBarcodeForm .btn {
height: 34px;
line-height: 1.2;
margin-top: 0;
}
#issueByBarcodeForm .form-text {
margin-top: 2px;
}
.issues-table .row-actions__inner {
max-height: 0;
}
#issueCancelCard {
background: transparent;
border: none;
}
#issueCancelCard .card-body {
background: transparent;
padding-left: 0;
padding-right: 0;
}
</style>
<div class="cartridges-page">
<div class="d-flex align-items-center justify-content-between mb-2">
<h3 class="mb-0" style="font-family: 'Chicago Regular', sans-serif;">Картриджи</h3>
<div class="d-flex gap-2">
{% if session.get('role') in ('admin','storekeeper') %}
<button class="btn btn-outline-primary" type="button" id="toggleSearchForm" aria-label="Поиск">&#128269;</button>
<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>
<div class="card mb-3 d-none" id="searchFormCard">
<div class="card-body">
<form class="row g-2 align-items-center" id="searchForm">
<div class="col-md-6">
<input id="searchModelInput" class="form-control" placeholder="Модель картриджа">
</div>
<div class="col-md-2 d-grid">
<button class="btn btn-primary" type="submit">Найти</button>
</div>
<div class="col-md-2 d-grid">
<button class="btn btn-outline-secondary" type="button" id="searchClearBtn">Очистить</button>
</div>
</form>
</div>
</div>
{% if session.get('role') not in ('admin','storekeeper') %}
<div class="alert alert-secondary">Режим просмотра: изменения доступны только администратору.</div>
{% endif %}
<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 add-form">
<div class="col">
<input name="barcode" class="form-control" placeholder="Штрихкод">
</div>
<div class="col">
<input name="model" class="form-control" placeholder="Модель">
</div>
<div class="qty-col">
<input name="quantity" class="form-control" placeholder="Кол-во">
</div>
<div class="col">
<button class="btn btn-success w-100">Добавить</button>
</div>
</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>
<script>
window.addEventListener('DOMContentLoaded', () => {
const toggleBtn = document.getElementById('toggleSearchForm');
const card = document.getElementById('searchFormCard');
const form = document.getElementById('searchForm');
const input = document.getElementById('searchModelInput');
const clearBtn = document.getElementById('searchClearBtn');
if (!toggleBtn || !card || !form || !input) return;
const rows = () => document.querySelectorAll('.js-cartridge-row');
const applyFilter = () => {
const q = (input.value || '').trim().toLowerCase();
rows().forEach((row) => {
const model = (row.getAttribute('data-model') || '').toLowerCase();
const match = !q || model.includes(q);
row.classList.toggle('d-none', !match);
});
};
toggleBtn.addEventListener('click', () => {
card.classList.toggle('d-none');
if (!card.classList.contains('d-none')) {
input.focus();
}
});
form.addEventListener('submit', (e) => {
e.preventDefault();
applyFilter();
});
if (clearBtn) {
clearBtn.addEventListener('click', () => {
input.value = '';
applyFilter();
});
}
});
</script>
{% if session.get('role') in ('admin','storekeeper') %}
<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="excel-table">
<tr>
<th>Штрихкод</th><th>Модель</th><th>Остаток</th>
</tr>
{% for b,m,q,minq in items %}
{% set model_key = (m or '')|trim|lower %}
<tr class="js-cartridge-row {{ 'table-danger' if (q < minq) or (model_key in orphan_model_keys) 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') in ('admin','storekeeper') %}
<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 %}
</div>
{% endblock %}