377 lines
13 KiB
HTML
377 lines
13 KiB
HTML
{% extends "base.html" %}
|
|
{% block body_class %}page-bg bg-catalog{% endblock %}
|
|
{% block content %}
|
|
<style>
|
|
body.bg-catalog {
|
|
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_catalog.png", v=1) }}');
|
|
background-size: auto 40vh;
|
|
}
|
|
.catalog-page .card {
|
|
background-color: #B3B3DA;
|
|
border: 1px solid #B3B3DA;
|
|
box-shadow: none;
|
|
}
|
|
.catalog-page .card-body {
|
|
background-color: transparent;
|
|
}
|
|
.catalog-page .form-control,
|
|
.catalog-page .form-select {
|
|
background-color: #ffffff;
|
|
border: 1px solid #B3B3DA;
|
|
}
|
|
.catalog-page .form-control:focus,
|
|
.catalog-page .form-select:focus {
|
|
box-shadow: 0 0 0 0.1rem rgba(0, 0, 0, 0.15);
|
|
border-color: #B3B3DA;
|
|
}
|
|
.catalog-page .list-group-item {
|
|
background-color: transparent;
|
|
border: 0;
|
|
padding-left: 0;
|
|
padding-right: 0;
|
|
}
|
|
.catalog-page .device-hints {
|
|
width: 100%;
|
|
background: #ffffff;
|
|
border: 1px solid #9499b3;
|
|
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.16);
|
|
border-radius: 0.25rem;
|
|
max-height: 260px;
|
|
overflow-y: auto;
|
|
}
|
|
.catalog-page .device-hints .list-group-item {
|
|
padding: 0.375rem 0.75rem;
|
|
font-size: 1rem;
|
|
line-height: 1.5;
|
|
background: #ffffff;
|
|
border: 0;
|
|
border-bottom: 1px solid #ececec;
|
|
text-align: left;
|
|
}
|
|
.catalog-page .device-hints .list-group-item:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
.catalog-page .device-hints .list-group-item:hover,
|
|
.catalog-page .device-hints .list-group-item:focus {
|
|
background: #ecefff;
|
|
}
|
|
</style>
|
|
<div class="catalog-page">
|
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
|
<h3 class="mb-0">Каталог</h3>
|
|
</div>
|
|
|
|
<div class="card mb-3">
|
|
<div class="card-body">
|
|
<h5 class="card-title mb-2">Подбор картриджа и расходного материала по модели устройства</h5>
|
|
<form method="get" action="/catalog" class="row g-2 align-items-center">
|
|
<div class="col-md-6 position-relative">
|
|
<input type="hidden" name="device" id="deviceHidden" value="{{ selected_key }}">
|
|
<input
|
|
name="device_display"
|
|
class="form-control"
|
|
placeholder="Выберите модель..."
|
|
value="{{ selected_label }}"
|
|
autocomplete="off"
|
|
id="deviceInput"
|
|
/>
|
|
<div id="deviceHints" class="device-hints list-group position-absolute w-100" style="z-index: 10; display: none;"></div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button class="btn btn-primary w-100">Показать</button>
|
|
</div>
|
|
</form>
|
|
{% if hint %}
|
|
<div class="alert alert-warning mt-2 mb-0">{{ hint }}</div>
|
|
{% endif %}
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
const input = document.getElementById("deviceInput");
|
|
const hidden = document.getElementById("deviceHidden");
|
|
const hints = document.getElementById("deviceHints");
|
|
const form = input ? input.closest("form") : null;
|
|
if (!input || !hidden || !hints) return;
|
|
|
|
const rows = {{ device_models | tojson }};
|
|
const seen = new Set();
|
|
const data = rows
|
|
.map((r) => {
|
|
const brand = (r[0] || "").trim();
|
|
const model = (r[1] || "").trim();
|
|
const dtype = (r[2] || "").trim();
|
|
const typeLabel = dtype === "printer" ? "Принтер" : (dtype === "mfp" ? "МФУ" : dtype);
|
|
const base = (brand ? brand + " " : "") + model;
|
|
const label = base + (typeLabel ? " (" + typeLabel + ")" : "");
|
|
const key = model && dtype ? (model + "||" + dtype) : model;
|
|
return { label, key, model, dtype, base };
|
|
})
|
|
.filter((d) => {
|
|
if (!d.model || !d.dtype) return false;
|
|
const k = d.label + "||" + d.key;
|
|
if (seen.has(k)) return false;
|
|
seen.add(k);
|
|
return true;
|
|
});
|
|
|
|
function escapeHtml(value) {
|
|
return String(value || "")
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
function render(list) {
|
|
if (!list.length) {
|
|
hints.style.display = "none";
|
|
hints.innerHTML = "";
|
|
return;
|
|
}
|
|
hints.innerHTML = list
|
|
.map(
|
|
(item) =>
|
|
'<button type="button" class="list-group-item list-group-item-action" data-key="' +
|
|
escapeHtml(item.key) +
|
|
'">' +
|
|
escapeHtml(item.label) +
|
|
"</button>"
|
|
)
|
|
.join("");
|
|
hints.style.display = "block";
|
|
}
|
|
|
|
function showHints() {
|
|
const q = input.value.trim().toLowerCase();
|
|
const matches = data
|
|
.filter((d) => !q || d.label.toLowerCase().includes(q) || d.base.toLowerCase().includes(q))
|
|
.slice(0, 10);
|
|
render(matches);
|
|
}
|
|
|
|
input.addEventListener("input", () => {
|
|
const typed = input.value.trim();
|
|
const exact = data.find((d) => d.label.toLowerCase() === typed.toLowerCase());
|
|
hidden.value = exact ? exact.key : "";
|
|
showHints();
|
|
});
|
|
|
|
hints.addEventListener("click", (e) => {
|
|
const btn = e.target.closest("button[data-key]");
|
|
if (!btn) return;
|
|
const selectedKey = btn.getAttribute("data-key") || "";
|
|
const selected = data.find((d) => d.key === selectedKey);
|
|
input.value = selected ? selected.label : (btn.textContent || "");
|
|
hidden.value = selectedKey;
|
|
render([]);
|
|
input.focus();
|
|
});
|
|
|
|
input.addEventListener("focus", showHints);
|
|
input.addEventListener("click", showHints);
|
|
|
|
if (form) {
|
|
form.addEventListener("submit", () => {
|
|
if (hidden.value) return;
|
|
const typed = input.value.trim().toLowerCase();
|
|
if (!typed) return;
|
|
const exact = data.find((d) => d.label.toLowerCase() === typed || d.base.toLowerCase() === typed);
|
|
if (exact) {
|
|
hidden.value = exact.key;
|
|
input.value = exact.label;
|
|
return;
|
|
}
|
|
const firstMatch = data.find((d) => d.label.toLowerCase().includes(typed) || d.base.toLowerCase().includes(typed));
|
|
if (firstMatch) {
|
|
hidden.value = firstMatch.key;
|
|
input.value = firstMatch.label;
|
|
}
|
|
});
|
|
}
|
|
|
|
input.addEventListener("blur", () => {
|
|
setTimeout(() => render([]), 150);
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
{% if selected_model %}
|
|
<div class="card mb-3">
|
|
<div class="card-body">
|
|
<h5 class="card-title mb-2">Картриджи для выбранного устройства</h5>
|
|
{% if cartridge_rows %}
|
|
<div class="list-group">
|
|
{% for b, m, q, minq in cartridge_rows %}
|
|
<div class="list-group-item">
|
|
<div><strong>Модель:</strong> {{ m }}</div>
|
|
<div><strong>Остаток:</strong> {{ q }}</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="alert alert-warning mb-0">Картриджи для выбранной модели не найдены.</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mb-3">
|
|
<div class="card-body">
|
|
<h5 class="card-title mb-2">Расходные материалы для выбранного устройства</h5>
|
|
{% if consumable_groups %}
|
|
{% for ctype, items in consumable_groups.items() %}
|
|
<div class="mb-3">
|
|
<div class="fw-semibold mb-2">{{ ctype }}</div>
|
|
<div class="list-group">
|
|
{% for item in items %}
|
|
<div class="list-group-item">
|
|
<div><strong>Модель:</strong> {{ item.model or '—' }}</div>
|
|
<div><strong>Остаток:</strong> {{ item.qty if item.qty is not none else '—' }}</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="alert alert-warning mb-0">Для выбранного устройства расходники не настроены.</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% endif %}
|
|
|
|
{% if session.get('role') in ('admin','storekeeper') %}
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h5 class="card-title mb-2">Настройка соответствия (админ)</h5>
|
|
<form method="post" action="/catalog/map" class="row g-2 align-items-center">
|
|
<div class="col-md-5">
|
|
<select name="device" class="form-select">
|
|
<option value="">Модель устройства...</option>
|
|
{% for b, m, t in device_models %}
|
|
{% set brand = (b ~ " ") if b else "" %}
|
|
{% set label = (brand ~ m ~ " (" ~ ("Принтер" if t == "printer" else "МФУ") ~ ")") %}
|
|
{% set key = m ~ "||" ~ t %}
|
|
<option value="{{ key }}" {% if selected_key == key %}selected{% endif %}>
|
|
{{ label }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-5">
|
|
<select name="cartridge_model" class="form-select">
|
|
<option value="">Модель картриджа...</option>
|
|
{% for m in cartridges %}
|
|
<option value="{{ m }}" {% if cartridge_model == m %}selected{% endif %}>{{ m }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2 d-grid">
|
|
<button class="btn btn-success">Сохранить</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mt-3">
|
|
<div class="card-body">
|
|
<h5 class="card-title mb-2">Расходники для устройств (админ)</h5>
|
|
<form method="post" action="/catalog/consumables/map" class="row g-2 align-items-center">
|
|
<div class="col-md-4">
|
|
<select name="device" class="form-select">
|
|
<option value="">Модель устройства...</option>
|
|
{% for b, m, t in device_models %}
|
|
{% set brand = (b ~ " ") if b else "" %}
|
|
{% set label = (brand ~ m ~ " (" ~ ("Принтер" if t == "printer" else "МФУ") ~ ")") %}
|
|
{% set key = m ~ "||" ~ t %}
|
|
<option value="{{ key }}" {% if selected_key == key %}selected{% endif %}>
|
|
{{ label }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="d-flex gap-2">
|
|
<select class="form-select" data-consumable-type-select>
|
|
<option value="">Тип расходника...</option>
|
|
{% for t in consumable_types %}
|
|
<option value="{{ t }}">{{ t }}</option>
|
|
{% endfor %}
|
|
<option value="__custom__">Другое...</option>
|
|
</select>
|
|
<input class="form-control" placeholder="Свой тип" data-consumable-type-custom>
|
|
<input type="hidden" name="consumable_type" data-consumable-type-value>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<select name="consumable_barcode" class="form-select" data-consumable-barcode-select>
|
|
<option value="">Расходный материал...</option>
|
|
{% for b, m, t, q in consumables %}
|
|
<option value="{{ b }}" data-consumable-type="{{ t }}">{{ m }} ({{ b }})</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2 d-grid">
|
|
<button class="btn btn-success">Сохранить</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
<script>
|
|
(function() {
|
|
const select = document.querySelector('[data-consumable-type-select]');
|
|
if (!select) return;
|
|
const custom = document.querySelector('[data-consumable-type-custom]');
|
|
const value = document.querySelector('[data-consumable-type-value]');
|
|
const barcodeSelect = document.querySelector('[data-consumable-barcode-select]');
|
|
|
|
function sync() {
|
|
const sel = select.value;
|
|
if (sel === '__custom__') {
|
|
custom.disabled = false;
|
|
value.value = (custom.value || '').trim();
|
|
} else if (sel) {
|
|
custom.disabled = true;
|
|
value.value = sel;
|
|
} else {
|
|
custom.disabled = false;
|
|
value.value = (custom.value || '').trim();
|
|
}
|
|
filterConsumables();
|
|
}
|
|
|
|
function filterConsumables() {
|
|
if (!barcodeSelect) return;
|
|
const selectedType = (value.value || '').trim();
|
|
const normSelected = selectedType.toLowerCase();
|
|
const options = Array.from(barcodeSelect.options);
|
|
let hasMatch = false;
|
|
options.forEach((opt, idx) => {
|
|
if (idx === 0) return; // placeholder
|
|
const optType = (opt.getAttribute('data-consumable-type') || '').trim();
|
|
const normOpt = optType.toLowerCase();
|
|
const show = !selectedType || normOpt === normSelected;
|
|
opt.hidden = !show;
|
|
opt.disabled = !show;
|
|
if (show) hasMatch = true;
|
|
});
|
|
if (!hasMatch) {
|
|
barcodeSelect.value = '';
|
|
}
|
|
}
|
|
|
|
select.addEventListener('change', sync);
|
|
custom.addEventListener('input', sync);
|
|
sync();
|
|
})();
|
|
</script>
|
|
</div>
|
|
{% endblock %}
|