Files
converter_nginx/interface.html
2026-03-31 18:53:02 +04:00

925 lines
38 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.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Поиск товаров/услуг и конвертер ЕИ</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Commissioner:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root {
--blue-950: oklch(0.22 0.08 255);
--blue-900: oklch(0.28 0.10 255);
--blue-800: oklch(0.35 0.12 252);
--blue-700: oklch(0.42 0.14 250);
--blue-600: oklch(0.50 0.15 248);
--blue-500: oklch(0.58 0.14 248);
--blue-400: oklch(0.66 0.12 248);
--blue-300: oklch(0.75 0.08 250);
--blue-200: oklch(0.84 0.05 250);
--blue-100: oklch(0.92 0.025 250);
--blue-50: oklch(0.97 0.01 250);
--slate-900: oklch(0.25 0.02 255);
--slate-700: oklch(0.38 0.02 255);
--slate-600: oklch(0.48 0.02 255);
--slate-500: oklch(0.55 0.015 255);
--slate-400: oklch(0.65 0.01 255);
--slate-300: oklch(0.78 0.008 255);
--slate-200: oklch(0.88 0.005 255);
--slate-100: oklch(0.94 0.003 255);
--slate-50: oklch(0.98 0.002 255);
--green-600: oklch(0.55 0.16 155);
--green-100: oklch(0.93 0.04 155);
--amber-600: oklch(0.55 0.14 75);
--amber-100: oklch(0.93 0.04 75);
--red-600: oklch(0.55 0.18 25);
--red-100: oklch(0.93 0.05 25);
--surface: oklch(0.985 0.003 250);
--surface-raised: oklch(1 0 0);
--surface-sunken: oklch(0.96 0.006 250);
--text-primary: var(--slate-900);
--text-secondary: var(--slate-600);
--text-tertiary: var(--slate-500);
--text-on-accent: oklch(0.99 0.005 250);
--accent: var(--blue-600);
--accent-hover: var(--blue-700);
--accent-subtle: var(--blue-100);
--accent-border: var(--blue-200);
--border: var(--slate-200);
--border-focus: var(--blue-400);
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--shadow-sm: 0 1px 2px oklch(0.22 0.08 255 / 0.04);
--shadow-md: 0 2px 8px oklch(0.22 0.08 255 / 0.06), 0 1px 3px oklch(0.22 0.08 255 / 0.04);
--shadow-lg: 0 8px 32px oklch(0.22 0.08 255 / 0.08), 0 2px 8px oklch(0.22 0.08 255 / 0.04);
--shadow-focus: 0 0 0 3px oklch(0.58 0.14 248 / 0.18);
--font-body: 'Commissioner', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
html {
font-family: var(--font-body);
color: var(--text-primary);
background: var(--surface);
-webkit-font-smoothing: antialiased;
}
body { min-height: 100vh; display: flex; flex-direction: column; }
/* ── Header ── */
.app-header {
background: var(--blue-950);
color: var(--text-on-accent);
padding: 0 clamp(1.25rem, 4vw, 3rem);
height: 56px;
display: flex;
align-items: center;
gap: 12px;
position: sticky;
top: 0;
z-index: 100;
}
.app-header svg { width: 22px; height: 22px; opacity: 0.7; flex-shrink: 0; }
.app-header h1 { font-size: 0.9375rem; font-weight: 500; letter-spacing: 0.01em; opacity: 0.92; }
/* ── Tabs ── */
.tabs-nav {
display: flex;
gap: 0;
background: var(--surface-raised);
border-bottom: 1.5px solid var(--border);
padding: 0 clamp(1.25rem, 4vw, 3rem);
position: sticky;
top: 56px;
z-index: 90;
}
.tab-btn {
font-family: var(--font-body);
font-size: 0.875rem;
font-weight: 500;
color: var(--text-tertiary);
background: none;
border: none;
padding: 14px 20px;
cursor: pointer;
position: relative;
transition: color 0.15s;
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
}
.tab-btn svg { width: 16px; height: 16px; }
.tab-btn:hover { color: var(--text-primary); }
.tab-btn.active { color: var(--accent); }
.tab-btn.active::after {
content: '';
position: absolute;
bottom: -1.5px;
left: 0;
right: 0;
height: 2px;
background: var(--accent);
border-radius: 2px 2px 0 0;
}
/* ── Main ── */
.app-main {
flex: 1;
padding: clamp(1.5rem, 4vw, 2.5rem) clamp(1.25rem, 4vw, 3rem);
max-width: 1180px;
width: 100%;
margin: 0 auto;
}
.tab-panel { display: none; }
.tab-panel.active { display: block; animation: fadeUp 0.3s cubic-bezier(0.16, 1, 0.3, 1) both; }
@keyframes fadeUp {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── Form Elements ── */
.section-label {
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--blue-600);
margin-bottom: 0.75rem;
}
.form-card {
background: var(--surface-raised);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: clamp(1.25rem, 3vw, 2rem);
box-shadow: var(--shadow-sm);
}
.field { display: flex; flex-direction: column; gap: 6px; }
.field-label {
font-size: 0.8125rem;
font-weight: 500;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 4px;
}
.field-label .req { color: var(--blue-500); font-size: 0.75rem; }
input[type="text"],
input[type="number"],
select,
textarea {
font-family: var(--font-body);
font-size: 0.9375rem;
color: var(--text-primary);
background: var(--surface-sunken);
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
padding: 10px 14px;
transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
width: 100%;
outline: none;
}
input::placeholder, textarea::placeholder { color: var(--slate-400); font-weight: 300; }
input:hover, select:hover, textarea:hover { border-color: var(--slate-300); background: var(--surface-raised); }
input:focus, select:focus, textarea:focus { border-color: var(--border-focus); background: var(--surface-raised); box-shadow: var(--shadow-focus); }
select {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5l5 5 5-5' stroke='%23788599' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 36px;
cursor: pointer;
}
/* ── Buttons ── */
.btn {
font-family: var(--font-body);
font-size: 0.875rem;
font-weight: 600;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px 20px;
transition: all 0.15s;
white-space: nowrap;
outline: none;
}
.btn:focus-visible { box-shadow: var(--shadow-focus); }
.btn svg { width: 16px; height: 16px; flex-shrink: 0; }
.btn-primary { background: var(--accent); color: var(--text-on-accent); }
.btn-primary:hover { background: var(--accent-hover); }
.btn-primary:active { transform: scale(0.98); }
.btn-primary:disabled { background: var(--slate-300); color: var(--slate-500); cursor: not-allowed; transform: none; }
.btn-secondary { background: transparent; color: var(--accent); border: 1.5px solid var(--accent-border); }
.btn-secondary:hover { background: var(--accent-subtle); border-color: var(--blue-300); }
.btn-ghost { background: transparent; color: var(--text-secondary); padding: 8px 12px; }
.btn-ghost:hover { color: var(--accent); background: var(--accent-subtle); }
.btn-sm { font-size: 0.8125rem; padding: 7px 14px; }
/* ── Grid helpers ── */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; }
.grid-search { display: grid; grid-template-columns: 1fr 180px; gap: 1rem; align-items: end; }
@media (max-width: 640px) {
.grid-2, .grid-3, .grid-search { grid-template-columns: 1fr; }
}
/* ── Advanced toggle ── */
.advanced-toggle {
font-size: 0.8125rem; font-weight: 500; color: var(--text-secondary);
background: none; border: none; cursor: pointer;
display: flex; align-items: center; gap: 6px;
padding: 8px 0; font-family: var(--font-body); transition: color 0.15s;
}
.advanced-toggle:hover { color: var(--accent); }
.advanced-toggle svg { width: 14px; height: 14px; transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1); }
.advanced-toggle[aria-expanded="true"] svg { transform: rotate(180deg); }
.collapse-panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.35s cubic-bezier(0.16, 1, 0.3, 1); }
.collapse-panel[data-open="true"] { grid-template-rows: 1fr; }
.collapse-inner { overflow: hidden; }
.advanced-fields {
padding-top: 1.25rem; margin-top: 1.25rem;
border-top: 1px solid var(--border);
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;
}
.form-actions { display: flex; align-items: center; gap: 12px; margin-top: 1.25rem; flex-wrap: wrap; }
/* ── Filters bar ── */
.filters-bar {
background: var(--surface-raised);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: clamp(1rem, 2vw, 1.5rem);
margin-bottom: 1rem;
box-shadow: var(--shadow-sm);
}
.filters-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 0.875rem; align-items: end; }
.filters-actions { display: flex; gap: 8px; align-items: end; padding-top: 6px; }
.date-chips { display: flex; gap: 6px; flex-wrap: wrap; }
.chip {
font-family: var(--font-body); font-size: 0.75rem; font-weight: 500;
padding: 5px 12px; border-radius: 100px;
border: 1.5px solid var(--border); background: var(--surface-raised);
color: var(--text-secondary); cursor: pointer; transition: all 0.15s; white-space: nowrap;
}
.chip:hover { border-color: var(--blue-300); color: var(--accent); }
.chip.active { background: var(--accent-subtle); border-color: var(--blue-300); color: var(--blue-700); }
/* ── Table ── */
.table-wrap {
background: var(--surface-raised);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.table-scroll { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
thead { position: sticky; top: 0; z-index: 2; }
th {
background: var(--blue-50); font-size: 0.75rem; font-weight: 600;
letter-spacing: 0.03em; color: var(--text-secondary);
text-align: left; padding: 12px 16px;
border-bottom: 1.5px solid var(--blue-200); white-space: nowrap;
}
th:first-child, td:first-child { padding-left: 20px; }
td { padding: 12px 16px; border-bottom: 1px solid var(--slate-100); vertical-align: middle; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: oklch(0.97 0.008 250); }
.cell-name { font-weight: 500; max-width: 360px; }
.cell-num { font-family: var(--font-mono); font-size: 0.8125rem; color: var(--text-secondary); text-align: center; }
.cell-region { color: var(--text-secondary); }
.cell-unit { font-size: 0.8125rem; }
.cell-qty { font-family: var(--font-mono); font-size: 0.8125rem; text-align: right; }
.cell-date { font-family: var(--font-mono); font-size: 0.8125rem; color: var(--text-secondary); white-space: nowrap; }
.table-footer {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 20px; border-top: 1px solid var(--border);
font-size: 0.8125rem; color: var(--text-tertiary);
}
/* ── Result card (converter) ── */
.result-card {
background: var(--surface-raised);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: clamp(1.5rem, 3vw, 2rem);
box-shadow: var(--shadow-sm);
margin-top: 1.5rem;
}
.coeff-display {
display: flex;
align-items: baseline;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 0.5rem;
}
.coeff-value {
font-family: var(--font-mono);
font-size: clamp(2rem, 5vw, 3rem);
font-weight: 700;
color: var(--blue-700);
line-height: 1;
}
.coeff-label {
font-size: 0.9375rem;
color: var(--text-secondary);
line-height: 1.5;
}
.coeff-formula {
font-size: 0.8125rem;
color: var(--text-tertiary);
font-family: var(--font-mono);
margin-top: 0.5rem;
padding: 8px 12px;
background: var(--surface-sunken);
border-radius: var(--radius-sm);
display: inline-block;
}
/* ── Logs panel ── */
.logs-section { margin-top: 1.5rem; }
.logs-toggle {
font-family: var(--font-body); font-size: 0.8125rem; font-weight: 500;
color: var(--text-secondary); background: none; border: none;
cursor: pointer; display: flex; align-items: center; gap: 8px;
padding: 8px 0; transition: color 0.15s;
}
.logs-toggle:hover { color: var(--accent); }
.logs-toggle svg { width: 14px; height: 14px; transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1); }
.logs-toggle[aria-expanded="true"] svg { transform: rotate(180deg); }
.logs-badge {
font-size: 0.6875rem; font-weight: 600;
background: var(--slate-100); color: var(--text-tertiary);
padding: 1px 7px; border-radius: 100px;
}
.logs-container {
margin-top: 0.75rem;
background: var(--slate-900);
border-radius: var(--radius-md);
overflow: hidden;
box-shadow: var(--shadow-md);
}
.logs-header {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 16px;
border-bottom: 1px solid oklch(0.35 0.02 255);
}
.logs-header span {
font-size: 0.75rem; font-weight: 600;
letter-spacing: 0.04em; text-transform: uppercase;
color: var(--slate-400);
}
.logs-body {
padding: 12px 16px;
max-height: 320px;
overflow-y: auto;
font-family: var(--font-mono);
font-size: 0.75rem;
line-height: 1.8;
color: var(--slate-300);
}
.logs-body::-webkit-scrollbar { width: 6px; }
.logs-body::-webkit-scrollbar-track { background: transparent; }
.logs-body::-webkit-scrollbar-thumb { background: oklch(0.4 0.02 255); border-radius: 3px; }
.log-line { display: flex; gap: 10px; }
.log-time { color: var(--slate-500); flex-shrink: 0; }
.log-level { font-weight: 500; flex-shrink: 0; min-width: 44px; }
.log-level.info { color: var(--blue-400); }
.log-level.ok { color: var(--green-600); }
.log-level.warn { color: var(--amber-600); }
.log-level.err { color: var(--red-600); }
.log-msg { color: oklch(0.82 0.01 255); }
/* ── Empty state ── */
.empty-state { text-align: center; padding: 4rem 2rem; }
.empty-state svg { width: 48px; height: 48px; color: var(--slate-300); margin-bottom: 1rem; }
.empty-state p { color: var(--text-tertiary); font-size: 0.9375rem; max-width: 380px; margin: 0 auto; line-height: 1.6; }
.empty-state p strong { color: var(--text-secondary); font-weight: 500; }
.hidden { display: none !important; }
/* ── Responsive ── */
@media (max-width: 768px) {
.filters-grid { grid-template-columns: 1fr 1fr; }
.form-actions { flex-direction: column; align-items: stretch; }
.form-actions .btn { width: 100%; }
}
@media (max-width: 480px) {
.filters-grid { grid-template-columns: 1fr; }
.tabs-nav { gap: 0; overflow-x: auto; }
.tab-btn { padding: 12px 14px; font-size: 0.8125rem; }
}
</style>
</head>
<body>
<!-- ═══ Header ═══ -->
<header class="app-header">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
</svg>
<h1>Поиск товаров/услуг и конвертер ЕИ</h1>
</header>
<!-- ═══ Tabs ═══ -->
<nav class="tabs-nav">
<button class="tab-btn active" data-tab="search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
</svg>
Поиск товаров/услуг
</button>
<button class="tab-btn" data-tab="convert">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/>
<polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/>
</svg>
Конвертер ЕИ
</button>
</nav>
<!-- ═══ Main ═══ -->
<main class="app-main">
<!-- ════════════════════════════════════ -->
<!-- TAB 1: ПОИСК ТОВАРОВ/УСЛУГ -->
<!-- ════════════════════════════════════ -->
<div class="tab-panel active" id="panel-search">
<div class="section-label">Параметры поиска</div>
<form class="form-card" id="searchForm" autocomplete="off">
<div class="grid-search">
<div class="field">
<label class="field-label" for="s_product">Наименование товара/услуги <span class="req">*</span></label>
<input type="text" id="s_product" placeholder="Введите наименование товара или услуги" required>
</div>
<div class="field">
<label class="field-label" for="s_count">Кол-во записей <span class="req">*</span></label>
<input type="number" id="s_count" placeholder="10" min="1" max="500" value="20">
</div>
</div>
<!-- Advanced -->
<div class="collapse-panel" id="s_advPanel" data-open="false">
<div class="collapse-inner">
<div class="advanced-fields">
<div class="field">
<label class="field-label" for="s_region">Регион</label>
<select id="s_region">
<option value="">Все регионы</option>
<option>Москва</option><option>Санкт-Петербург</option>
<option>Новосибирская область</option><option>Свердловская область</option>
<option>Краснодарский край</option><option>Республика Татарстан</option>
</select>
</div>
<div class="field">
<label class="field-label" for="s_unit">Единица измерения</label>
<select id="s_unit">
<option value="">Любая</option>
<option>шт</option><option>кг</option><option>л</option>
<option>м</option><option>упак</option><option>компл</option>
</select>
</div>
<div class="field">
<label class="field-label" for="s_qty">Количество</label>
<input type="number" id="s_qty" placeholder="Например, 100" min="0">
</div>
</div>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" id="searchBtn" disabled>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
</svg>
Найти похожие товары/услуги
</button>
<button type="button" class="advanced-toggle" id="s_advToggle" aria-expanded="false">
<span>Дополнительные параметры</span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
</div>
</form>
<!-- Search results -->
<div id="s_empty" class="empty-state" style="margin-top:2rem">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/><path d="M11 8v6"/><path d="M8 11h6"/>
</svg>
<p><strong>Введите наименование товара или услуги</strong> и нажмите «Найти» — система подберёт похожие позиции из реестра закупок</p>
</div>
<div class="hidden" id="s_results" style="margin-top:1.5rem">
<div style="display:flex;align-items:baseline;gap:12px;margin-bottom:1.25rem;flex-wrap:wrap">
<div class="section-label" style="margin-bottom:0">Результаты</div>
<span style="font-size:0.8125rem;color:var(--text-tertiary)">Найдено <strong id="s_totalCount" style="font-family:var(--font-mono);font-size:0.8rem;color:var(--text-primary);font-weight:600">0</strong> записей</span>
</div>
<!-- Filters -->
<div class="filters-bar">
<div class="filters-grid">
<div class="field">
<label class="field-label">Дата закупки</label>
<div class="date-chips">
<button type="button" class="chip" data-days="7">Неделя</button>
<button type="button" class="chip" data-days="30">Месяц</button>
<button type="button" class="chip active" data-days="90">3 месяца</button>
</div>
</div>
<div class="field">
<label class="field-label" for="f_region">Регион</label>
<select id="f_region"><option value="">Все</option></select>
</div>
<div class="field">
<label class="field-label" for="f_unit">Ед. измерения</label>
<select id="f_unit"><option value="">Все</option></select>
</div>
<div class="field">
<label class="field-label" for="f_qtyMin">Кол-во от</label>
<input type="number" id="f_qtyMin" placeholder="мин">
</div>
<div class="filters-actions">
<button type="button" class="btn btn-primary btn-sm" id="applyFilters">Применить</button>
<button type="button" class="btn btn-ghost btn-sm" id="resetFilters">Сбросить</button>
</div>
</div>
</div>
<!-- Table -->
<div class="table-wrap">
<div class="table-scroll">
<table>
<thead>
<tr>
<th style="width:52px">#</th>
<th>Наименование товара/услуги</th>
<th>Регион</th>
<th>Ед. изм.</th>
<th style="text-align:right">Кол-во</th>
<th>Дата закупки</th>
</tr>
</thead>
<tbody id="s_tbody"></tbody>
</table>
</div>
<div class="table-footer">
<span id="s_shownInfo">Показано 0 из 0</span>
</div>
</div>
</div>
</div>
<!-- ════════════════════════════════════ -->
<!-- TAB 2: КОНВЕРТЕР ЕИ -->
<!-- ════════════════════════════════════ -->
<div class="tab-panel" id="panel-convert">
<div class="section-label">Конвертер единиц измерения</div>
<form class="form-card" id="convertForm" autocomplete="off">
<div class="field" style="margin-bottom:1rem">
<label class="field-label" for="c_product">Наименование товара/услуги <span class="req">*</span></label>
<input type="text" id="c_product" placeholder="Введите наименование товара или услуги" required>
</div>
<div class="grid-3">
<div class="field">
<label class="field-label" for="c_count">Кол-во записей</label>
<input type="number" id="c_count" placeholder="10" min="1" max="500" value="20">
</div>
<div class="field">
<label class="field-label" for="c_sourceUnit">Исходная ЕИ <span class="req">*</span></label>
<select id="c_sourceUnit" required>
<option value="">Выберите...</option>
<option>шт</option><option>кг</option><option>г</option>
<option>л</option><option>мл</option><option>м</option>
<option>см</option><option>упак</option><option>компл</option>
</select>
</div>
<div class="field">
<label class="field-label" for="c_targetUnit">Целевая ЕИ <span class="req">*</span></label>
<select id="c_targetUnit" required>
<option value="">Выберите...</option>
<option>шт</option><option>кг</option><option>г</option>
<option>л</option><option>мл</option><option>м</option>
<option>см</option><option>упак</option><option>компл</option>
</select>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" id="convertBtn" disabled>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/>
<polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/>
</svg>
Конвертировать
</button>
</div>
</form>
<!-- Converter empty state -->
<div id="c_empty" class="empty-state" style="margin-top:2rem">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/>
<polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/>
</svg>
<p><strong>Укажите товар/услугу и единицы измерения</strong> — система рассчитает коэффициент пересчёта на основе данных из реестра закупок</p>
</div>
<!-- Converter result -->
<div class="hidden" id="c_result">
<!-- Coefficient -->
<div class="result-card">
<div style="font-size:0.75rem;font-weight:600;letter-spacing:0.06em;text-transform:uppercase;color:var(--text-tertiary);margin-bottom:1rem">Коэффициент пересчёта</div>
<div class="coeff-display">
<span class="coeff-value" id="c_coeffValue"></span>
<span class="coeff-label" id="c_coeffLabel"></span>
</div>
<div class="coeff-formula" id="c_formula"></div>
</div>
<!-- Logs -->
<div class="logs-section">
<button type="button" class="logs-toggle" id="logsToggle" aria-expanded="false">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2z"/>
<path d="M9 17h6"/><path d="M9 13h6"/>
</svg>
<span>Логи бэкенда</span>
<span class="logs-badge" id="logsBadge">0</span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
<div class="collapse-panel" id="logsPanel" data-open="false">
<div class="collapse-inner">
<div class="logs-container">
<div class="logs-header">
<span>Backend logs</span>
<button class="btn btn-ghost btn-sm" style="color:var(--slate-400);font-size:0.75rem;padding:4px 8px" id="copyLogs">Копировать</button>
</div>
<div class="logs-body" id="logsBody"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<script>
// ═══ Tabs ═══
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('panel-' + btn.dataset.tab).classList.add('active');
});
});
// ═══ TAB 1: Search ═══
const DEMO_DATA = [
{ name: 'Бумага офисная А4, 80 г/м², 500 листов', region: 'Москва', unit: 'упак', qty: 500, date: '2026-03-15' },
{ name: 'Бумага писчая А4 «Снегурочка»', region: 'Санкт-Петербург', unit: 'упак', qty: 200, date: '2026-03-10' },
{ name: 'Бумага для принтера А4, класс B', region: 'Новосибирская область', unit: 'упак', qty: 1000, date: '2026-02-28' },
{ name: 'Бумага офисная А3, 80 г/м²', region: 'Краснодарский край', unit: 'упак', qty: 150, date: '2026-02-20' },
{ name: 'Бумага для копировальных аппаратов А4', region: 'Москва', unit: 'шт', qty: 3000, date: '2026-02-14' },
{ name: 'Бумага SvetoCopy А4 500 л.', region: 'Свердловская область', unit: 'упак', qty: 800, date: '2026-01-30' },
{ name: 'Бумага мелованная А4 для лазерной печати', region: 'Республика Татарстан', unit: 'упак', qty: 50, date: '2026-01-22' },
{ name: 'Бумага офисная А4 «Ballet Classic»', region: 'Москва', unit: 'упак', qty: 2400, date: '2026-01-15' },
{ name: 'Бумага цветная А4, 10 цветов', region: 'Санкт-Петербург', unit: 'упак', qty: 120, date: '2026-01-08' },
{ name: 'Бумага для заметок 76×76 мм, жёлтая', region: 'Краснодарский край', unit: 'шт', qty: 5000, date: '2025-12-29' },
{ name: 'Бумага А4 для факсимильных аппаратов', region: 'Новосибирская область', unit: 'упак', qty: 300, date: '2025-12-20' },
{ name: 'Бумага крафт упаковочная 84 см', region: 'Москва', unit: 'м', qty: 1500, date: '2025-12-15' },
];
let allResults = [], filteredResults = [], activeDateFilter = 90;
const searchForm = document.getElementById('searchForm');
const searchBtn = document.getElementById('searchBtn');
const s_product = document.getElementById('s_product');
// Validate
function validateSearch() { searchBtn.disabled = !s_product.value.trim(); }
s_product.addEventListener('input', validateSearch);
// Advanced toggle
document.getElementById('s_advToggle').addEventListener('click', function() {
const panel = document.getElementById('s_advPanel');
const open = panel.dataset.open === 'true';
panel.dataset.open = String(!open);
this.setAttribute('aria-expanded', String(!open));
});
// Submit search
searchForm.addEventListener('submit', (e) => {
e.preventDefault();
const count = parseInt(document.getElementById('s_count').value) || 20;
allResults = DEMO_DATA.slice(0, Math.min(count, DEMO_DATA.length));
const regions = [...new Set(allResults.map(r => r.region))];
const units = [...new Set(allResults.map(r => r.unit))];
document.getElementById('f_region').innerHTML = '<option value="">Все</option>' + regions.map(r => `<option>${r}</option>`).join('');
document.getElementById('f_unit').innerHTML = '<option value="">Все</option>' + units.map(u => `<option>${u}</option>`).join('');
applySearchFilters();
document.getElementById('s_empty').classList.add('hidden');
document.getElementById('s_results').classList.remove('hidden');
document.getElementById('s_results').style.animation = 'none';
requestAnimationFrame(() => { document.getElementById('s_results').style.animation = 'fadeUp 0.3s cubic-bezier(0.16,1,0.3,1) both'; });
});
// Date chips
document.querySelectorAll('.chip[data-days]').forEach(chip => {
chip.addEventListener('click', () => {
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
activeDateFilter = parseInt(chip.dataset.days);
});
});
function applySearchFilters() {
const rVal = document.getElementById('f_region').value;
const uVal = document.getElementById('f_unit').value;
const qMin = parseFloat(document.getElementById('f_qtyMin').value) || 0;
const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - activeDateFilter);
filteredResults = allResults.filter(r => {
if (rVal && r.region !== rVal) return false;
if (uVal && r.unit !== uVal) return false;
if (qMin && r.qty < qMin) return false;
if (new Date(r.date) < cutoff) return false;
return true;
});
renderSearchTable();
}
document.getElementById('applyFilters').addEventListener('click', applySearchFilters);
document.getElementById('resetFilters').addEventListener('click', () => {
document.getElementById('f_region').value = '';
document.getElementById('f_unit').value = '';
document.getElementById('f_qtyMin').value = '';
activeDateFilter = 90;
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
document.querySelector('.chip[data-days="90"]').classList.add('active');
applySearchFilters();
});
function renderSearchTable() {
document.getElementById('s_totalCount').textContent = filteredResults.length;
document.getElementById('s_shownInfo').textContent = `Показано ${filteredResults.length} из ${allResults.length}`;
const tbody = document.getElementById('s_tbody');
if (!filteredResults.length) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;padding:2.5rem;color:var(--text-tertiary)">Нет записей</td></tr>';
return;
}
tbody.innerHTML = filteredResults.map((r, i) => `
<tr>
<td class="cell-num">${i + 1}</td>
<td class="cell-name">${r.name}</td>
<td class="cell-region">${r.region}</td>
<td class="cell-unit">${r.unit}</td>
<td class="cell-qty">${r.qty.toLocaleString('ru-RU')}</td>
<td class="cell-date">${new Date(r.date).toLocaleDateString('ru-RU', {day:'2-digit',month:'2-digit',year:'numeric'})}</td>
</tr>
`).join('');
}
// ═══ TAB 2: Converter ═══
const convertForm = document.getElementById('convertForm');
const convertBtn = document.getElementById('convertBtn');
const c_product = document.getElementById('c_product');
const c_src = document.getElementById('c_sourceUnit');
const c_tgt = document.getElementById('c_targetUnit');
function validateConvert() {
convertBtn.disabled = !(c_product.value.trim() && c_src.value && c_tgt.value);
}
[c_product, c_src, c_tgt].forEach(el => el.addEventListener('input', validateConvert));
[c_src, c_tgt].forEach(el => el.addEventListener('change', validateConvert));
// Demo logs
function generateLogs(product, src, tgt) {
const now = new Date();
const t = (offset) => {
const d = new Date(now.getTime() - offset);
return d.toLocaleTimeString('ru-RU', {hour:'2-digit',minute:'2-digit',second:'2-digit'}) + '.' + String(d.getMilliseconds()).padStart(3,'0');
};
return [
{ time: t(2400), level: 'info', msg: `Получен запрос на конвертацию: "${product}"` },
{ time: t(2100), level: 'info', msg: `Параметры: source_unit=${src}, target_unit=${tgt}` },
{ time: t(1800), level: 'info', msg: 'Отправка запроса в Elasticsearch...' },
{ time: t(1200), level: 'ok', msg: `Elasticsearch: найдено 847 записей за 0.124s` },
{ time: t(1000), level: 'info', msg: 'Фильтрация записей с совпадающими ЕИ...' },
{ time: t(800), level: 'info', msg: `Отобрано 312 записей с ЕИ "${src}" и 89 с ЕИ "${tgt}"` },
{ time: t(600), level: 'info', msg: 'Расчёт медианного коэффициента пересчёта...' },
{ time: t(400), level: 'warn', msg: '3 выброса исключены из расчёта (отклонение > 3σ)' },
{ time: t(200), level: 'ok', msg: `Коэффициент рассчитан: 1 ${src} = 0.45 ${tgt}` },
{ time: t(50), level: 'ok', msg: 'Запрос выполнен успешно (2.35s)' },
];
}
convertForm.addEventListener('submit', (e) => {
e.preventDefault();
const product = c_product.value.trim();
const src = c_src.value;
const tgt = c_tgt.value;
// Show result
document.getElementById('c_empty').classList.add('hidden');
const resultEl = document.getElementById('c_result');
resultEl.classList.remove('hidden');
resultEl.style.animation = 'none';
requestAnimationFrame(() => { resultEl.style.animation = 'fadeUp 0.3s cubic-bezier(0.16,1,0.3,1) both'; });
// Demo coefficient
const coeff = 0.45;
document.getElementById('c_coeffValue').textContent = coeff;
document.getElementById('c_coeffLabel').innerHTML = `${src}${tgt}`;
document.getElementById('c_formula').textContent = `1 ${src} = ${coeff} ${tgt} • 1 ${tgt} = ${(1/coeff).toFixed(4)} ${src}`;
// Logs
const logs = generateLogs(product, src, tgt);
document.getElementById('logsBadge').textContent = logs.length;
document.getElementById('logsBody').innerHTML = logs.map(l =>
`<div class="log-line"><span class="log-time">${l.time}</span><span class="log-level ${l.level}">${l.level.toUpperCase().padEnd(4)}</span><span class="log-msg">${l.msg}</span></div>`
).join('');
});
// Logs toggle
document.getElementById('logsToggle').addEventListener('click', function() {
const panel = document.getElementById('logsPanel');
const open = panel.dataset.open === 'true';
panel.dataset.open = String(!open);
this.setAttribute('aria-expanded', String(!open));
});
// Copy logs
document.getElementById('copyLogs').addEventListener('click', () => {
const text = [...document.querySelectorAll('#logsBody .log-line')].map(l => l.textContent).join('\n');
navigator.clipboard.writeText(text).then(() => {
const btn = document.getElementById('copyLogs');
btn.textContent = 'Скопировано';
setTimeout(() => btn.textContent = 'Копировать', 1500);
});
});
</script>
</body>
</html>