Files
converter_nginx/www/interface.html

795 lines
31 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="field">
<label class="field-label" for="s_product">Наименование товара/услуги <span class="req">*</span></label>
<input type="text" id="s_product" placeholder="Введите наименование товара или услуги" required>
</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" 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="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>Наименование (raw)</th>
<th>Норм. наименование</th>
<th>Регион</th>
<th>Ед. изм.</th>
<th>ID контракта</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_sourceUnit">Исходная ЕИ <span class="req">*</span></label>
<input type="text" id="c_sourceUnit" placeholder="Например: кг" required>
</div>
<div class="field">
<label class="field-label" for="c_targetUnit">Целевая ЕИ <span class="req">*</span></label>
<input type="text" id="c_targetUnit" placeholder="Например: шт" required>
</div>
<div class="field">
<label class="field-label" for="c_attributes">Характеристики</label>
<input type="text" id="c_attributes" placeholder="Например: фракция 0-40, объём 500мл">
</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 (будет подключён когда появится API) -->
<div class="hidden" id="c_result">
<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>
</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 ═══
let allResults = [], filteredResults = [];
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', async (e) => {
e.preventDefault();
const queryText = s_product.value.trim();
const topK = 20;
searchBtn.disabled = true;
searchBtn.textContent = 'Поиск…';
try {
const resp = await fetch('/app/api/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query_text: queryText, top_k: topK, use_rerank: false, rerank_strategy: 'lexical' })
});
if (!resp.ok) throw new Error('Ошибка сервера: ' + resp.status);
const data = await resp.json();
allResults = data.results || [];
} catch (err) {
alert('Не удалось выполнить поиск: ' + err.message);
searchBtn.disabled = false;
searchBtn.innerHTML = '<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> Найти похожие товары/услуги';
return;
}
searchBtn.disabled = false;
searchBtn.innerHTML = '<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> Найти похожие товары/услуги';
const regions = [...new Set(allResults.map(r => r.region).filter(Boolean))];
const units = [...new Set(allResults.map(r => r.unit).filter(Boolean))];
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'; });
});
function applySearchFilters() {
const rVal = document.getElementById('f_region').value;
const uVal = document.getElementById('f_unit').value;
filteredResults = allResults.filter(r => {
if (rVal && r.region !== rVal) return false;
if (uVal && r.unit !== uVal) 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 = '';
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.raw_name || ''}</td>
<td class="cell-name">${r.norm_name || ''}</td>
<td class="cell-region">${r.region || '—'}</td>
<td class="cell-unit">${r.unit || ''}</td>
<td class="cell-num">${r.contract_id || ''}</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.trim() && c_tgt.value.trim());
}
[c_product, c_src, c_tgt].forEach(el => el.addEventListener('input', validateConvert));
convertForm.addEventListener('submit', (e) => {
e.preventDefault();
// TODO: подключить API конвертера, когда будет готов endpoint
});
</script>
</body>
</html>